mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
fix(lint): let an explicit frozen=False override earlier or inherited frozen=True in LIT013
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
c8cbdb3897
commit
997c1cab9b
2 changed files with 60 additions and 28 deletions
|
|
@ -1073,25 +1073,23 @@ def _pydantic_classes(tree: ast.AST) -> tuple[ast.ClassDef, ...]:
|
|||
return tuple(cls for cls in classes if cls.name in names)
|
||||
|
||||
|
||||
def _model_config_is_frozen(value: ast.expr) -> bool:
|
||||
"""`ConfigDict(frozen=True)` in any kwarg position, or a dict literal carrying
|
||||
`"frozen": True`."""
|
||||
def _bool_constant(value: ast.expr) -> bool | None:
|
||||
return value.value if isinstance(value, ast.Constant) and isinstance(value.value, bool) else None
|
||||
|
||||
|
||||
def _model_config_frozen(value: ast.expr) -> bool | None:
|
||||
"""The `frozen` flag a `ConfigDict(...)` call or dict literal sets, None when it sets none."""
|
||||
if isinstance(value, ast.Call) and _head_name(value.func) == "ConfigDict":
|
||||
return any(
|
||||
kw.arg == "frozen"
|
||||
and isinstance(kw.value, ast.Constant)
|
||||
and kw.value.value is True
|
||||
for kw in value.keywords
|
||||
)
|
||||
flags = tuple(_bool_constant(kw.value) for kw in value.keywords if kw.arg == "frozen")
|
||||
return flags[-1] if flags else None
|
||||
if isinstance(value, ast.Dict):
|
||||
return any(
|
||||
isinstance(key, ast.Constant)
|
||||
and key.value == "frozen"
|
||||
and isinstance(item, ast.Constant)
|
||||
and item.value is True
|
||||
flags = tuple(
|
||||
_bool_constant(item)
|
||||
for key, item in zip(value.keys, value.values)
|
||||
if isinstance(key, ast.Constant) and key.value == "frozen"
|
||||
)
|
||||
return False
|
||||
return flags[-1] if flags else None
|
||||
return None
|
||||
|
||||
|
||||
def _assigns_name(stmt: ast.stmt, name: str) -> ast.expr | None:
|
||||
|
|
@ -1103,33 +1101,44 @@ def _assigns_name(stmt: ast.stmt, name: str) -> ast.expr | None:
|
|||
return None
|
||||
|
||||
|
||||
def _config_class_is_frozen(node: ast.ClassDef) -> bool:
|
||||
"""An inner `class Config:` counts only when it binds `frozen = True`."""
|
||||
return any(
|
||||
isinstance(value, ast.Constant) and value.value is True
|
||||
def _config_class_frozen(node: ast.ClassDef) -> bool | None:
|
||||
"""The `frozen = ...` flag an inner `class Config:` binds, None when it binds none."""
|
||||
flags = tuple(
|
||||
_bool_constant(value)
|
||||
for stmt in node.body
|
||||
for value in (_assigns_name(stmt, "frozen"),)
|
||||
if value is not None
|
||||
)
|
||||
return flags[-1] if flags else None
|
||||
|
||||
|
||||
def _class_is_frozen(cls: ast.ClassDef) -> bool:
|
||||
for stmt in cls.body:
|
||||
config_value = _assigns_name(stmt, "model_config")
|
||||
if config_value is not None and _model_config_is_frozen(config_value):
|
||||
return True
|
||||
if isinstance(stmt, ast.ClassDef) and stmt.name == "Config" and _config_class_is_frozen(stmt):
|
||||
return True
|
||||
return False
|
||||
def _stmt_frozen_flag(stmt: ast.stmt) -> bool | None:
|
||||
config_value = _assigns_name(stmt, "model_config")
|
||||
if config_value is not None:
|
||||
return _model_config_frozen(config_value)
|
||||
if isinstance(stmt, ast.ClassDef) and stmt.name == "Config":
|
||||
return _config_class_frozen(stmt)
|
||||
return None
|
||||
|
||||
|
||||
def _class_frozen_override(cls: ast.ClassDef) -> bool | None:
|
||||
"""The `frozen` flag the class body itself sets; the last statement that sets one wins,
|
||||
like pydantic. None means the class inherits its parent's setting."""
|
||||
flags = tuple(flag for flag in map(_stmt_frozen_flag, cls.body) if flag is not None)
|
||||
return flags[-1] if flags else None
|
||||
|
||||
|
||||
def iter_pydantic_violations(path: Path, tree: ast.AST, comments: Comments) -> Iterator[Violation]:
|
||||
models = _pydantic_classes(tree)
|
||||
bases_of = {cls.name: _base_names(cls) for cls in models}
|
||||
|
||||
override_of = {cls.name: _class_frozen_override(cls) for cls in models}
|
||||
|
||||
def frozen(known: frozenset[str]) -> frozenset[str]:
|
||||
grown = known | frozenset(
|
||||
cls.name for cls in models if _class_is_frozen(cls) or bases_of[cls.name] & known
|
||||
cls.name
|
||||
for cls in models
|
||||
if override_of[cls.name] is True or (override_of[cls.name] is None and bases_of[cls.name] & known)
|
||||
)
|
||||
return grown if grown == known else frozen(grown)
|
||||
|
||||
|
|
|
|||
|
|
@ -761,6 +761,29 @@ def test_frozen_false_is_flagged(tmp_path):
|
|||
assert "LIT013" in _codes(tmp_path, src)
|
||||
|
||||
|
||||
def test_later_model_config_frozen_false_overrides_earlier_frozen_true(tmp_path):
|
||||
src = (
|
||||
"from pydantic import BaseModel, ConfigDict\n"
|
||||
"class P(BaseModel):\n"
|
||||
" model_config = ConfigDict(frozen=True)\n"
|
||||
" model_config = ConfigDict(frozen=False)\n"
|
||||
)
|
||||
assert "LIT013" in _codes(tmp_path, src)
|
||||
|
||||
|
||||
def test_subclass_frozen_false_overrides_frozen_parent(tmp_path):
|
||||
src = (
|
||||
"from pydantic import BaseModel, ConfigDict\n"
|
||||
"class Base(BaseModel):\n"
|
||||
" model_config = ConfigDict(frozen=True)\n"
|
||||
"class Writable(Base):\n"
|
||||
" model_config = ConfigDict(frozen=False)\n"
|
||||
"class StillFrozen(Base):\n"
|
||||
" model_config = ConfigDict(extra='allow')\n"
|
||||
)
|
||||
assert _codes(tmp_path, src).count("LIT013") == 1
|
||||
|
||||
|
||||
def test_root_model_without_frozen_is_flagged(tmp_path):
|
||||
src = "from pydantic import RootModel\nclass P(RootModel):\n root: int\n"
|
||||
assert "LIT013" in _codes(tmp_path, src)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue