From 54f495421e3aaf7d3fede9ff899ce78c1cd6cf66 Mon Sep 17 00:00:00 2001 From: Max Chang <62986841+changhoetyng@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:15:58 +0100 Subject: [PATCH] [PEP 695] Fix multiple nested classes don't work (#17820) This PR modifies the `lookup_fully_qualified_or_none` method to support multiple nested classes. Fixes #17780 --- mypy/semanal.py | 50 ++++++++++---- test-data/unit/check-python312.test | 76 ++++++++++++++++++++++ test-data/unit/fine-grained-python312.test | 20 ++++++ 3 files changed, 135 insertions(+), 11 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 37aafc3b2647..95efe2b0f30c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6461,18 +6461,46 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non Note that this can't be used for names nested in class namespaces. """ # TODO: unify/clean-up/simplify lookup methods, see #4157. - # TODO: support nested classes (but consider performance impact, - # we might keep the module level only lookup for thing like 'builtins.int'). - assert "." in fullname module, name = fullname.rsplit(".", maxsplit=1) - if module not in self.modules: - return None - filenode = self.modules[module] - result = filenode.names.get(name) - if result is None and self.is_incomplete_namespace(module): - # TODO: More explicit handling of incomplete refs? - self.record_incomplete_ref() - return result + + if module in self.modules: + # If the module exists, look up the name in the module. + # This is the common case. + filenode = self.modules[module] + result = filenode.names.get(name) + if result is None and self.is_incomplete_namespace(module): + # TODO: More explicit handling of incomplete refs? + self.record_incomplete_ref() + return result + else: + # Else, try to find the longest prefix of the module name that is in the modules dictionary. + splitted_modules = fullname.split(".") + names = [] + + while splitted_modules and ".".join(splitted_modules) not in self.modules: + names.append(splitted_modules.pop()) + + if not splitted_modules or not names: + # If no module or name is found, return None. + return None + + # Reverse the names list to get the correct order of names. + names.reverse() + + module = ".".join(splitted_modules) + filenode = self.modules[module] + result = filenode.names.get(names[0]) + + if result is None and self.is_incomplete_namespace(module): + # TODO: More explicit handling of incomplete refs? + self.record_incomplete_ref() + + for part in names[1:]: + if result is not None and isinstance(result.node, TypeInfo): + result = result.node.names.get(part) + else: + return None + return result def object_type(self) -> Instance: return self.named_type("builtins.object") diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 085cc052705d..c5c8ada1aae1 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1873,3 +1873,79 @@ d1: Multi[int, str] = Multi[float, str]() # E: Incompatible types in assignment d2: Multi[float, str] = Multi[int, str]() # E: Incompatible types in assignment (expression has type "Multi[int, str]", variable has type "Multi[float, str]") d3: Multi[str, int] = Multi[str, float]() d4: Multi[str, float] = Multi[str, int]() # E: Incompatible types in assignment (expression has type "Multi[str, int]", variable has type "Multi[str, float]") + +[case testPEP695MultipleNestedGenericClass1] +# flags: --enable-incomplete-feature=NewGenericSyntax +class A: + class B: + class C: + class D[Q]: + def g(self, x: Q): ... + d: D[str] + +x: A.B.C.D[int] +x.g('a') # E: Argument 1 to "g" of "D" has incompatible type "str"; expected "int" +reveal_type(x) # N: Revealed type is "__main__.A.B.C.D[builtins.int]" +reveal_type(A.B.C.d) # N: Revealed type is "__main__.A.B.C.D[builtins.str]" + +[case testPEP695MultipleNestedGenericClass2] +# flags: --enable-incomplete-feature=NewGenericSyntax +class A: + class B: + def m(self) -> None: + class C[T]: + def f(self) -> T: ... + x: C[int] + reveal_type(x.f()) # N: Revealed type is "builtins.int" + self.a = C[str]() + +reveal_type(A().B().a) # N: Revealed type is "__main__.C@5[builtins.str]" + +[case testPEP695MultipleNestedGenericClass3] +# flags: --enable-incomplete-feature=NewGenericSyntax +class A: + class C[T]: + def f(self) -> T: ... + class D[S]: + x: T # E: Name "T" is not defined + def g(self) -> S: ... + +a: A.C[int] +reveal_type(a.f()) # N: Revealed type is "builtins.int" +b: A.C.D[str] +reveal_type(b.g()) # N: Revealed type is "builtins.str" + +class B: + class E[T]: + class F[T]: # E: "T" already defined as a type parameter + x: T + +c: B.E.F[int] + +[case testPEP695MultipleNestedGenericClass4] +# flags: --enable-incomplete-feature=NewGenericSyntax +class Z: + class A: + class B[T]: + def __get__(self, instance: Z.A, owner: type[Z.A]) -> T: + return None # E: Incompatible return value type (got "None", expected "T") + f = B[int]() + +a = Z.A() +v = a.f + +[case testPEP695MultipleNestedGenericClass5] +# flags: --enable-incomplete-feature=NewGenericSyntax +from a.b.c import d +x: d.D.E.F.G[int] +x.g('a') # E: Argument 1 to "g" of "G" has incompatible type "str"; expected "int" +reveal_type(x) # N: Revealed type is "a.b.c.d.D.E.F.G[builtins.int]" +reveal_type(d.D.E.F.d) # N: Revealed type is "a.b.c.d.D.E.F.G[builtins.str]" + +[file a/b/c/d.py] +class D: + class E: + class F: + class G[Q]: + def g(self, x: Q): ... + d: G[str] diff --git a/test-data/unit/fine-grained-python312.test b/test-data/unit/fine-grained-python312.test index 0e438ca06574..2cb2148a66fe 100644 --- a/test-data/unit/fine-grained-python312.test +++ b/test-data/unit/fine-grained-python312.test @@ -95,3 +95,23 @@ def f(x: int) -> None: pass [out] == main:7: error: Missing positional argument "x" in call to "f" + +[case testPEP695MultipleNestedGenericClassMethodUpdated] +from a import f + +class A: + class C: + class D[T]: + x: T + def m(self) -> T: + f() + return self.x + +[file a.py] +def f() -> None: pass + +[file a.py.2] +def f(x: int) -> None: pass +[out] +== +main:8: error: Missing positional argument "x" in call to "f"