backend/tools/test_lint_no_new_env_fields.py
Rama master · réplica de lectura del repositorio original.
"""Self-test for the no-new-env-fields AST linter.
Builds a handful of synthetic ``config_settings.py`` variants and asserts
the linter reports the right violations on each.
Run with: ``python tools/test_lint_no_new_env_fields.py``.
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from lint_no_new_env_fields import lint # noqa: E402
# fmt: off
GOOD = """
from pydantic import BaseModel
from pydantic_settings import BaseSettings
_ALLOWED_BOOT_FIELDS = frozenset({"port", "api_key"})
class BootSettings(BaseSettings):
port: int = 5765
api_key: str = ""
class UISettings(BaseModel):
items_per_page: int = 25
target_language: str = "de"
class Settings:
pass
"""
EXTRA_BOOT_FIELD = """
from pydantic import BaseModel
from pydantic_settings import BaseSettings
_ALLOWED_BOOT_FIELDS = frozenset({"port"})
class BootSettings(BaseSettings):
port: int = 5765
smuggled_field: str = "" # NOT in allowlist
class UISettings(BaseModel):
foo: int = 1
"""
UI_SUBCLASSES_BASESETTINGS = """
from pydantic_settings import BaseSettings
_ALLOWED_BOOT_FIELDS = frozenset({"port"})
class BootSettings(BaseSettings):
port: int = 5765
class UISettings(BaseSettings): # WRONG — would re-enable env loading
foo: int = 1
"""
OTHER_CLASS_SUBCLASSES_BASESETTINGS = """
from pydantic import BaseModel
from pydantic_settings import BaseSettings
_ALLOWED_BOOT_FIELDS = frozenset({"port"})
class BootSettings(BaseSettings):
port: int = 5765
class UISettings(BaseModel):
foo: int = 1
class SneakySettings(BaseSettings): # WRONG — only BootSettings may
debug: bool = False
"""
MISSING_ALLOWLIST = """
from pydantic import BaseModel
from pydantic_settings import BaseSettings
class BootSettings(BaseSettings):
port: int = 5765
class UISettings(BaseModel):
foo: int = 1
"""
ALLOWLIST_MISSING_FIELD = """
from pydantic import BaseModel
from pydantic_settings import BaseSettings
_ALLOWED_BOOT_FIELDS = frozenset({"port", "api_key"})
class BootSettings(BaseSettings):
port: int = 5765
# api_key declared in allowlist but missing on the class
class UISettings(BaseModel):
foo: int = 1
"""
# fmt: on
def _write(content: str) -> Path:
fd, name = tempfile.mkstemp(suffix=".py")
os.close(fd) # Windows: leaving the fd open blocks the later unlink().
f = Path(name)
f.write_text(content, encoding="utf-8")
return f
def main() -> int:
failures: list[str] = []
# 1. GOOD — no violations expected.
p = _write(GOOD)
v = lint(p)
if v:
failures.append(f"GOOD case must be clean, got {v}")
p.unlink()
# 2. Boot field outside allowlist — expect 1 violation mentioning the field.
p = _write(EXTRA_BOOT_FIELD)
v = lint(p)
if not any("smuggled_field" in m for m in v):
failures.append(f"EXTRA_BOOT_FIELD must flag smuggled_field, got {v}")
p.unlink()
# 3. UISettings(BaseSettings) — expect violation about UISettings.
p = _write(UI_SUBCLASSES_BASESETTINGS)
v = lint(p)
if not any("UISettings must NOT subclass BaseSettings" in m for m in v):
failures.append(f"UI_SUBCLASSES_BASESETTINGS must flag UISettings, got {v}")
p.unlink()
# 4. Some other class subclasses BaseSettings — expect violation naming it.
p = _write(OTHER_CLASS_SUBCLASSES_BASESETTINGS)
v = lint(p)
if not any("SneakySettings" in m for m in v):
failures.append(f"OTHER_CLASS must flag SneakySettings, got {v}")
p.unlink()
# 5. Allowlist missing entirely.
p = _write(MISSING_ALLOWLIST)
v = lint(p)
if not any("_ALLOWED_BOOT_FIELDS" in m for m in v):
failures.append(f"MISSING_ALLOWLIST must flag missing allowlist, got {v}")
p.unlink()
# 6. Allowlist names a field BootSettings doesn't declare.
p = _write(ALLOWLIST_MISSING_FIELD)
v = lint(p)
if not any("missing fields" in m for m in v):
failures.append(f"ALLOWLIST_MISSING_FIELD must flag missing fields, got {v}")
p.unlink()
if failures:
for f in failures:
print(f"FAIL: {f}", file=sys.stderr)
return 1
print("no-new-env-fields linter self-test: ok (6 cases checked)")
return 0
if __name__ == "__main__":
sys.exit(main())