From d93ee65090c06b8ecba347b1b96a9cbff6986cfc Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 16:03:35 +0100 Subject: [PATCH 01/39] suggest attributes of anchestors --- mypy/messages.py | 4 +++- test-data/unit/check-async-await.test | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 04ab40fc4474..4a1becd7f0b1 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -490,7 +490,9 @@ def has_no_attr( ) failed = True else: - alternatives = set(original_type.type.names.keys()) + alternatives: set[str] = set() + for type_ in original_type.type.mro: + alternatives.update(type_.names.keys()) if module_symbol_table is not None: alternatives |= { k for k, v in module_symbol_table.items() if v.module_public diff --git a/test-data/unit/check-async-await.test b/test-data/unit/check-async-await.test index 0ef08e5a0775..c8ce09605195 100644 --- a/test-data/unit/check-async-await.test +++ b/test-data/unit/check-async-await.test @@ -177,7 +177,7 @@ async def f() -> None: [builtins fixtures/async_await.pyi] [typing fixtures/typing-async.pyi] [out] -main:7: error: "Coroutine[Any, Any, AsyncGenerator[str, None]]" has no attribute "__aiter__" (not async iterable) +main:7: error: "Coroutine[Any, Any, AsyncGenerator[str, None]]" has no attribute "__aiter__"; maybe "__await__"? (not async iterable) main:7: note: Maybe you forgot to use "await"? [case testAsyncForErrorCanBeIgnored] From 5695d25bf2dff1b29747fc9e5064a838fff01db4 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 17:24:58 +0100 Subject: [PATCH 02/39] add a note to `is_private` --- mypy/checker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index c69b80a55fd9..dcf66c4d3eec 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8908,7 +8908,11 @@ def is_overlapping_types_for_overload(left: Type, right: Type) -> bool: def is_private(node_name: str) -> bool: - """Check if node is private to class definition.""" + """Check if node is private to class definition. + + Since Mypy supports name mangling, `is_private` is likely only required for + internally introduced names like `__mypy-replace` and `__mypy-post_init`. + """ return node_name.startswith("__") and not node_name.endswith("__") From c54fa1f53de36b1d70799bd7fdf30a374d0812fd Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 17:45:34 +0100 Subject: [PATCH 03/39] handle enum.__member --- mypy/checker.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index dcf66c4d3eec..bda3fc117126 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2698,25 +2698,26 @@ def check_enum(self, defn: ClassDef) -> None: def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None: for sym in base.names.values(): - if self.is_final_enum_value(sym): + if self.is_final_enum_value(sym, base): self.fail(f'Cannot extend enum with existing members: "{base.name}"', defn) break - def is_final_enum_value(self, sym: SymbolTableNode) -> bool: + def is_final_enum_value(self, sym: SymbolTableNode, base: TypeInfo) -> bool: if isinstance(sym.node, (FuncBase, Decorator)): return False # A method is fine if not isinstance(sym.node, Var): return True # Can be a class or anything else # Now, only `Var` is left, we need to check: - # 1. Private name like in `__prop = 1` + # 1. Mangled name like in `_class__prop = 1` # 2. Dunder name like `__hash__ = some_hasher` # 3. Sunder name like `_order_ = 'a, b, c'` # 4. If it is a method / descriptor like in `method = classmethod(func)` + name = sym.node.name if ( - is_private(sym.node.name) - or is_dunder(sym.node.name) - or is_sunder(sym.node.name) + name.startswith(f"_{base.name}__") and not name.endswith("__") + or is_dunder(name) + or is_sunder(name) # TODO: make sure that `x = @class/staticmethod(func)` # and `x = property(prop)` both work correctly. # Now they are incorrectly counted as enum members. From d56c4af58040ddcf13dae97c90e0e81304885f7f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 17:58:27 +0100 Subject: [PATCH 04/39] implement and apply `NameMangler` # Conflicts: # mypy/fastparse.py --- mypy/fastparse.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 14b30e5d7826..cc8759b54287 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -353,6 +353,51 @@ def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> as return None +class NameMangler(ast3.NodeTransformer): + """Mangle all private identifiers within a class body (including nested classes).""" + + _name_complete: str + _name_trimmed: str + + def __init__(self, classname: str) -> None: + self._name_complete = classname + self._name_trimmed = classname.lstrip("_") + + def _mangle(self, name: str) -> str: + """Mangle the given name if it looks like a private attribute.""" + if name.startswith("__") and not name.endswith("__"): + return f"_{self._name_trimmed}{name}" + return name + + def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute: + node.attr = self._mangle(node.attr) + self.generic_visit(node) + return node + + def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: + node.name = self._mangle(node.name) + self.generic_visit(node) + return node + + def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef: + node.name = self._mangle(node.name) + self.generic_visit(node) + return node + + def visit_Name(self, node: Name) -> Name: + node.id = self._mangle(node.id) + self.generic_visit(node) + return node + + def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: + if self._name_complete == node.name: + self.generic_visit(node) + else: + NameMangler(node.name).visit(node) + node.name = self._mangle(node.name) + return node + + class ASTConverter: def __init__( self, @@ -1133,6 +1178,8 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef: if sys.version_info >= (3, 12) and n.type_params: explicit_type_params = self.translate_type_params(n.type_params) + NameMangler(n.name).visit(n) + cdef = ClassDef( n.name, self.as_required_block(n.body), From 35b228e4b10233b926586c861a5586140a9410cf Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 17:59:00 +0100 Subject: [PATCH 05/39] fix `testSelfTypeReallyTrickyExample` --- test-data/unit/check-selftype.test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 814007f0e144..253332c7c489 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -955,17 +955,17 @@ In = TypeVar('In') Out = TypeVar('Out') Other = TypeVar('Other') -_1 = TypeVar('_1') -_2 = TypeVar('_2') -__1 = TypeVar('__1') -__2 = TypeVar('__2') +A1 = TypeVar('A1') +A2 = TypeVar('A2') +B1 = TypeVar('B1') +B2 = TypeVar('B2') class Lnk(Generic[In, Out]): @overload def __rshift__(self, other: Lnk[Out, Other]) -> Lnk[In,Other]: ... @overload - def __rshift__(self: Lnk[In, Tuple[_1, _2]], - other: Tuple[Lnk[_1, __1], Lnk[_2, __2]]) -> Lnk[In, Tuple[__1, __2]]: ... + def __rshift__(self: Lnk[In, Tuple[A1, A2]], + other: Tuple[Lnk[A1, B1], Lnk[A2, B2]]) -> Lnk[In, Tuple[B1, B2]]: ... def __rshift__(self: Any, other: Any) -> Any: ... From b41f09513b9f3cbe6610c4ea8c4f565cf613e810 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 20:50:06 +0100 Subject: [PATCH 06/39] fix stubtest `test_name_mangling` --- mypy/test/teststubtest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index f3199dae7f73..94a36a6279a5 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1833,7 +1833,7 @@ class X: def __mangle_good(self, text): pass def __mangle_bad(self, text): pass """, - error="X.__mangle_bad", + error="X._X__mangle_bad", ) yield Case( stub=""" @@ -1850,7 +1850,7 @@ class __Mangled2: def __mangle_good(self, text): pass def __mangle_bad(self, text): pass """, - error="Klass.__Mangled1.__Mangled2.__mangle_bad", + error="Klass._Klass__Mangled1._Mangled1__Mangled2._Mangled2__mangle_bad", ) yield Case( stub=""" @@ -1863,7 +1863,7 @@ class __Dunder__: def __mangle_good(self, text): pass def __mangle_bad(self, text): pass """, - error="__Dunder__.__mangle_bad", + error="__Dunder__._Dunder____mangle_bad", ) yield Case( stub=""" @@ -1876,7 +1876,7 @@ class _Private: def __mangle_good(self, text): pass def __mangle_bad(self, text): pass """, - error="_Private.__mangle_bad", + error="_Private._Private__mangle_bad", ) @collect_cases From 6530bf1bfbbdd6459abe29d52ddb8c6c46c18562 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 23:46:26 +0100 Subject: [PATCH 07/39] add name mangling test cases --- mypy/fastparse.py | 5 ++ test-data/unit/check-classes.test | 129 ++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index cc8759b54287..5db8e865d227 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -384,6 +384,11 @@ def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunct self.generic_visit(node) return node + def visit_arg(self, node: ast3.arg) -> ast3.arg: + node.arg = self._mangle(node.arg) + self.generic_visit(node) + return node + def visit_Name(self, node: Name) -> Name: node.id = self._mangle(node.id) self.generic_visit(node) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index cf401bc2aece..86c73314f596 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8443,3 +8443,132 @@ class C: def x(self) -> None: pass [builtins fixtures/property.pyi] + + +[case testNameManglingAttribute] + +class A: + __x: int + + def __init__(self) -> None: + self.__y: int + self._A__z: int + + def get(self) -> None: + reveal_type(self.__x) # N: Revealed type is "builtins.int" + reveal_type(self._A__x) # N: Revealed type is "builtins.int" + reveal_type(self.__y) # N: Revealed type is "builtins.int" + reveal_type(self._A__y) # N: Revealed type is "builtins.int" + reveal_type(self.__z) # N: Revealed type is "builtins.int" + reveal_type(self._A__z) # N: Revealed type is "builtins.int" + + def set(self) -> None: + self.__x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self._A__x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self.__y += "" # E: Unsupported operand types for + ("int" and "str") + self._A__y += "" # E: Unsupported operand types for + ("int" and "str") + self.__z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self._A__z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +class B(A): + __x: str + + def __init__(self) -> None: + self.__y: str + self._A__z: str # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") + + def get(self) -> None: + reveal_type(self.__x) # N: Revealed type is "builtins.str" + reveal_type(self._A__x) # N: Revealed type is "builtins.int" + reveal_type(self._B__x) # N: Revealed type is "builtins.str" + reveal_type(self.__y) # N: Revealed type is "builtins.str" + reveal_type(self._A__y) # N: Revealed type is "builtins.int" + reveal_type(self._B__y) # N: Revealed type is "builtins.str" + self.__z # E: "B" has no attribute "_B__z"; maybe "_A__z", "_B__x", or "_B__y"? + reveal_type(self._A__z) # N: Revealed type is "builtins.str" + self._B__z # E: "B" has no attribute "_B__z"; maybe "_A__z", "_B__x", or "_B__y"? + +[case testNameManglingMethod] + +class A: + + def __f(self) -> int: + ... + + def g(self) -> None: + reveal_type(self.__f()) # N: Revealed type is "builtins.int" + reveal_type(self._A__f()) # N: Revealed type is "builtins.int" + + async def __h(self) -> int: + ... + + async def i(self) -> None: + await reveal_type(self.__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" + await reveal_type(self._A__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" + +class B(A): + + def j(self) -> None: + reveal_type(self._A__f()) # N: Revealed type is "builtins.int" + self.__f() # E: "B" has no attribute "_B__f"; maybe "_A__f"? + self._B__f() # E: "B" has no attribute "_B__f"; maybe "_A__f"? + + async def k(self) -> None: + await reveal_type(self._A__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" + self.__h() # E: "B" has no attribute "_B__h"; maybe "_A__h"? + self._B__h() # E: "B" has no attribute "_B__h"; maybe "_A__h"? + +class C(B): + + def __f(self) -> str: + ... + + def _A__f(self) -> str: # E: Return type "str" of "_A__f" incompatible with return type "int" in supertype "A" + ... + + async def __h(self) -> str: + ... + + async def _A__h(self) -> str: # E: Return type "Coroutine[Any, Any, str]" of "_A__h" incompatible with return type "Coroutine[Any, Any, int]" in supertype "A" + ... + +[case testNameManglingNestedClass] + +class A: + class __B: + __y: int + + def __g(self) -> str: + reveal_type(self.__y) # N: Revealed type is "builtins.int" + reveal_type(self._B__y) # N: Revealed type is "builtins.int" + return "x" + + reveal_type(__g) # N: Revealed type is "def (self: __main__.A._A__B) -> builtins.str" + reveal_type(_B__g) # N: Revealed type is "def (self: __main__.A._A__B) -> builtins.str" + + __x: int + + def __f(self) -> float: + reveal_type(self.__x) # N: Revealed type is "builtins.int" + reveal_type(self._A__x) # N: Revealed type is "builtins.int" + b = self.__B() + b.__y # E: "_A__B" has no attribute "_A__y"; maybe "_B__y"? + b._A__y # E: "_A__B" has no attribute "_A__y"; maybe "_B__y"? + reveal_type(b._B__y) # N: Revealed type is "builtins.int" + return 1.0 + + reveal_type(__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" + reveal_type(_A__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" + +[case testNameManglingGoesCrazy] + +class A: + __x: int +class B: + def f(self, a: A) -> None: + a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + +__y = int +class C: + def f(self, __x: __y, __z: int) -> None: # E: Name "_C__y" is not defined + reveal_type(__z) # N: Revealed type is "builtins.int" From 8016d332162e6007048510bc8d0c5cedd2d5173b Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 29 Dec 2023 23:47:45 +0100 Subject: [PATCH 08/39] use `/` instead of `__` to mark positional-only method parameters --- mypy/typeanal.py | 2 +- test-data/unit/check-generics.test | 2 +- test-data/unit/check-inference.test | 2 +- test-data/unit/check-protocols.test | 4 ++-- test-data/unit/check-python38.test | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index fa7cf4242d82..13744806d414 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -2016,7 +2016,7 @@ def tuple_type(self, items: list[Type], line: int, column: int) -> TupleType: class MsgCallback(Protocol): - def __call__(self, __msg: str, __ctx: Context, *, code: ErrorCode | None = None) -> None: ... + def __call__(self, msg: str, ctx: Context, /, *, code: ErrorCode | None = None) -> None: ... def get_omitted_any( diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 5d6ad8e19631..6f314213a489 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2995,7 +2995,7 @@ from typing import TypeVar, Protocol, Generic, Optional T = TypeVar('T') class F(Protocol[T]): - def __call__(self, __x: T) -> T: ... + def __call__(self, x: T, /) -> T: ... def lift(f: F[T]) -> F[Optional[T]]: ... def g(x: T) -> T: diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 0da1c092efe8..62903fce1921 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3615,7 +3615,7 @@ T = TypeVar('T', contravariant=True) S = TypeVar('S', contravariant=True) class Call(Protocol[T, S]): - def __call__(self, __x: T, *args: S) -> None: ... + def __call__(self, x: T, /, *args: S) -> None: ... def f(x: Call[T, S]) -> Tuple[T, S]: ... def g(*x: int) -> None: ... diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 294bacb1b7d9..1a9de2a6a912 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2585,7 +2585,7 @@ class Caller(Protocol): def __call__(self, x: str) -> None: ... class CallerAnon(Protocol): - def __call__(self, __x: str) -> None: ... + def __call__(self, x: str, /) -> None: ... def call(x: str) -> None: pass @@ -2657,7 +2657,7 @@ class C(B): ... class D(B): ... class Call(Protocol): - def __call__(self, __x: C) -> B: ... + def __call__(self, x: C, /) -> B: ... Normal = Callable[[D], A] def a(x: Call) -> None: ... diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test index 4add107baef4..9a980a748af7 100644 --- a/test-data/unit/check-python38.test +++ b/test-data/unit/check-python38.test @@ -637,7 +637,7 @@ from typing import overload class Some: @overload - def foo(self, __a: int): ... + def foo(self, a: int, /): ... @overload def foo(self, a: float, /): ... @overload @@ -658,7 +658,7 @@ from typing import overload class Some: @overload @classmethod - def foo(cls, __a: int): ... + def foo(cls, a: int, /): ... @overload @classmethod def foo(cls, a: float, /): ... From cf554fb6213fc26f4436a8673c9d3ea48ef69894 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 23:30:19 +0000 Subject: [PATCH 09/39] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index bda3fc117126..01165eb30cbd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2715,7 +2715,8 @@ def is_final_enum_value(self, sym: SymbolTableNode, base: TypeInfo) -> bool: # 4. If it is a method / descriptor like in `method = classmethod(func)` name = sym.node.name if ( - name.startswith(f"_{base.name}__") and not name.endswith("__") + name.startswith(f"_{base.name}__") + and not name.endswith("__") or is_dunder(name) or is_sunder(name) # TODO: make sure that `x = @class/staticmethod(func)` From b00ea249bb3864b80a42249fa0bda35b8f1da823 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 30 Dec 2023 07:42:49 +0100 Subject: [PATCH 10/39] improve auto fix --- mypy/checker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 01165eb30cbd..cc0ccb23e7dc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2715,8 +2715,7 @@ def is_final_enum_value(self, sym: SymbolTableNode, base: TypeInfo) -> bool: # 4. If it is a method / descriptor like in `method = classmethod(func)` name = sym.node.name if ( - name.startswith(f"_{base.name}__") - and not name.endswith("__") + (name.startswith(f"_{base.name}__") and not name.endswith("__")) or is_dunder(name) or is_sunder(name) # TODO: make sure that `x = @class/staticmethod(func)` From a6eb9e421369bf94d1c4d71af24e4044fa2e0218 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 30 Dec 2023 10:49:37 +0100 Subject: [PATCH 11/39] first hacky step/suggestion for introducing a flag to avoid method parameter mangling --- mypy/fastparse.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 5db8e865d227..becb5489378e 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -358,10 +358,14 @@ class NameMangler(ast3.NodeTransformer): _name_complete: str _name_trimmed: str + _unmangled_args: set[str] + + _MANGLE_ARGS: bool = False def __init__(self, classname: str) -> None: self._name_complete = classname self._name_trimmed = classname.lstrip("_") + self._unmangled_args = set() def _mangle(self, name: str) -> str: """Mangle the given name if it looks like a private attribute.""" @@ -376,21 +380,35 @@ def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute: def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: node.name = self._mangle(node.name) - self.generic_visit(node) + if self._MANGLE_ARGS: + self.generic_visit(node) + else: + mangler = NameMangler(self._name_complete) + for subnode in ast3.iter_child_nodes(node): + mangler.visit(subnode) return node def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef: node.name = self._mangle(node.name) - self.generic_visit(node) + if self._MANGLE_ARGS: + self.generic_visit(node) + else: + mangler = NameMangler(self._name_complete) + for subnode in ast3.iter_child_nodes(node): + mangler.visit(subnode) return node def visit_arg(self, node: ast3.arg) -> ast3.arg: - node.arg = self._mangle(node.arg) + if self._MANGLE_ARGS: + node.arg = self._mangle(node.arg) + else: + self._unmangled_args.add(node.arg) self.generic_visit(node) return node def visit_Name(self, node: Name) -> Name: - node.id = self._mangle(node.id) + if self._MANGLE_ARGS or (node.id not in self._unmangled_args): + node.id = self._mangle(node.id) self.generic_visit(node) return node From bacd994395dd73e7c195f3f861d37bffa613563c Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Jan 2024 13:44:59 +0100 Subject: [PATCH 12/39] mangle strings in __slots__ --- mypy/fastparse.py | 19 ++++++++- test-data/unit/check-classes.test | 64 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index becb5489378e..bf84a9cd72fe 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -4,7 +4,7 @@ import sys import warnings from collections.abc import Sequence -from typing import Any, Callable, Final, Literal, Optional, TypeVar, Union, cast, overload +from typing import Any, Callable, Final, Iterable, Literal, Optional, TypeVar, Union, cast, overload from mypy import defaults, errorcodes as codes, message_registry from mypy.errors import Errors @@ -415,11 +415,28 @@ def visit_Name(self, node: Name) -> Name: def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: if self._name_complete == node.name: self.generic_visit(node) + self._mangle_slots(node) else: NameMangler(node.name).visit(node) node.name = self._mangle(node.name) return node + def _mangle_slots(self, node: ast3.ClassDef) -> None: + for assign in node.body: + if isinstance(assign, ast3.Assign): + for target in assign.targets: + if isinstance(target, ast3.Name) and (target.id == "__slots__"): + constants: Iterable[ast3.expr] = () + if isinstance(values := assign.value, ast3.Constant): + constants = (values,) + elif isinstance(values, (ast3.Tuple, ast3.List)): + constants = values.elts + elif isinstance(values, ast3.Dict): + constants = (key for key in values.keys if key is not None) + for value in constants: + if isinstance(value, ast3.Constant) and isinstance(value.value, str): + value.value = self._mangle(value.value) + class ASTConverter: def __init__( diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 86c73314f596..680c3e755b78 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8572,3 +8572,67 @@ __y = int class C: def f(self, __x: __y, __z: int) -> None: # E: Name "_C__y" is not defined reveal_type(__z) # N: Revealed type is "builtins.int" + +[case testNameManglingSlots] + +class A: + __slots__ = ["__x"] + + def __init__(self) -> None: + self.__x = 1 + +A().__x = 1 # E: "A" has no attribute "__x" +A()._A__x = 1 + +class B(A): + def __init__(self) -> None: + self.__x = 1 + +B().__x = 1 # E: "B" has no attribute "__x" +B()._B__x = 1 + +class C(A): + __slots__ = [] + + def __init__(self) -> None: + self.__x = 1 # E: Trying to assign name "_C__x" that is not in "__slots__" of type "__main__.C" + +C().__x = 1 # E: "C" has no attribute "__x" +C()._C__x = 1 # E: Trying to assign name "_C__x" that is not in "__slots__" of type "__main__.C" + +class D(A): + __slots__ = ["__x"] + + def __init__(self) -> None: + self.__x = 1 + +D().__x = 1 # E: "D" has no attribute "__x" +D()._D__x = 1 + +class E(A): + __slots__ = ("__x",) + + def __init__(self) -> None: + self.__x = 1 + +E().__x = 1 # E: "E" has no attribute "__x" +E()._E__x = 1 + +class F(A): + __slots__ = "__x" + + def __init__(self) -> None: + self.__x = 1 + +F().__x = 1 # E: "F" has no attribute "__x" +F()._F__x = 1 + +class G(A): + __slots__ = {"__x": "docstring"} + + def __init__(self) -> None: + self.__x = 1 + +G().__x = 1 # E: "G" has no attribute "__x" +G()._G__x = 1 +[builtins fixtures/dict.pyi] From 1c3cba70def51df5e33e5cc100b4cb4768f60eda Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 2 Jan 2024 19:36:16 +0100 Subject: [PATCH 13/39] different annotation handling when using `from __future__ import annotations` statements --- mypy/fastparse.py | 30 ++++++++++++++++------ test-data/unit/check-classes.test | 41 ++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index bf84a9cd72fe..54dfa6592557 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -358,13 +358,15 @@ class NameMangler(ast3.NodeTransformer): _name_complete: str _name_trimmed: str + _future_annotations: bool _unmangled_args: set[str] _MANGLE_ARGS: bool = False - def __init__(self, classname: str) -> None: + def __init__(self, classname: str, future_annotations: bool) -> None: self._name_complete = classname self._name_trimmed = classname.lstrip("_") + self._future_annotations = future_annotations self._unmangled_args = set() def _mangle(self, name: str) -> str: @@ -383,7 +385,7 @@ def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: if self._MANGLE_ARGS: self.generic_visit(node) else: - mangler = NameMangler(self._name_complete) + mangler = NameMangler(self._name_complete, self._future_annotations) for subnode in ast3.iter_child_nodes(node): mangler.visit(subnode) return node @@ -393,7 +395,7 @@ def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunct if self._MANGLE_ARGS: self.generic_visit(node) else: - mangler = NameMangler(self._name_complete) + mangler = NameMangler(self._name_complete, self._future_annotations) for subnode in ast3.iter_child_nodes(node): mangler.visit(subnode) return node @@ -403,13 +405,13 @@ def visit_arg(self, node: ast3.arg) -> ast3.arg: node.arg = self._mangle(node.arg) else: self._unmangled_args.add(node.arg) - self.generic_visit(node) + if (node.annotation is not None) and not self._future_annotations: + self.visit(node.annotation) return node def visit_Name(self, node: Name) -> Name: if self._MANGLE_ARGS or (node.id not in self._unmangled_args): node.id = self._mangle(node.id) - self.generic_visit(node) return node def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: @@ -417,7 +419,7 @@ def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: self.generic_visit(node) self._mangle_slots(node) else: - NameMangler(node.name).visit(node) + NameMangler(node.name, self._future_annotations).visit(node) node.name = self._mangle(node.name) return node @@ -437,6 +439,14 @@ def _mangle_slots(self, node: ast3.ClassDef) -> None: if isinstance(value, ast3.Constant) and isinstance(value.value, str): value.value = self._mangle(value.value) + def visit_AnnAssign(self, node: ast3.AnnAssign) -> ast3.AnnAssign: + self.visit(node.target) + if node.value is not None: + self.visit(node.value) + if not self._future_annotations: + self.visit(node.annotation) + return node + class ASTConverter: def __init__( @@ -1218,7 +1228,13 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef: if sys.version_info >= (3, 12) and n.type_params: explicit_type_params = self.translate_type_params(n.type_params) - NameMangler(n.name).visit(n) + future_annotations = any( + isinstance(i, ImportFrom) + and (i.id == "__future__") + and any(j[0] == "annotations" for j in i.names) + for i in self.imports + ) + NameMangler(n.name, future_annotations).visit(n) cdef = ClassDef( n.name, diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 680c3e755b78..ccfbe4ce6749 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8560,19 +8560,54 @@ class A: reveal_type(__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" reveal_type(_A__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" -[case testNameManglingGoesCrazy] +[case testNameManglingAnnotationsPast] class A: __x: int -class B: +class __B: + def f(self, a: A) -> None: + a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + +__y = int +class C: + b1: __B # E: Name "_C__B" is not defined + b2: "__B" + + def f1(self, __x: __y, __z: int) -> None: # E: Name "_C__y" is not defined + reveal_type(__z) # N: Revealed type is "builtins.int" + + def f2(self, __x: "__y", __z: "int") -> None: + reveal_type(__x) # N: Revealed type is "builtins.int" + reveal_type(__z) # N: Revealed type is "builtins.int" + +reveal_type(C().b2) # N: Revealed type is "__main__.__B" + +[case testNameManglingAnnotationsFuture] +from __future__ import annotations + +class A: + __x: int +class __B: def f(self, a: A) -> None: a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? __y = int class C: - def f(self, __x: __y, __z: int) -> None: # E: Name "_C__y" is not defined + b1: __B + b2: "__B" + + def f1(self, __x: __y, __z: int) -> None: + reveal_type(__x) # N: Revealed type is "builtins.int" + reveal_type(__z) # N: Revealed type is "builtins.int" + + def f2(self, __x: "__y", __z: "int") -> None: + reveal_type(__x) # N: Revealed type is "builtins.int" reveal_type(__z) # N: Revealed type is "builtins.int" +reveal_type(C().b1) # N: Revealed type is "__main__.__B" +reveal_type(C().b2) # N: Revealed type is "__main__.__B" +[builtins fixtures/tuple.pyi] + [case testNameManglingSlots] class A: From 89bd1582b33ddc1cf87cbd8abb13fb7a73d7ff6f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 3 Jan 2024 00:25:08 +0100 Subject: [PATCH 14/39] improve visit_ClassDef --- mypy/fastparse.py | 3 ++- test-data/unit/check-classes.test | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 54dfa6592557..a986967519e8 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -416,7 +416,8 @@ def visit_Name(self, node: Name) -> Name: def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: if self._name_complete == node.name: - self.generic_visit(node) + for subnode in node.body: + self.visit(subnode) self._mangle_slots(node) else: NameMangler(node.name, self._future_annotations).visit(node) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index ccfbe4ce6749..24480717648d 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8560,6 +8560,14 @@ class A: reveal_type(__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" reveal_type(_A__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" +[case testNameManglingInheritance] + +class __A: ... +class B(__A): ... + +import enum as __enum +class C(__enum.Flag): ... + [case testNameManglingAnnotationsPast] class A: From 8f5f187eaa394a57bae060c1690f0d4586efcfcd Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 3 Jan 2024 00:40:10 +0100 Subject: [PATCH 15/39] improve visit_FunctionDef --- mypy/fastparse.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index a986967519e8..fa84bcf054a7 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -386,8 +386,7 @@ def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: self.generic_visit(node) else: mangler = NameMangler(self._name_complete, self._future_annotations) - for subnode in ast3.iter_child_nodes(node): - mangler.visit(subnode) + mangler.generic_visit(node) return node def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef: @@ -396,8 +395,7 @@ def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunct self.generic_visit(node) else: mangler = NameMangler(self._name_complete, self._future_annotations) - for subnode in ast3.iter_child_nodes(node): - mangler.visit(subnode) + mangler.generic_visit(node) return node def visit_arg(self, node: ast3.arg) -> ast3.arg: From 2bd762630330a59b759cdde7d88615fe8d2b8f73 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 3 Jan 2024 23:42:42 +0100 Subject: [PATCH 16/39] refactor visit_FunctionDef and visit_AsyncFunctionDef --- mypy/fastparse.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index fa84bcf054a7..9a0f2173d12d 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -353,6 +353,9 @@ def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> as return None +_T_FuncDef = TypeVar("_T_FuncDef", ast3.FunctionDef, ast3.AsyncFunctionDef) + + class NameMangler(ast3.NodeTransformer): """Mangle all private identifiers within a class body (including nested classes).""" @@ -381,21 +384,18 @@ def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute: return node def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: - node.name = self._mangle(node.name) - if self._MANGLE_ARGS: - self.generic_visit(node) - else: - mangler = NameMangler(self._name_complete, self._future_annotations) - mangler.generic_visit(node) - return node + return self._visit_funcdef(node) def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef: + return self._visit_funcdef(node) + + def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: node.name = self._mangle(node.name) if self._MANGLE_ARGS: - self.generic_visit(node) + mangler = self else: mangler = NameMangler(self._name_complete, self._future_annotations) - mangler.generic_visit(node) + mangler.generic_visit(node) return node def visit_arg(self, node: ast3.arg) -> ast3.arg: From 77513a61e0a5e55cd34ecdb8233c5a7b650d9b27 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jan 2024 00:23:43 +0100 Subject: [PATCH 17/39] different return type annotation handling when using `from __future__ import annotations` statements --- mypy/fastparse.py | 6 +++++- test-data/unit/check-classes.test | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 9a0f2173d12d..5ba20f066222 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -395,7 +395,11 @@ def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: mangler = self else: mangler = NameMangler(self._name_complete, self._future_annotations) - mangler.generic_visit(node) + mangler.visit(node.args) + if not self._future_annotations: + mangler.visit(node.returns) + for stmt in node.body: + mangler.visit(stmt) return node def visit_arg(self, node: ast3.arg) -> ast3.arg: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 24480717648d..7aeb17855772 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8570,13 +8570,19 @@ class C(__enum.Flag): ... [case testNameManglingAnnotationsPast] +__y = int + class A: __x: int + class __B: - def f(self, a: A) -> None: + def f1(self, a: A) -> __y: # E: Name "_B__y" is not defined a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? -__y = int + def f2(self, a: A) -> "__y": + a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + return 1 + class C: b1: __B # E: Name "_C__B" is not defined b2: "__B" @@ -8593,13 +8599,20 @@ reveal_type(C().b2) # N: Revealed type is "__main__.__B" [case testNameManglingAnnotationsFuture] from __future__ import annotations +__y = int + class A: __x: int + class __B: - def f(self, a: A) -> None: + def f1(self, a: A) -> __y: a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + return 1 + + def f2(self, a: A) -> "__y": + a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + return 1 -__y = int class C: b1: __B b2: "__B" From 74b46c27a88cabe2517046f6a0c75eec4a271e41 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jan 2024 00:59:57 +0100 Subject: [PATCH 18/39] fix --- mypy/fastparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 5ba20f066222..5fcfd97b3fe9 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -396,7 +396,7 @@ def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: else: mangler = NameMangler(self._name_complete, self._future_annotations) mangler.visit(node.args) - if not self._future_annotations: + if (node.returns is not None) and not self._future_annotations: mangler.visit(node.returns) for stmt in node.body: mangler.visit(stmt) From 9a96ff99151df74ca73b76540d813164768d8218 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jan 2024 09:23:45 +0100 Subject: [PATCH 19/39] support decorators --- mypy/fastparse.py | 4 ++++ test-data/unit/check-classes.test | 32 +++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 5fcfd97b3fe9..8784e10c6fd7 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -396,6 +396,8 @@ def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: else: mangler = NameMangler(self._name_complete, self._future_annotations) mangler.visit(node.args) + for dec in node.decorator_list: + mangler.visit(dec) if (node.returns is not None) and not self._future_annotations: mangler.visit(node.returns) for stmt in node.body: @@ -422,6 +424,8 @@ def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: self.visit(subnode) self._mangle_slots(node) else: + for subnode in node.decorator_list: + self.visit(subnode) NameMangler(node.name, self._future_annotations).visit(node) node.name = self._mangle(node.name) return node diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 7aeb17855772..d4d22693771d 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8532,6 +8532,32 @@ class C(B): async def _A__h(self) -> str: # E: Return type "Coroutine[Any, Any, str]" of "_A__h" incompatible with return type "Coroutine[Any, Any, int]" in supertype "A" ... +[case testNameManglingDecorator] +from typing import Callable, Protocol, Type + +class FuncWrapper(Protocol): + def __call__(__self, f: Callable[[A], int]) -> Callable[[A], str]: ... +def decorator_func(string: str) -> FuncWrapper: ... + +class ClassModifier(Protocol): + def __call__(__self, c: Type[A._A__B]) -> Type[A._A__B]: ... +def decorator_class(string: str) -> ClassModifier: ... + +class A: + __x: str = "test" + + @decorator_func(__x) + def __wrapped(self) -> int: ... + + @decorator_class(__x) + class __B: ... + +a: A +a.__wrapped() # E: "A" has no attribute "__wrapped"; maybe "_A__wrapped"? +reveal_type(a._A__wrapped()) # N: Revealed type is "builtins.str" +a.__B # E: "A" has no attribute "__B" +reveal_type(a._A__B) # N: Revealed type is "def () -> __main__.A._A__B" + [case testNameManglingNestedClass] class A: @@ -8570,11 +8596,10 @@ class C(__enum.Flag): ... [case testNameManglingAnnotationsPast] -__y = int - class A: __x: int +__y = int class __B: def f1(self, a: A) -> __y: # E: Name "_B__y" is not defined a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? @@ -8599,11 +8624,10 @@ reveal_type(C().b2) # N: Revealed type is "__main__.__B" [case testNameManglingAnnotationsFuture] from __future__ import annotations -__y = int - class A: __x: int +__y = int class __B: def f1(self, a: A) -> __y: a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? From 29f186ce9c9933e874bc56daf7428c3c11b3ba83 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jan 2024 10:23:57 +0100 Subject: [PATCH 20/39] fix --- mypy/fastparse.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 8784e10c6fd7..30ba79b7e061 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -420,12 +420,12 @@ def visit_Name(self, node: Name) -> Name: def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: if self._name_complete == node.name: - for subnode in node.body: - self.visit(subnode) + for stmt in node.body: + self.visit(stmt) self._mangle_slots(node) else: - for subnode in node.decorator_list: - self.visit(subnode) + for dec in node.decorator_list: + self.visit(dec) NameMangler(node.name, self._future_annotations).visit(node) node.name = self._mangle(node.name) return node From 292890acdf91915a8316ecd95b43945babe0e4b8 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jan 2024 10:23:44 +0100 Subject: [PATCH 21/39] Add TypeVar tests --- test-data/unit/check-classes.test | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index d4d22693771d..9f619dae971a 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8595,17 +8595,20 @@ import enum as __enum class C(__enum.Flag): ... [case testNameManglingAnnotationsPast] +from typing import Generic, TypeVar -class A: - __x: int +__T = TypeVar("__T") + +class A(Generic[__T]): + __x: __T # E: Name "_A__T" is not defined __y = int class __B: - def f1(self, a: A) -> __y: # E: Name "_B__y" is not defined - a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + def f1(self, a: A[int]) -> __y: # E: Name "_B__y" is not defined + a.__x # E: "A[int]" has no attribute "_B__x"; maybe "_A__x"? def f2(self, a: A) -> "__y": - a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + a.__x # E: "A[Any]" has no attribute "_B__x"; maybe "_A__x"? return 1 class C: @@ -8623,18 +8626,20 @@ reveal_type(C().b2) # N: Revealed type is "__main__.__B" [case testNameManglingAnnotationsFuture] from __future__ import annotations +from typing import Generic, TypeVar -class A: - __x: int +__T = TypeVar("__T") +class A(Generic[__T]): + __x: __T __y = int class __B: - def f1(self, a: A) -> __y: - a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + def f1(self, a: A[int]) -> __y: + a.__x # E: "A[int]" has no attribute "_B__x"; maybe "_A__x"? return 1 def f2(self, a: A) -> "__y": - a.__x # E: "A" has no attribute "_B__x"; maybe "_A__x"? + a.__x # E: "A[Any]" has no attribute "_B__x"; maybe "_A__x"? return 1 class C: From e6904d88cb65812998134f35c30704a0e7d848a8 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 4 Jan 2024 22:13:21 +0100 Subject: [PATCH 22/39] for testing: do not mangle annotations when defined in stub files --- mypy/fastparse.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 30ba79b7e061..3edf2ab63069 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -361,15 +361,15 @@ class NameMangler(ast3.NodeTransformer): _name_complete: str _name_trimmed: str - _future_annotations: bool + _mangle_annotations: bool _unmangled_args: set[str] _MANGLE_ARGS: bool = False - def __init__(self, classname: str, future_annotations: bool) -> None: + def __init__(self, classname: str, mangle_annotations: bool) -> None: self._name_complete = classname self._name_trimmed = classname.lstrip("_") - self._future_annotations = future_annotations + self._mangle_annotations = mangle_annotations self._unmangled_args = set() def _mangle(self, name: str) -> str: @@ -394,11 +394,11 @@ def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: if self._MANGLE_ARGS: mangler = self else: - mangler = NameMangler(self._name_complete, self._future_annotations) + mangler = NameMangler(self._name_complete, self._mangle_annotations) mangler.visit(node.args) for dec in node.decorator_list: mangler.visit(dec) - if (node.returns is not None) and not self._future_annotations: + if self._mangle_annotations and (node.returns is not None): mangler.visit(node.returns) for stmt in node.body: mangler.visit(stmt) @@ -409,7 +409,7 @@ def visit_arg(self, node: ast3.arg) -> ast3.arg: node.arg = self._mangle(node.arg) else: self._unmangled_args.add(node.arg) - if (node.annotation is not None) and not self._future_annotations: + if self._mangle_annotations and (node.annotation is not None): self.visit(node.annotation) return node @@ -426,7 +426,7 @@ def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: else: for dec in node.decorator_list: self.visit(dec) - NameMangler(node.name, self._future_annotations).visit(node) + NameMangler(node.name, self._mangle_annotations).visit(node) node.name = self._mangle(node.name) return node @@ -450,7 +450,7 @@ def visit_AnnAssign(self, node: ast3.AnnAssign) -> ast3.AnnAssign: self.visit(node.target) if node.value is not None: self.visit(node.value) - if not self._future_annotations: + if self._mangle_annotations: self.visit(node.annotation) return node @@ -1235,13 +1235,13 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef: if sys.version_info >= (3, 12) and n.type_params: explicit_type_params = self.translate_type_params(n.type_params) - future_annotations = any( + mangle_annotations = not self.is_stub and not any( isinstance(i, ImportFrom) and (i.id == "__future__") and any(j[0] == "annotations" for j in i.names) for i in self.imports ) - NameMangler(n.name, future_annotations).visit(n) + NameMangler(n.name, mangle_annotations).visit(n) cdef = ClassDef( n.name, From cfaddb3b0b82ee481cf430c6db0f7edb27a44fb2 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jan 2024 00:47:42 +0100 Subject: [PATCH 23/39] refactor --- mypy/fastparse.py | 119 ++++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 3edf2ab63069..53da63fc9759 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -357,95 +357,82 @@ def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> as class NameMangler(ast3.NodeTransformer): - """Mangle all private identifiers within a class body (including nested classes).""" + """Mangle (nearly) all private identifiers within a class body (including nested classes).""" - _name_complete: str - _name_trimmed: str + _classname_complete: str + _classname_trimmed: str _mangle_annotations: bool _unmangled_args: set[str] - _MANGLE_ARGS: bool = False + _MANGLE_ARGS: bool = False # ToDo: remove it or make it an option? def __init__(self, classname: str, mangle_annotations: bool) -> None: - self._name_complete = classname - self._name_trimmed = classname.lstrip("_") + self._classname_complete = classname + self._classname_trimmed = classname.lstrip("_") self._mangle_annotations = mangle_annotations self._unmangled_args = set() - def _mangle(self, name: str) -> str: - """Mangle the given name if it looks like a private attribute.""" + def _mangle_name(self, name: str) -> str: if name.startswith("__") and not name.endswith("__"): - return f"_{self._name_trimmed}{name}" + return f"_{self._classname_trimmed}{name}" return name - def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute: - node.attr = self._mangle(node.attr) - self.generic_visit(node) - return node - - def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: - return self._visit_funcdef(node) + def _mangle_slots(self, node: ast3.ClassDef) -> None: + for assign in node.body: + if isinstance(assign, ast3.Assign): + for target in assign.targets: + if isinstance(target, ast3.Name) and (target.id == "__slots__"): + constants: Iterable[ast3.expr] = () + if isinstance(values := assign.value, ast3.Constant): + constants = (values,) + elif isinstance(values, (ast3.Tuple, ast3.List)): + constants = values.elts + elif isinstance(values, ast3.Dict): + constants = (key for key in values.keys if key is not None) + for value in constants: + if isinstance(value, ast3.Constant) and isinstance(value.value, str): + value.value = self._mangle_name(value.value) - def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef: - return self._visit_funcdef(node) + def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: + if self._classname_complete == node.name: + for stmt in node.body: + self.visit(stmt) + self._mangle_slots(node) + else: + for dec in node.decorator_list: + self.visit(dec) + NameMangler(node.name, self._mangle_annotations).visit(node) + node.name = self._mangle_name(node.name) + return node def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: - node.name = self._mangle(node.name) - if self._MANGLE_ARGS: - mangler = self - else: - mangler = NameMangler(self._name_complete, self._mangle_annotations) - mangler.visit(node.args) + node.name = self._mangle_name(node.name) + if not self._MANGLE_ARGS: + self = NameMangler(self._classname_complete, self._mangle_annotations) + self.visit(node.args) for dec in node.decorator_list: - mangler.visit(dec) + self.visit(dec) if self._mangle_annotations and (node.returns is not None): - mangler.visit(node.returns) + self.visit(node.returns) for stmt in node.body: - mangler.visit(stmt) + self.visit(stmt) return node + def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef: + return self._visit_funcdef(node) + + def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef: + return self._visit_funcdef(node) + def visit_arg(self, node: ast3.arg) -> ast3.arg: if self._MANGLE_ARGS: - node.arg = self._mangle(node.arg) + node.arg = self._mangle_name(node.arg) else: self._unmangled_args.add(node.arg) if self._mangle_annotations and (node.annotation is not None): self.visit(node.annotation) return node - def visit_Name(self, node: Name) -> Name: - if self._MANGLE_ARGS or (node.id not in self._unmangled_args): - node.id = self._mangle(node.id) - return node - - def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: - if self._name_complete == node.name: - for stmt in node.body: - self.visit(stmt) - self._mangle_slots(node) - else: - for dec in node.decorator_list: - self.visit(dec) - NameMangler(node.name, self._mangle_annotations).visit(node) - node.name = self._mangle(node.name) - return node - - def _mangle_slots(self, node: ast3.ClassDef) -> None: - for assign in node.body: - if isinstance(assign, ast3.Assign): - for target in assign.targets: - if isinstance(target, ast3.Name) and (target.id == "__slots__"): - constants: Iterable[ast3.expr] = () - if isinstance(values := assign.value, ast3.Constant): - constants = (values,) - elif isinstance(values, (ast3.Tuple, ast3.List)): - constants = values.elts - elif isinstance(values, ast3.Dict): - constants = (key for key in values.keys if key is not None) - for value in constants: - if isinstance(value, ast3.Constant) and isinstance(value.value, str): - value.value = self._mangle(value.value) - def visit_AnnAssign(self, node: ast3.AnnAssign) -> ast3.AnnAssign: self.visit(node.target) if node.value is not None: @@ -454,6 +441,16 @@ def visit_AnnAssign(self, node: ast3.AnnAssign) -> ast3.AnnAssign: self.visit(node.annotation) return node + def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute: + node.attr = self._mangle_name(node.attr) + self.generic_visit(node) + return node + + def visit_Name(self, node: Name) -> Name: + if self._MANGLE_ARGS or (node.id not in self._unmangled_args): + node.id = self._mangle_name(node.id) + return node + class ASTConverter: def __init__( From a1eb0dd2f9573c59917e7de618257371030c85f4 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jan 2024 11:16:48 +0100 Subject: [PATCH 24/39] remove method `is_private` by mangling "__mypy-replace" and "__mypy-post_init". --- mypy/checker.py | 30 ++++-------------------------- mypy/plugins/dataclasses.py | 12 ++++++++---- test-data/unit/deps.test | 8 +++++--- test-data/unit/pythoneval.test | 28 ++++++++++++++++++++++------ 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index cc0ccb23e7dc..ad89117a5ada 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1336,7 +1336,7 @@ def check_func_def( if ( arg_type.variance == COVARIANT and defn.name not in ("__init__", "__new__", "__post_init__") - and not is_private(defn.name) # private methods are not inherited + and "mypy-" not in defn.name # skip internally added methods ): ctx: Context = arg_type if ctx.line < 0: @@ -1979,7 +1979,6 @@ def check_explicit_override_decorator( and found_method_base_classes and not defn.is_explicit_override and defn.name not in ("__init__", "__new__") - and not is_private(defn.name) ): self.msg.explicit_override_decorator_missing( defn.name, found_method_base_classes[0].fullname, context or defn @@ -2036,7 +2035,7 @@ def check_method_or_accessor_override_for_base( base_attr = base.names.get(name) if base_attr: # First, check if we override a final (always an error, even with Any types). - if is_final_node(base_attr.node) and not is_private(name): + if is_final_node(base_attr.node): self.msg.cant_override_final(name, base.name, defn) # Second, final can't override anything writeable independently of types. if defn.is_final: @@ -2393,9 +2392,6 @@ def check_override( if original.type_is is not None and override.type_is is None: fail = True - if is_private(name): - fail = False - if fail: emitted_msg = False @@ -2828,12 +2824,8 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None: # Verify that inherited attributes are compatible. mro = typ.mro[1:] all_names = {name for base in mro for name in base.names} + # Sort for reproducible message order. for name in sorted(all_names - typ.names.keys()): - # Sort for reproducible message order. - # Attributes defined in both the type and base are skipped. - # Normal checks for attribute compatibility should catch any problems elsewhere. - if is_private(name): - continue # Compare the first base defining a name with the rest. # Remaining bases may not be pairwise compatible as the first base provides # the used definition. @@ -2945,7 +2937,7 @@ class C(B, A[int]): ... # this is unsafe because... ok = True # Final attributes can never be overridden, but can override # non-final read-only attributes. - if is_final_node(second.node) and not is_private(name): + if is_final_node(second.node): self.msg.cant_override_final(name, base2.name, ctx) if is_final_node(first.node): self.check_if_final_var_override_writable(name, second.node, ctx) @@ -3415,9 +3407,6 @@ def check_compatibility_all_supers( ): continue - if is_private(lvalue_node.name): - continue - base_type, base_node = self.lvalue_type_from_base(lvalue_node, base) custom_setter = is_custom_settable_property(base_node) if isinstance(base_type, PartialType): @@ -3621,8 +3610,6 @@ def check_compatibility_final_super( """ if not isinstance(base_node, (Var, FuncBase, Decorator)): return True - if is_private(node.name): - return True if base_node.is_final and (node.is_final or not isinstance(base_node, Var)): # Give this error only for explicit override attempt with `Final`, or # if we are overriding a final method with variable. @@ -8908,15 +8895,6 @@ def is_overlapping_types_for_overload(left: Type, right: Type) -> bool: ) -def is_private(node_name: str) -> bool: - """Check if node is private to class definition. - - Since Mypy supports name mangling, `is_private` is likely only required for - internally introduced names like `__mypy-replace` and `__mypy-post_init`. - """ - return node_name.startswith("__") and not node_name.endswith("__") - - def is_string_literal(typ: Type) -> bool: strs = try_getting_str_literals_from_type(typ) return strs is not None and len(strs) == 1 diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 6e0e22272356..0c9cc0740a8f 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -422,20 +422,22 @@ def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) -> Stashes the signature of 'dataclasses.replace(...)' for this specific dataclass to be used later whenever 'dataclasses.replace' is called for this dataclass. """ + mangled_name = f"_{self._cls.name.lstrip('_')}{_INTERNAL_REPLACE_SYM_NAME}" add_method_to_class( self._api, self._cls, - _INTERNAL_REPLACE_SYM_NAME, + mangled_name, args=[attr.to_argument(self._cls.info, of="replace") for attr in attributes], return_type=NoneType(), is_staticmethod=True, ) def _add_internal_post_init_method(self, attributes: list[DataclassAttribute]) -> None: + mangled_name = f"_{self._cls.name.lstrip('_')}{_INTERNAL_POST_INIT_SYM_NAME}" add_method_to_class( self._api, self._cls, - _INTERNAL_POST_INIT_SYM_NAME, + mangled_name, args=[ attr.to_argument(self._cls.info, of="__post_init__") for attr in attributes @@ -1027,7 +1029,8 @@ def _get_expanded_dataclasses_fields( ctx, get_proper_type(typ.upper_bound), display_typ, parent_typ ) elif isinstance(typ, Instance): - replace_sym = typ.type.get_method(_INTERNAL_REPLACE_SYM_NAME) + mangled_name = f"_{typ.type.name.lstrip('_')}{_INTERNAL_REPLACE_SYM_NAME}" + replace_sym = typ.type.get_method(mangled_name) if replace_sym is None: return None replace_sig = replace_sym.type @@ -1111,7 +1114,8 @@ def check_post_init(api: TypeChecker, defn: FuncItem, info: TypeInfo) -> None: return assert isinstance(defn.type, FunctionLike) - ideal_sig_method = info.get_method(_INTERNAL_POST_INIT_SYM_NAME) + mangled_name = f"_{info.name.lstrip('_')}{_INTERNAL_POST_INIT_SYM_NAME}" + ideal_sig_method = info.get_method(mangled_name) assert ideal_sig_method is not None and ideal_sig_method.type is not None ideal_sig = ideal_sig_method.type assert isinstance(ideal_sig, ProperType) # we set it ourselves diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 2c231c9afff6..04b97f75dee9 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -1389,15 +1389,16 @@ class B(A): [out] -> , m + -> , m + -> m.B._B__mypy-replace -> -> , m.B.__init__ - -> , m, m.B.__mypy-replace -> -> -> -> m, m.A, m.B -> m - -> m + -> m -> m -> m.B -> m @@ -1421,10 +1422,11 @@ class B(A): [out] -> , m + -> , m + -> m.B._B__mypy-replace -> -> , m.B.__init__ -> - -> , m, m.B.__mypy-replace -> -> -> diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 48d6ee04b514..6732e78d1e62 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1963,17 +1963,33 @@ from dataclasses import dataclass, replace class A: x: int +@dataclass +class B(A): + y: str + a = A(x=42) a2 = replace(a, x=42) reveal_type(a2) a2 = replace() -a2 = replace(a, x='spam') +a2 = replace(a, x="spam") a2 = replace(a, x=42, q=42) -[out] -_testDataclassReplace.py:9: note: Revealed type is "_testDataclassReplace.A" -_testDataclassReplace.py:10: error: Too few arguments for "replace" -_testDataclassReplace.py:11: error: Argument "x" to "replace" of "A" has incompatible type "str"; expected "int" -_testDataclassReplace.py:12: error: Unexpected keyword argument "q" for "replace" of "A" + +b = B(x=42, y="egg") +b2 = replace(b, x=42, y="egg") +reveal_type(b2) +replace() +replace(b, x="egg", y=42) +replace(b, x=42, y="egg", q=42) +[out] +_testDataclassReplace.py:13: note: Revealed type is "_testDataclassReplace.A" +_testDataclassReplace.py:14: error: Too few arguments for "replace" +_testDataclassReplace.py:15: error: Argument "x" to "replace" of "A" has incompatible type "str"; expected "int" +_testDataclassReplace.py:16: error: Unexpected keyword argument "q" for "replace" of "A" +_testDataclassReplace.py:20: note: Revealed type is "_testDataclassReplace.B" +_testDataclassReplace.py:21: error: Too few arguments for "replace" +_testDataclassReplace.py:22: error: Argument "x" to "replace" of "B" has incompatible type "str"; expected "int" +_testDataclassReplace.py:22: error: Argument "y" to "replace" of "B" has incompatible type "int"; expected "str" +_testDataclassReplace.py:23: error: Unexpected keyword argument "q" for "replace" of "B" [case testGenericInferenceWithTuple] # flags: --new-type-inference From b7443497d566036bd8dfa05837fc57286a2c7085 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Jan 2024 11:54:50 +0100 Subject: [PATCH 25/39] fix --- test-data/unit/deps.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 04b97f75dee9..02ad5897d919 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -1432,7 +1432,7 @@ class B(A): -> -> m, m.A, m.B -> m - -> m + -> m -> m -> m.B -> m From 9892dc25dcd4ec424e6184628964a622018f1190 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 30 Jan 2025 20:48:14 +0100 Subject: [PATCH 26/39] add missing [builtins fixtures/tuple.pyi] --- test-data/unit/check-classes.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 9f619dae971a..97b0e168b913 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8593,6 +8593,7 @@ class B(__A): ... import enum as __enum class C(__enum.Flag): ... +[builtins fixtures/tuple.pyi] [case testNameManglingAnnotationsPast] from typing import Generic, TypeVar From aec6a87541253826ba0b7d08c679a42378cb959d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:00:26 +0000 Subject: [PATCH 27/39] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/fastparse.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 53da63fc9759..a010db9525c6 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -3,8 +3,18 @@ import re import sys import warnings -from collections.abc import Sequence -from typing import Any, Callable, Final, Iterable, Literal, Optional, TypeVar, Union, cast, overload +from collections.abc import Iterable, Sequence +from typing import ( + Any, + Callable, + Final, + Literal, + Optional, + TypeVar, + Union, + cast, + overload, +) from mypy import defaults, errorcodes as codes, message_registry from mypy.errors import Errors From d8c7c3ef80e1c36bea08d99cabee5c1f4b1e2810 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 31 Jan 2025 07:14:22 +0100 Subject: [PATCH 28/39] after rebasing: adjust `infer_variance` to mangled `__mypy-replace` members --- mypy/subtypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 804930fc9d0c..68ee21306b0d 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2056,9 +2056,10 @@ def infer_variance(info: TypeInfo, i: int) -> bool: tvar = info.defn.type_vars[i] self_type = fill_typevars(info) for member in all_non_object_members(info): - # __mypy-replace is an implementation detail of the dataclass plugin - if member in ("__init__", "__new__", "__mypy-replace"): + if member in ("__init__", "__new__"): continue + if "mypy-" in member: + continue # skip internally added methods if isinstance(self_type, TupleType): self_type = mypy.typeops.tuple_fallback(self_type) From a04ca0ad3d38de5ecc58157b9d63119ce6a0aad3 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 31 Jan 2025 07:19:27 +0100 Subject: [PATCH 29/39] fix format --- mypy/fastparse.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index a010db9525c6..e6f077749537 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -4,17 +4,7 @@ import sys import warnings from collections.abc import Iterable, Sequence -from typing import ( - Any, - Callable, - Final, - Literal, - Optional, - TypeVar, - Union, - cast, - overload, -) +from typing import Any, Callable, Final, Literal, Optional, TypeVar, Union, cast, overload from mypy import defaults, errorcodes as codes, message_registry from mypy.errors import Errors From fd68e7b444af73e3ffe3799ee1fb1e9d7e491f77 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 2 Feb 2025 00:38:57 +0100 Subject: [PATCH 30/39] add function `maybe_mangled` mainly for fixing the handling of "private" enum members --- mypy/checker.py | 4 ++-- mypy/checkmember.py | 3 ++- mypy/nodes.py | 4 ++-- mypy/semanal.py | 4 ++-- mypy/typeanal.py | 3 ++- mypy/util.py | 6 +++++- test-data/unit/check-enum.test | 3 +-- test-data/unit/check-literal.test | 3 +-- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ad89117a5ada..8f2315484302 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -210,7 +210,7 @@ from mypy.types_utils import is_overlapping_none, remove_optional, store_argument_type, strip_type from mypy.typetraverser import TypeTraverserVisitor from mypy.typevars import fill_typevars, fill_typevars_with_any, has_no_typevars -from mypy.util import is_dunder, is_sunder +from mypy.util import maybe_mangled, is_dunder, is_sunder from mypy.visitor import NodeVisitor T = TypeVar("T") @@ -2711,7 +2711,7 @@ def is_final_enum_value(self, sym: SymbolTableNode, base: TypeInfo) -> bool: # 4. If it is a method / descriptor like in `method = classmethod(func)` name = sym.node.name if ( - (name.startswith(f"_{base.name}__") and not name.endswith("__")) + maybe_mangled(name, base.name) or is_dunder(name) or is_sunder(name) # TODO: make sure that `x = @class/staticmethod(func)` diff --git a/mypy/checkmember.py b/mypy/checkmember.py index f6b5e6be2c53..7749c97418ae 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -73,6 +73,7 @@ get_proper_type, ) from mypy.typetraverser import TypeTraverserVisitor +from mypy.util import maybe_mangled, is_dunder if TYPE_CHECKING: # import for forward declaration only import mypy.checker @@ -1197,7 +1198,7 @@ def analyze_enum_class_attribute_access( if name in EXCLUDED_ENUM_ATTRIBUTES: return report_missing_attribute(mx.original_type, itype, name, mx) # Dunders and private names are not Enum members - if name.startswith("__") and name.replace("_", "") != "": + if is_dunder(name) or maybe_mangled(name, itype.type.name): return None node = itype.type.get(name) diff --git a/mypy/nodes.py b/mypy/nodes.py index 9364805d44d4..facdf6d633ea 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -14,7 +14,7 @@ import mypy.strconv from mypy.options import Options -from mypy.util import is_typeshed_file, short_type +from mypy.util import maybe_mangled, is_dunder, is_typeshed_file, short_type from mypy.visitor import ExpressionVisitor, NodeVisitor, StatementVisitor if TYPE_CHECKING: @@ -3237,7 +3237,7 @@ def enum_members(self) -> list[str]: if ( isinstance(sym.node, Var) and name not in EXCLUDED_ENUM_ATTRIBUTES - and not name.startswith("__") + and not (is_dunder(name) or maybe_mangled(name, self.name)) and sym.node.has_explicit_value ) ] diff --git a/mypy/semanal.py b/mypy/semanal.py index d769178dc298..b99194de50e0 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -303,7 +303,7 @@ ) from mypy.types_utils import is_invalid_recursive_alias, store_argument_type from mypy.typevars import fill_typevars -from mypy.util import correct_relative_import, is_dunder, module_prefix, unmangle, unnamed_function +from mypy.util import correct_relative_import, maybe_mangled, is_dunder, module_prefix, unmangle, unnamed_function from mypy.visitor import NodeVisitor T = TypeVar("T") @@ -4292,7 +4292,7 @@ def analyze_name_lvalue( kind == MDEF and isinstance(self.type, TypeInfo) and self.type.is_enum - and not name.startswith("__") + and not (is_dunder(name) or maybe_mangled(name, self.type.name)) ): # Special case: we need to be sure that `Enum` keys are unique. if existing is not None and not isinstance(existing.node, PlaceholderNode): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 13744806d414..1e9e6b9f6bd2 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -114,6 +114,7 @@ ) from mypy.types_utils import get_bad_type_type_item from mypy.typevars import fill_typevars +from mypy.util import maybe_mangled T = TypeVar("T") @@ -980,7 +981,7 @@ def analyze_unbound_type_without_type_info( isinstance(sym.node, Var) and sym.node.info and sym.node.info.is_enum - and not sym.node.name.startswith("__") + and not maybe_mangled(sym.node.name, sym.node.info.name) ): value = sym.node.name base_enum_short_name = sym.node.info.name diff --git a/mypy/util.py b/mypy/util.py index f79d7113ca91..d43731456ec1 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -62,13 +62,17 @@ def is_dunder(name: str, exclude_special: bool = False) -> bool: """ if exclude_special and name in SPECIAL_DUNDERS: return False - return name.startswith("__") and name.endswith("__") + return name.startswith("__") and name.endswith("__") and name.replace("_", "") def is_sunder(name: str) -> bool: return not is_dunder(name) and name.startswith("_") and name.endswith("_") +def maybe_mangled(member_name: str, type_name: str) -> bool: + return member_name.startswith(f"_{type_name}__") and not member_name.endswith("__") + + def split_module_names(mod_name: str) -> list[str]: """Return the module and all parent module names. diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 37c63f43179d..7026d7085f6a 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -2260,8 +2260,7 @@ class MyEnum(Enum): B = 2 __my_dict = {A: "ham", B: "spam"} -# TODO: change the next line to use MyEnum._MyEnum__my_dict when mypy implements name mangling -x: MyEnum = MyEnum.__my_dict # E: Incompatible types in assignment (expression has type "Dict[int, str]", variable has type "MyEnum") +x: MyEnum = MyEnum._MyEnum__my_dict # E: Incompatible types in assignment (expression has type "Dict[int, str]", variable has type "MyEnum") [builtins fixtures/enum.pyi] [case testEnumWithPrivateAttributeReachability] diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 856bc941435d..df64a263966c 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -2506,8 +2506,7 @@ b: Literal[Color.BLUE] bad1: Literal[Color] # E: Parameter 1 of Literal[...] is invalid bad2: Literal[Color.func] # E: Parameter 1 of Literal[...] is invalid bad3: Literal[Color.func()] # E: Invalid type: Literal[...] cannot contain arbitrary expressions -# TODO: change the next line to use Color._Color__ROUGE when mypy implements name mangling -bad4: Literal[Color.__ROUGE] # E: Parameter 1 of Literal[...] is invalid +bad4: Literal[Color._Color__ROUGE] # E: Parameter 1 of Literal[...] is invalid def expects_color(x: Color) -> None: pass def expects_red(x: Literal[Color.RED]) -> None: pass From b2a63ad3e8b42af96d405d0bda3c3f10aba219fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 1 Feb 2025 23:40:32 +0000 Subject: [PATCH 31/39] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 2 +- mypy/checkmember.py | 2 +- mypy/nodes.py | 2 +- mypy/semanal.py | 9 ++++++++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8f2315484302..daec3a02f61b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -210,7 +210,7 @@ from mypy.types_utils import is_overlapping_none, remove_optional, store_argument_type, strip_type from mypy.typetraverser import TypeTraverserVisitor from mypy.typevars import fill_typevars, fill_typevars_with_any, has_no_typevars -from mypy.util import maybe_mangled, is_dunder, is_sunder +from mypy.util import is_dunder, is_sunder, maybe_mangled from mypy.visitor import NodeVisitor T = TypeVar("T") diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 7749c97418ae..4f65c5f66ead 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -73,7 +73,7 @@ get_proper_type, ) from mypy.typetraverser import TypeTraverserVisitor -from mypy.util import maybe_mangled, is_dunder +from mypy.util import is_dunder, maybe_mangled if TYPE_CHECKING: # import for forward declaration only import mypy.checker diff --git a/mypy/nodes.py b/mypy/nodes.py index facdf6d633ea..839b0e4791ea 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -14,7 +14,7 @@ import mypy.strconv from mypy.options import Options -from mypy.util import maybe_mangled, is_dunder, is_typeshed_file, short_type +from mypy.util import is_dunder, is_typeshed_file, maybe_mangled, short_type from mypy.visitor import ExpressionVisitor, NodeVisitor, StatementVisitor if TYPE_CHECKING: diff --git a/mypy/semanal.py b/mypy/semanal.py index b99194de50e0..9922a705485e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -303,7 +303,14 @@ ) from mypy.types_utils import is_invalid_recursive_alias, store_argument_type from mypy.typevars import fill_typevars -from mypy.util import correct_relative_import, maybe_mangled, is_dunder, module_prefix, unmangle, unnamed_function +from mypy.util import ( + correct_relative_import, + is_dunder, + maybe_mangled, + module_prefix, + unmangle, + unnamed_function, +) from mypy.visitor import NodeVisitor T = TypeVar("T") From 8e8c1299b05b9e7eeeb05a72205ed8bcfab5a779 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 2 Feb 2025 23:12:14 +0100 Subject: [PATCH 32/39] add the module path to the mangled name of an internal dataclass symbol --- mypy/plugins/dataclasses.py | 21 +++++++++++++++------ test-data/unit/check-dataclasses.test | 19 +++++++++++++++++++ test-data/unit/deps.test | 12 ++++++------ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 0c9cc0740a8f..e994682933b1 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -91,8 +91,8 @@ frozen_default=False, field_specifiers=DATACLASS_FIELD_SPECIFIERS, ) -_INTERNAL_REPLACE_SYM_NAME: Final = "__mypy-replace" -_INTERNAL_POST_INIT_SYM_NAME: Final = "__mypy-post_init" +_INTERNAL_REPLACE_SYM_NAME: Final = "mypy-replace" +_INTERNAL_POST_INIT_SYM_NAME: Final = "mypy-post_init" class DataclassAttribute: @@ -422,7 +422,7 @@ def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) -> Stashes the signature of 'dataclasses.replace(...)' for this specific dataclass to be used later whenever 'dataclasses.replace' is called for this dataclass. """ - mangled_name = f"_{self._cls.name.lstrip('_')}{_INTERNAL_REPLACE_SYM_NAME}" + mangled_name = _mangle_internal_sym_name(self._cls.fullname, _INTERNAL_REPLACE_SYM_NAME) add_method_to_class( self._api, self._cls, @@ -433,7 +433,7 @@ def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) -> ) def _add_internal_post_init_method(self, attributes: list[DataclassAttribute]) -> None: - mangled_name = f"_{self._cls.name.lstrip('_')}{_INTERNAL_POST_INIT_SYM_NAME}" + mangled_name = _mangle_internal_sym_name(self._cls.fullname, _INTERNAL_POST_INIT_SYM_NAME) add_method_to_class( self._api, self._cls, @@ -971,6 +971,15 @@ def dataclass_class_maker_callback(ctx: ClassDefContext) -> bool: return transformer.transform() +def _mangle_internal_sym_name(type_fullname: str, member_name: str) -> str: + """Create an internal symbol name with the class name mangled in as usual, but that also + contains the class module path to avoid false positives when subclassing a dataclass with + same name.""" + module_name, type_name = type_fullname.rsplit(".") + module_name = module_name.replace(".", "-") + type_name = type_name.lstrip('_') + return f"_{type_name}__{module_name}__{member_name}" + def _get_transform_spec(reason: Expression) -> DataclassTransformSpec: """Find the relevant transform parameters from the decorator/parent class/metaclass that triggered the dataclasses plugin. @@ -1029,7 +1038,7 @@ def _get_expanded_dataclasses_fields( ctx, get_proper_type(typ.upper_bound), display_typ, parent_typ ) elif isinstance(typ, Instance): - mangled_name = f"_{typ.type.name.lstrip('_')}{_INTERNAL_REPLACE_SYM_NAME}" + mangled_name = _mangle_internal_sym_name(typ.type.fullname, _INTERNAL_REPLACE_SYM_NAME) replace_sym = typ.type.get_method(mangled_name) if replace_sym is None: return None @@ -1114,7 +1123,7 @@ def check_post_init(api: TypeChecker, defn: FuncItem, info: TypeInfo) -> None: return assert isinstance(defn.type, FunctionLike) - mangled_name = f"_{info.name.lstrip('_')}{_INTERNAL_POST_INIT_SYM_NAME}" + mangled_name = _mangle_internal_sym_name(info.fullname, _INTERNAL_POST_INIT_SYM_NAME) ideal_sig_method = info.get_method(mangled_name) assert ideal_sig_method is not None and ideal_sig_method.type is not None ideal_sig = ideal_sig_method.type diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 2e7259e4de0a..2a479ad3c46c 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -226,6 +226,25 @@ reveal_type(d.foo) # N: Revealed type is "builtins.str" reveal_type(d.bar) # N: Revealed type is "builtins.int" [builtins fixtures/dataclasses.pyi] +[case testDataclassCompatibleOverrideEqualName] +from dataclasses import dataclass, InitVar +import m + +@dataclass +class D(m.D): + x: InitVar[int] + +[file m.py] +from dataclasses import dataclass + +@dataclass +class D: + y: int + +[out] + +[builtins fixtures/dataclasses.pyi] + [case testDataclassIncompatibleFrozenOverride] from dataclasses import dataclass diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 02ad5897d919..8947062e0999 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -1389,8 +1389,8 @@ class B(A): [out] -> , m - -> , m - -> m.B._B__mypy-replace + -> , m + -> m.B._B__m__mypy-replace -> -> , m.B.__init__ -> @@ -1398,7 +1398,7 @@ class B(A): -> -> m, m.A, m.B -> m - -> m + -> m -> m -> m.B -> m @@ -1422,8 +1422,8 @@ class B(A): [out] -> , m - -> , m - -> m.B._B__mypy-replace + -> , m + -> m.B._B__m__mypy-replace -> -> , m.B.__init__ -> @@ -1432,7 +1432,7 @@ class B(A): -> -> m, m.A, m.B -> m - -> m + -> m -> m -> m.B -> m From 4a01786f3d20575065abffd08f486851bf0f9fb1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:14:24 +0000 Subject: [PATCH 33/39] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/plugins/dataclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index e994682933b1..1aa5aebf991b 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -977,9 +977,10 @@ def _mangle_internal_sym_name(type_fullname: str, member_name: str) -> str: same name.""" module_name, type_name = type_fullname.rsplit(".") module_name = module_name.replace(".", "-") - type_name = type_name.lstrip('_') + type_name = type_name.lstrip("_") return f"_{type_name}__{module_name}__{member_name}" + def _get_transform_spec(reason: Expression) -> DataclassTransformSpec: """Find the relevant transform parameters from the decorator/parent class/metaclass that triggered the dataclasses plugin. From 384b0eed74b3e81cf8fd2cb7f404a2f9fe63139b Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 2 Feb 2025 23:23:09 +0100 Subject: [PATCH 34/39] add the module path to the mangled name of an internal dataclass symbol --- mypy/plugins/dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 1aa5aebf991b..660dcb408f1e 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -975,7 +975,7 @@ def _mangle_internal_sym_name(type_fullname: str, member_name: str) -> str: """Create an internal symbol name with the class name mangled in as usual, but that also contains the class module path to avoid false positives when subclassing a dataclass with same name.""" - module_name, type_name = type_fullname.rsplit(".") + module_name, _, type_name = type_fullname.rpartition(".") module_name = module_name.replace(".", "-") type_name = type_name.lstrip("_") return f"_{type_name}__{module_name}__{member_name}" From 44a9e328df0209ca7cc0cbca71f73ff79286adeb Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 2 Feb 2025 23:27:09 +0100 Subject: [PATCH 35/39] add the module path to the mangled name of an internal dataclass symbol --- mypy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/util.py b/mypy/util.py index d43731456ec1..57e1e55e1bf1 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -62,7 +62,7 @@ def is_dunder(name: str, exclude_special: bool = False) -> bool: """ if exclude_special and name in SPECIAL_DUNDERS: return False - return name.startswith("__") and name.endswith("__") and name.replace("_", "") + return name.startswith("__") and name.endswith("__") and bool(name.replace("_", "")) def is_sunder(name: str) -> bool: From 0acbd5aff4a8feecc08ac23cd14ba0886ad012dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 16 Feb 2025 19:40:41 +0000 Subject: [PATCH 36/39] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-data/unit/check-classes.test | 1 - 1 file changed, 1 deletion(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index ec7174802117..ebf3fc4ad948 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8743,4 +8743,3 @@ class G(A): G().__x = 1 # E: "G" has no attribute "__x" G()._G__x = 1 [builtins fixtures/dict.pyi] - From 89d11a6fcf3e38d3b02f0d41784b9ef80b7c9708 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 20 Feb 2025 22:39:06 +0100 Subject: [PATCH 37/39] remove `_MANGLE_ARGS` -> never mangle parameter names --- mypy/fastparse.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 9f7cff5390a1..38100a404d06 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -364,8 +364,6 @@ class NameMangler(ast3.NodeTransformer): _mangle_annotations: bool _unmangled_args: set[str] - _MANGLE_ARGS: bool = False # ToDo: remove it or make it an option? - def __init__(self, classname: str, mangle_annotations: bool) -> None: self._classname_complete = classname self._classname_trimmed = classname.lstrip("_") @@ -407,8 +405,7 @@ def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef: def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef: node.name = self._mangle_name(node.name) - if not self._MANGLE_ARGS: - self = NameMangler(self._classname_complete, self._mangle_annotations) + self = NameMangler(self._classname_complete, self._mangle_annotations) self.visit(node.args) for dec in node.decorator_list: self.visit(dec) @@ -425,10 +422,7 @@ def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunct return self._visit_funcdef(node) def visit_arg(self, node: ast3.arg) -> ast3.arg: - if self._MANGLE_ARGS: - node.arg = self._mangle_name(node.arg) - else: - self._unmangled_args.add(node.arg) + self._unmangled_args.add(node.arg) if self._mangle_annotations and (node.annotation is not None): self.visit(node.annotation) return node @@ -447,7 +441,7 @@ def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute: return node def visit_Name(self, node: Name) -> Name: - if self._MANGLE_ARGS or (node.id not in self._unmangled_args): + if node.id not in self._unmangled_args: node.id = self._mangle_name(node.id) return node From eb923194d5fc13989b2fb84f6d7eda6906f1a715 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 21 Feb 2025 20:39:12 +0100 Subject: [PATCH 38/39] move the new test cases from `check-classes.test` to `check-mangling.text` --- test-data/unit/check-classes.test | 278 ----------------------------- test-data/unit/check-mangling.test | 277 ++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 278 deletions(-) create mode 100644 test-data/unit/check-mangling.test diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index ebf3fc4ad948..d48a27dbed03 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -8465,281 +8465,3 @@ def deco(fn: Callable[[], list[T]]) -> Callable[[], T]: ... @deco def f() -> list[str]: ... [builtins fixtures/property.pyi] - -[case testNameManglingAttribute] - -class A: - __x: int - - def __init__(self) -> None: - self.__y: int - self._A__z: int - - def get(self) -> None: - reveal_type(self.__x) # N: Revealed type is "builtins.int" - reveal_type(self._A__x) # N: Revealed type is "builtins.int" - reveal_type(self.__y) # N: Revealed type is "builtins.int" - reveal_type(self._A__y) # N: Revealed type is "builtins.int" - reveal_type(self.__z) # N: Revealed type is "builtins.int" - reveal_type(self._A__z) # N: Revealed type is "builtins.int" - - def set(self) -> None: - self.__x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") - self._A__x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") - self.__y += "" # E: Unsupported operand types for + ("int" and "str") - self._A__y += "" # E: Unsupported operand types for + ("int" and "str") - self.__z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") - self._A__z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") - -class B(A): - __x: str - - def __init__(self) -> None: - self.__y: str - self._A__z: str # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") - - def get(self) -> None: - reveal_type(self.__x) # N: Revealed type is "builtins.str" - reveal_type(self._A__x) # N: Revealed type is "builtins.int" - reveal_type(self._B__x) # N: Revealed type is "builtins.str" - reveal_type(self.__y) # N: Revealed type is "builtins.str" - reveal_type(self._A__y) # N: Revealed type is "builtins.int" - reveal_type(self._B__y) # N: Revealed type is "builtins.str" - self.__z # E: "B" has no attribute "_B__z"; maybe "_A__z", "_B__x", or "_B__y"? - reveal_type(self._A__z) # N: Revealed type is "builtins.str" - self._B__z # E: "B" has no attribute "_B__z"; maybe "_A__z", "_B__x", or "_B__y"? - -[case testNameManglingMethod] - -class A: - - def __f(self) -> int: - ... - - def g(self) -> None: - reveal_type(self.__f()) # N: Revealed type is "builtins.int" - reveal_type(self._A__f()) # N: Revealed type is "builtins.int" - - async def __h(self) -> int: - ... - - async def i(self) -> None: - await reveal_type(self.__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" - await reveal_type(self._A__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" - -class B(A): - - def j(self) -> None: - reveal_type(self._A__f()) # N: Revealed type is "builtins.int" - self.__f() # E: "B" has no attribute "_B__f"; maybe "_A__f"? - self._B__f() # E: "B" has no attribute "_B__f"; maybe "_A__f"? - - async def k(self) -> None: - await reveal_type(self._A__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" - self.__h() # E: "B" has no attribute "_B__h"; maybe "_A__h"? - self._B__h() # E: "B" has no attribute "_B__h"; maybe "_A__h"? - -class C(B): - - def __f(self) -> str: - ... - - def _A__f(self) -> str: # E: Return type "str" of "_A__f" incompatible with return type "int" in supertype "A" - ... - - async def __h(self) -> str: - ... - - async def _A__h(self) -> str: # E: Return type "Coroutine[Any, Any, str]" of "_A__h" incompatible with return type "Coroutine[Any, Any, int]" in supertype "A" - ... - -[case testNameManglingDecorator] -from typing import Callable, Protocol, Type - -class FuncWrapper(Protocol): - def __call__(__self, f: Callable[[A], int]) -> Callable[[A], str]: ... -def decorator_func(string: str) -> FuncWrapper: ... - -class ClassModifier(Protocol): - def __call__(__self, c: Type[A._A__B]) -> Type[A._A__B]: ... -def decorator_class(string: str) -> ClassModifier: ... - -class A: - __x: str = "test" - - @decorator_func(__x) - def __wrapped(self) -> int: ... - - @decorator_class(__x) - class __B: ... - -a: A -a.__wrapped() # E: "A" has no attribute "__wrapped"; maybe "_A__wrapped"? -reveal_type(a._A__wrapped()) # N: Revealed type is "builtins.str" -a.__B # E: "A" has no attribute "__B" -reveal_type(a._A__B) # N: Revealed type is "def () -> __main__.A._A__B" - -[case testNameManglingNestedClass] - -class A: - class __B: - __y: int - - def __g(self) -> str: - reveal_type(self.__y) # N: Revealed type is "builtins.int" - reveal_type(self._B__y) # N: Revealed type is "builtins.int" - return "x" - - reveal_type(__g) # N: Revealed type is "def (self: __main__.A._A__B) -> builtins.str" - reveal_type(_B__g) # N: Revealed type is "def (self: __main__.A._A__B) -> builtins.str" - - __x: int - - def __f(self) -> float: - reveal_type(self.__x) # N: Revealed type is "builtins.int" - reveal_type(self._A__x) # N: Revealed type is "builtins.int" - b = self.__B() - b.__y # E: "_A__B" has no attribute "_A__y"; maybe "_B__y"? - b._A__y # E: "_A__B" has no attribute "_A__y"; maybe "_B__y"? - reveal_type(b._B__y) # N: Revealed type is "builtins.int" - return 1.0 - - reveal_type(__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" - reveal_type(_A__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" - -[case testNameManglingInheritance] - -class __A: ... -class B(__A): ... - -import enum as __enum -class C(__enum.Flag): ... -[builtins fixtures/tuple.pyi] - -[case testNameManglingAnnotationsPast] -from typing import Generic, TypeVar - -__T = TypeVar("__T") - -class A(Generic[__T]): - __x: __T # E: Name "_A__T" is not defined - -__y = int -class __B: - def f1(self, a: A[int]) -> __y: # E: Name "_B__y" is not defined - a.__x # E: "A[int]" has no attribute "_B__x"; maybe "_A__x"? - - def f2(self, a: A) -> "__y": - a.__x # E: "A[Any]" has no attribute "_B__x"; maybe "_A__x"? - return 1 - -class C: - b1: __B # E: Name "_C__B" is not defined - b2: "__B" - - def f1(self, __x: __y, __z: int) -> None: # E: Name "_C__y" is not defined - reveal_type(__z) # N: Revealed type is "builtins.int" - - def f2(self, __x: "__y", __z: "int") -> None: - reveal_type(__x) # N: Revealed type is "builtins.int" - reveal_type(__z) # N: Revealed type is "builtins.int" - -reveal_type(C().b2) # N: Revealed type is "__main__.__B" - -[case testNameManglingAnnotationsFuture] -from __future__ import annotations -from typing import Generic, TypeVar - -__T = TypeVar("__T") -class A(Generic[__T]): - __x: __T - -__y = int -class __B: - def f1(self, a: A[int]) -> __y: - a.__x # E: "A[int]" has no attribute "_B__x"; maybe "_A__x"? - return 1 - - def f2(self, a: A) -> "__y": - a.__x # E: "A[Any]" has no attribute "_B__x"; maybe "_A__x"? - return 1 - -class C: - b1: __B - b2: "__B" - - def f1(self, __x: __y, __z: int) -> None: - reveal_type(__x) # N: Revealed type is "builtins.int" - reveal_type(__z) # N: Revealed type is "builtins.int" - - def f2(self, __x: "__y", __z: "int") -> None: - reveal_type(__x) # N: Revealed type is "builtins.int" - reveal_type(__z) # N: Revealed type is "builtins.int" - -reveal_type(C().b1) # N: Revealed type is "__main__.__B" -reveal_type(C().b2) # N: Revealed type is "__main__.__B" -[builtins fixtures/tuple.pyi] - -[case testNameManglingSlots] - -class A: - __slots__ = ["__x"] - - def __init__(self) -> None: - self.__x = 1 - -A().__x = 1 # E: "A" has no attribute "__x" -A()._A__x = 1 - -class B(A): - def __init__(self) -> None: - self.__x = 1 - -B().__x = 1 # E: "B" has no attribute "__x" -B()._B__x = 1 - -class C(A): - __slots__ = [] - - def __init__(self) -> None: - self.__x = 1 # E: Trying to assign name "_C__x" that is not in "__slots__" of type "__main__.C" - -C().__x = 1 # E: "C" has no attribute "__x" -C()._C__x = 1 # E: Trying to assign name "_C__x" that is not in "__slots__" of type "__main__.C" - -class D(A): - __slots__ = ["__x"] - - def __init__(self) -> None: - self.__x = 1 - -D().__x = 1 # E: "D" has no attribute "__x" -D()._D__x = 1 - -class E(A): - __slots__ = ("__x",) - - def __init__(self) -> None: - self.__x = 1 - -E().__x = 1 # E: "E" has no attribute "__x" -E()._E__x = 1 - -class F(A): - __slots__ = "__x" - - def __init__(self) -> None: - self.__x = 1 - -F().__x = 1 # E: "F" has no attribute "__x" -F()._F__x = 1 - -class G(A): - __slots__ = {"__x": "docstring"} - - def __init__(self) -> None: - self.__x = 1 - -G().__x = 1 # E: "G" has no attribute "__x" -G()._G__x = 1 -[builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-mangling.test b/test-data/unit/check-mangling.test new file mode 100644 index 000000000000..9657f41e6d54 --- /dev/null +++ b/test-data/unit/check-mangling.test @@ -0,0 +1,277 @@ +[case testNameManglingAttribute] + +class A: + __x: int + + def __init__(self) -> None: + self.__y: int + self._A__z: int + + def get(self) -> None: + reveal_type(self.__x) # N: Revealed type is "builtins.int" + reveal_type(self._A__x) # N: Revealed type is "builtins.int" + reveal_type(self.__y) # N: Revealed type is "builtins.int" + reveal_type(self._A__y) # N: Revealed type is "builtins.int" + reveal_type(self.__z) # N: Revealed type is "builtins.int" + reveal_type(self._A__z) # N: Revealed type is "builtins.int" + + def set(self) -> None: + self.__x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self._A__x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self.__y += "" # E: Unsupported operand types for + ("int" and "str") + self._A__y += "" # E: Unsupported operand types for + ("int" and "str") + self.__z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self._A__z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +class B(A): + __x: str + + def __init__(self) -> None: + self.__y: str + self._A__z: str # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") + + def get(self) -> None: + reveal_type(self.__x) # N: Revealed type is "builtins.str" + reveal_type(self._A__x) # N: Revealed type is "builtins.int" + reveal_type(self._B__x) # N: Revealed type is "builtins.str" + reveal_type(self.__y) # N: Revealed type is "builtins.str" + reveal_type(self._A__y) # N: Revealed type is "builtins.int" + reveal_type(self._B__y) # N: Revealed type is "builtins.str" + self.__z # E: "B" has no attribute "_B__z"; maybe "_A__z", "_B__x", or "_B__y"? + reveal_type(self._A__z) # N: Revealed type is "builtins.str" + self._B__z # E: "B" has no attribute "_B__z"; maybe "_A__z", "_B__x", or "_B__y"? + +[case testNameManglingMethod] + +class A: + + def __f(self) -> int: + ... + + def g(self) -> None: + reveal_type(self.__f()) # N: Revealed type is "builtins.int" + reveal_type(self._A__f()) # N: Revealed type is "builtins.int" + + async def __h(self) -> int: + ... + + async def i(self) -> None: + await reveal_type(self.__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" + await reveal_type(self._A__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" + +class B(A): + + def j(self) -> None: + reveal_type(self._A__f()) # N: Revealed type is "builtins.int" + self.__f() # E: "B" has no attribute "_B__f"; maybe "_A__f"? + self._B__f() # E: "B" has no attribute "_B__f"; maybe "_A__f"? + + async def k(self) -> None: + await reveal_type(self._A__h()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]" + self.__h() # E: "B" has no attribute "_B__h"; maybe "_A__h"? + self._B__h() # E: "B" has no attribute "_B__h"; maybe "_A__h"? + +class C(B): + + def __f(self) -> str: + ... + + def _A__f(self) -> str: # E: Return type "str" of "_A__f" incompatible with return type "int" in supertype "A" + ... + + async def __h(self) -> str: + ... + + async def _A__h(self) -> str: # E: Return type "Coroutine[Any, Any, str]" of "_A__h" incompatible with return type "Coroutine[Any, Any, int]" in supertype "A" + ... + +[case testNameManglingDecorator] +from typing import Callable, Protocol, Type + +class FuncWrapper(Protocol): + def __call__(__self, f: Callable[[A], int]) -> Callable[[A], str]: ... +def decorator_func(string: str) -> FuncWrapper: ... + +class ClassModifier(Protocol): + def __call__(__self, c: Type[A._A__B]) -> Type[A._A__B]: ... +def decorator_class(string: str) -> ClassModifier: ... + +class A: + __x: str = "test" + + @decorator_func(__x) + def __wrapped(self) -> int: ... + + @decorator_class(__x) + class __B: ... + +a: A +a.__wrapped() # E: "A" has no attribute "__wrapped"; maybe "_A__wrapped"? +reveal_type(a._A__wrapped()) # N: Revealed type is "builtins.str" +a.__B # E: "A" has no attribute "__B" +reveal_type(a._A__B) # N: Revealed type is "def () -> __main__.A._A__B" + +[case testNameManglingNestedClass] + +class A: + class __B: + __y: int + + def __g(self) -> str: + reveal_type(self.__y) # N: Revealed type is "builtins.int" + reveal_type(self._B__y) # N: Revealed type is "builtins.int" + return "x" + + reveal_type(__g) # N: Revealed type is "def (self: __main__.A._A__B) -> builtins.str" + reveal_type(_B__g) # N: Revealed type is "def (self: __main__.A._A__B) -> builtins.str" + + __x: int + + def __f(self) -> float: + reveal_type(self.__x) # N: Revealed type is "builtins.int" + reveal_type(self._A__x) # N: Revealed type is "builtins.int" + b = self.__B() + b.__y # E: "_A__B" has no attribute "_A__y"; maybe "_B__y"? + b._A__y # E: "_A__B" has no attribute "_A__y"; maybe "_B__y"? + reveal_type(b._B__y) # N: Revealed type is "builtins.int" + return 1.0 + + reveal_type(__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" + reveal_type(_A__f) # N: Revealed type is "def (self: __main__.A) -> builtins.float" + +[case testNameManglingInheritance] + +class __A: ... +class B(__A): ... + +import enum as __enum +class C(__enum.Flag): ... +[builtins fixtures/tuple.pyi] + +[case testNameManglingAnnotationsPast] +from typing import Generic, TypeVar + +__T = TypeVar("__T") + +class A(Generic[__T]): + __x: __T # E: Name "_A__T" is not defined + +__y = int +class __B: + def f1(self, a: A[int]) -> __y: # E: Name "_B__y" is not defined + a.__x # E: "A[int]" has no attribute "_B__x"; maybe "_A__x"? + + def f2(self, a: A) -> "__y": + a.__x # E: "A[Any]" has no attribute "_B__x"; maybe "_A__x"? + return 1 + +class C: + b1: __B # E: Name "_C__B" is not defined + b2: "__B" + + def f1(self, __x: __y, __z: int) -> None: # E: Name "_C__y" is not defined + reveal_type(__z) # N: Revealed type is "builtins.int" + + def f2(self, __x: "__y", __z: "int") -> None: + reveal_type(__x) # N: Revealed type is "builtins.int" + reveal_type(__z) # N: Revealed type is "builtins.int" + +reveal_type(C().b2) # N: Revealed type is "__main__.__B" + +[case testNameManglingAnnotationsFuture] +from __future__ import annotations +from typing import Generic, TypeVar + +__T = TypeVar("__T") +class A(Generic[__T]): + __x: __T + +__y = int +class __B: + def f1(self, a: A[int]) -> __y: + a.__x # E: "A[int]" has no attribute "_B__x"; maybe "_A__x"? + return 1 + + def f2(self, a: A) -> "__y": + a.__x # E: "A[Any]" has no attribute "_B__x"; maybe "_A__x"? + return 1 + +class C: + b1: __B + b2: "__B" + + def f1(self, __x: __y, __z: int) -> None: + reveal_type(__x) # N: Revealed type is "builtins.int" + reveal_type(__z) # N: Revealed type is "builtins.int" + + def f2(self, __x: "__y", __z: "int") -> None: + reveal_type(__x) # N: Revealed type is "builtins.int" + reveal_type(__z) # N: Revealed type is "builtins.int" + +reveal_type(C().b1) # N: Revealed type is "__main__.__B" +reveal_type(C().b2) # N: Revealed type is "__main__.__B" +[builtins fixtures/tuple.pyi] + +[case testNameManglingSlots] + +class A: + __slots__ = ["__x"] + + def __init__(self) -> None: + self.__x = 1 + +A().__x = 1 # E: "A" has no attribute "__x" +A()._A__x = 1 + +class B(A): + def __init__(self) -> None: + self.__x = 1 + +B().__x = 1 # E: "B" has no attribute "__x" +B()._B__x = 1 + +class C(A): + __slots__ = [] + + def __init__(self) -> None: + self.__x = 1 # E: Trying to assign name "_C__x" that is not in "__slots__" of type "__main__.C" + +C().__x = 1 # E: "C" has no attribute "__x" +C()._C__x = 1 # E: Trying to assign name "_C__x" that is not in "__slots__" of type "__main__.C" + +class D(A): + __slots__ = ["__x"] + + def __init__(self) -> None: + self.__x = 1 + +D().__x = 1 # E: "D" has no attribute "__x" +D()._D__x = 1 + +class E(A): + __slots__ = ("__x",) + + def __init__(self) -> None: + self.__x = 1 + +E().__x = 1 # E: "E" has no attribute "__x" +E()._E__x = 1 + +class F(A): + __slots__ = "__x" + + def __init__(self) -> None: + self.__x = 1 + +F().__x = 1 # E: "F" has no attribute "__x" +F()._F__x = 1 + +class G(A): + __slots__ = {"__x": "docstring"} + + def __init__(self) -> None: + self.__x = 1 + +G().__x = 1 # E: "G" has no attribute "__x" +G()._G__x = 1 +[builtins fixtures/dict.pyi] From ec4409b0703abeead3e7f6df7bef6a9bc6a00a8f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sun, 23 Feb 2025 19:19:32 +0100 Subject: [PATCH 39/39] do not mangle TypedDict keywords --- mypy/fastparse.py | 10 ++++- mypy/nodes.py | 4 ++ mypy/semanal_typeddict.py | 1 + test-data/unit/check-mangling.test | 71 ++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 38100a404d06..eacb69df249f 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -359,12 +359,14 @@ def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> as class NameMangler(ast3.NodeTransformer): """Mangle (nearly) all private identifiers within a class body (including nested classes).""" + mangled2unmangled: dict[str, str] _classname_complete: str _classname_trimmed: str _mangle_annotations: bool _unmangled_args: set[str] def __init__(self, classname: str, mangle_annotations: bool) -> None: + self.mangled2unmangled = {} self._classname_complete = classname self._classname_trimmed = classname.lstrip("_") self._mangle_annotations = mangle_annotations @@ -372,7 +374,9 @@ def __init__(self, classname: str, mangle_annotations: bool) -> None: def _mangle_name(self, name: str) -> str: if name.startswith("__") and not name.endswith("__"): - return f"_{self._classname_trimmed}{name}" + mangled = f"_{self._classname_trimmed}{name}" + self.mangled2unmangled[mangled] = name + return mangled return name def _mangle_slots(self, node: ast3.ClassDef) -> None: @@ -1234,7 +1238,8 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef: and any(j[0] == "annotations" for j in i.names) for i in self.imports ) - NameMangler(n.name, mangle_annotations).visit(n) + mangler = NameMangler(n.name, mangle_annotations) + mangler.visit(n) cdef = ClassDef( n.name, @@ -1244,6 +1249,7 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef: metaclass=dict(keywords).get("metaclass"), keywords=keywords, type_args=explicit_type_params, + mangled2unmangled=mangler.mangled2unmangled, ) cdef.decorators = self.translate_expr_list(n.decorator_list) self.set_line(cdef, n) diff --git a/mypy/nodes.py b/mypy/nodes.py index 8ea92ce60ae9..6658a2405cae 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1136,6 +1136,7 @@ class ClassDef(Statement): "has_incompatible_baseclass", "docstring", "removed_statements", + "mangled2unmangled", ) __match_args__ = ("name", "defs") @@ -1159,6 +1160,7 @@ class ClassDef(Statement): has_incompatible_baseclass: bool # Used by special forms like NamedTuple and TypedDict to store invalid statements removed_statements: list[Statement] + mangled2unmangled: Final[dict[str, str]] def __init__( self, @@ -1169,6 +1171,7 @@ def __init__( metaclass: Expression | None = None, keywords: list[tuple[str, Expression]] | None = None, type_args: list[TypeParam] | None = None, + mangled2unmangled: dict[str, str] | None = None, ) -> None: super().__init__() self.name = name @@ -1186,6 +1189,7 @@ def __init__( self.has_incompatible_baseclass = False self.docstring: str | None = None self.removed_statements = [] + self.mangled2unmangled = mangled2unmangled or {} @property def fullname(self) -> str: diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 0d6a0b7ff87f..7ba787c111d5 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -325,6 +325,7 @@ def analyze_typeddict_classdef_fields( self.fail(TPDICT_CLASS_ERROR, stmt) else: name = stmt.lvalues[0].name + name = defn.mangled2unmangled.get(name, name) if name in (oldfields or []): self.fail(f'Overwriting TypedDict field "{name}" while extending', stmt) if name in fields: diff --git a/test-data/unit/check-mangling.test b/test-data/unit/check-mangling.test index 9657f41e6d54..03c718f3e1cf 100644 --- a/test-data/unit/check-mangling.test +++ b/test-data/unit/check-mangling.test @@ -275,3 +275,74 @@ class G(A): G().__x = 1 # E: "G" has no attribute "__x" G()._G__x = 1 [builtins fixtures/dict.pyi] + +[case testNameManglingTypedDictBasics] +from typing import TypedDict + +class A(TypedDict): + __a: int + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'__a': builtins.int})" +reveal_type(a["__a"]) # N: Revealed type is "builtins.int" +a = {"__a": 1} +a = {"_A__a": 1} # E: Missing key "__a" for TypedDict "A" \ + # E: Extra key "_A__a" for TypedDict "A" + +class B(A): + __b: int + +b: B +reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'__a': builtins.int, '__b': builtins.int})" +reveal_type(b["__a"] + b["__b"]) # N: Revealed type is "builtins.int" +b = {"__a": 1, "__b": 2} +b = {"_A__a": 1, "_B__b": 2} # E: Missing keys ("__a", "__b") for TypedDict "B" \ + # E: Extra keys ("_A__a", "_B__b") for TypedDict "B" + +class C(TypedDict): + __c: int + +class D(B, C): + pass + +d: D +reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'__c': builtins.int, '__a': builtins.int, '__b': builtins.int})" +reveal_type(d["__a"] + d["__b"] + d["__c"]) # N: Revealed type is "builtins.int" +d = {"__a": 1, "__b": 2, "__c": 3} +d = {"_A__a": 1, "_B__b": 2, "_C__c": 3} # E: Missing keys ("__c", "__a", "__b") for TypedDict "D" \ + # E: Extra keys ("_A__a", "_B__b", "_C__c") for TypedDict "D" + +class E(D): + __a: int # E: Overwriting TypedDict field "__a" while extending + __b: int # E: Overwriting TypedDict field "__b" while extending + __c: int # E: Overwriting TypedDict field "__c" while extending + +[typing fixtures/typing-typeddict.pyi] + +[case testNameManglingTypedDictNotTotal] +from typing import TypedDict + +class A(TypedDict, total=False): + __a: int + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'__a'?: builtins.int})" +reveal_type(a["__a"]) # N: Revealed type is "builtins.int" +a = {"__a": 1} +a = {"_A__a": 1} # E: Extra key "_A__a" for TypedDict "A" + +[typing fixtures/typing-typeddict.pyi] + +[case testNameManglingTypedDictAlternativeSyntax] +from typing import TypedDict + +A = TypedDict("A", {"__a": int}) + +a: A +reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'__a': builtins.int})" +reveal_type(a["__a"]) # N: Revealed type is "builtins.int" +a = {"__a": 1} +a = {"_A__a": 1} # E: Missing key "__a" for TypedDict "A" \ + # E: Extra key "_A__a" for TypedDict "A" + +[typing fixtures/typing-typeddict.pyi]