From 20a94a91001aa7f1ff01de3b8dc3774d4db3bbae Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:57:44 -0700 Subject: [PATCH] chore(lint): move MappingProxyType to the dynamic tail of the LIT002 freeze menu Revert the LIT001 build-clause inserts, phrase the LIT002 menu as 'or (if it really must be dynamic) a MappingProxyType wrapping a dict literal or comprehension', and fold the two freezing-wrapper exemption sentences into one that names MappingProxyType beside tuple/frozenset. --- scripts/check_type_discipline.py | 28 +++++++++---------- .../test_check_type_discipline.py | 9 +++--- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/scripts/check_type_discipline.py b/scripts/check_type_discipline.py index d73f1095895..65d0424fb5a 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -11,21 +11,20 @@ LIT001 Mutable collection in a type annotation, anywhere it appears: function collection lets whoever holds it grow or rewrite it after the fact; annotate a read-only view instead (Mapping/Sequence/AbstractSet/tuple[X, ...]/ frozenset[X], or a frozen dataclass / NamedTuple / ReadOnly TypedDict) and - build it functionally (comprehension / map / MappingProxyType({...}), not - append-in-a-loop). + build it functionally (comprehension / map, not append-in-a-loop). Suppress with `# mutable-ok: ` on the offending line. LIT002 Mutable-collection *construction*: a list/dict/set literal or comprehension, or a call to a mutable constructor (list/dict/set/deque/defaultdict/Counter/...). Catches the unannotated seed-then-mutate pattern LIT001 cannot see (`acc = []`). Build the value in one shot and freeze it: a `tuple`/`frozenset` wrapping a - generator (`tuple(f(x) for x in xs)`), a tuple literal, `MappingProxyType({...})` - for a dict-shaped value, or a frozen dataclass / NamedTuple / ReadOnly TypedDict. - Generator expressions and `tuple`/`frozenset` - calls are not construction and pass. Annotation-internal lists (`Callable[[int], - str]`) are exempt, as is a value passed directly to a freezing wrapper - (`tuple(...)`, `frozenset(...)`, `MappingProxyType(...)`): it is frozen before - it can escape, though anything mutable nested inside it still counts. - Suppress with `# mutable-ok: `. + generator (`tuple(f(x) for x in xs)`), a tuple literal, a frozen dataclass / + NamedTuple / ReadOnly TypedDict, or (if it really must be dynamic) a + MappingProxyType wrapping a dict literal or comprehension. Generator expressions + and freezing-wrapper calls (`tuple(...)`, `frozenset(...)`, + `MappingProxyType(...)`) are not construction and pass, as does the value passed + directly to a wrapper: it is frozen before it can escape, though anything + mutable nested inside it still counts. Annotation-internal lists + (`Callable[[int], str]`) are exempt. Suppress with `# mutable-ok: `. LIT003 noqa suppression without rule codes or without a reason. Required shape: `# noqa: TID251 # ` LIT004 pyright/mypy ignore without bracketed codes or without a reason. @@ -281,8 +280,7 @@ def _mutable_ann(path: Path, line: int, name: str, where: str) -> Violation: f"mutable `{name}` in {where}: a mutable collection can be grown or rewritten " f"by whoever holds it. Annotate a read-only view -- Mapping[...], Sequence[...], " f"AbstractSet[...], tuple[X, ...], frozenset[X], or a frozen dataclass / " - f"NamedTuple / ReadOnly TypedDict -- and build it functionally " - f"(comprehension / map / `MappingProxyType({{...}})`), not by " + f"NamedTuple / ReadOnly TypedDict -- and build it functionally, not by " f"append-in-a-loop (suppress: `# mutable-ok: `)", ) @@ -491,9 +489,9 @@ def iter_construction_violations(path: Path, tree: ast.AST, comments: Comments) path, node.lineno, "LIT002", f"mutable {kind}: this builds a collection that can be grown or rewritten. " f"Build it in one shot and freeze it -- a tuple/frozenset wrapping a generator " - f"(`tuple(f(x) for x in xs)`), a tuple literal, `MappingProxyType({{...}})` for a " - f"dict-shaped value, or a frozen dataclass / NamedTuple / ReadOnly TypedDict " - f"(suppress: `# mutable-ok: `)", + f"(`tuple(f(x) for x in xs)`), a tuple literal, a frozen dataclass / NamedTuple " + f"/ ReadOnly TypedDict, or (if it really must be dynamic) a MappingProxyType " + f"wrapping a dict literal or comprehension (suppress: `# mutable-ok: `)", ) diff --git a/tests/test_litellm/test_check_type_discipline.py b/tests/test_litellm/test_check_type_discipline.py index 81efbf0a4f8..4b8533df604 100644 --- a/tests/test_litellm/test_check_type_discipline.py +++ b/tests/test_litellm/test_check_type_discipline.py @@ -175,12 +175,11 @@ def test_unfrozen_literal_still_counts(tmp_path): assert "LIT002" in _codes(tmp_path, "from types import MappingProxyType\nd = {'a': 1}\nm = MappingProxyType(d)\n") -def test_fix_messages_name_mappingproxytype(tmp_path): +def test_lit002_fix_message_names_mappingproxytype(tmp_path): f = tmp_path / "snippet.py" - f.write_text("x: dict[str, int] = {}\n", encoding="utf-8") - messages = {v.code: v.message for v in checker.check_file(f)} - assert "MappingProxyType" in messages["LIT001"] - assert "MappingProxyType" in messages["LIT002"] + f.write_text("x = {'a': 1}\n", encoding="utf-8") + messages = [v.message for v in checker.check_file(f) if v.code == "LIT002"] + assert "MappingProxyType" in messages[0] def test_mutable_ok_with_reason_suppresses_both_rules(tmp_path):