From b0d9fe5ea3ce2d27dd5243d1b0468c5538acf182 Mon Sep 17 00:00:00 2001 From: changhoetyng Date: Wed, 25 Sep 2024 00:18:05 +0100 Subject: [PATCH 1/6] multiple nested case --- mypy/semanal.py | 25 ++++++++- test-data/unit/check-python312.test | 60 ++++++++++++++++++++++ test-data/unit/fine-grained-python312.test | 20 ++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 0b654d6b145ff..b4800adf9fb1e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6408,9 +6408,32 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non # 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) + + # The reason for this is that we want to be able to handle cases such as importlib.machinery if module not in self.modules: - return None + # Check if there's nested module A.B.C + splitted = fullname.rsplit(".") + module, name = splitted[0], splitted[1:] + # If module still not in modules, return None + if module not in self.modules: + return None + filenode = self.modules[module] + result = filenode.names.get(name[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 name[1:]: + if result is not None and isinstance(result.node, TypeInfo): + filenode = result.node + result = filenode.names.get(part) + else: + return None + return result + filenode = self.modules[module] result = filenode.names.get(name) if result is None and self.is_incomplete_namespace(module): diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 085cc052705d7..39cf8270bf7c5 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1873,3 +1873,63 @@ 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 diff --git a/test-data/unit/fine-grained-python312.test b/test-data/unit/fine-grained-python312.test index 0e438ca06574c..2cb2148a66fef 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" From 9b3fe0590e53e9e6940ad12ce526c7c4b182d98e Mon Sep 17 00:00:00 2001 From: changhoetyng Date: Wed, 25 Sep 2024 00:40:29 +0100 Subject: [PATCH 2/6] fix: failed type check --- mypy/semanal.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index b4800adf9fb1e..e66eea7262138 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6415,21 +6415,20 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non if module not in self.modules: # Check if there's nested module A.B.C splitted = fullname.rsplit(".") - module, name = splitted[0], splitted[1:] + module, names = splitted[0], splitted[1:] # If module still not in modules, return None if module not in self.modules: return None filenode = self.modules[module] - result = filenode.names.get(name[0]) + 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 name[1:]: + for part in names[1:]: if result is not None and isinstance(result.node, TypeInfo): - filenode = result.node - result = filenode.names.get(part) + result = result.node.names.get(part) else: return None return result From a267bb8630dda9098245c613582fd2fa61742e3d Mon Sep 17 00:00:00 2001 From: changhoetyng Date: Tue, 8 Oct 2024 21:54:12 +0100 Subject: [PATCH 3/6] fix: longest prefix for nested modules --- mypy/semanal.py | 52 +++++++++++++++++------------ test-data/unit/check-python312.test | 16 +++++++++ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 28b2784ef67c9..10f337b099385 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6409,35 +6409,43 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non # we might keep the module level only lookup for thing like 'builtins.int'). assert "." in fullname - module, name = fullname.rsplit(".", maxsplit=1) - - # The reason for this is that we want to be able to handle cases such as importlib.machinery - if module not in self.modules: - # Check if there's nested module A.B.C - splitted = fullname.rsplit(".") - module, names = splitted[0], splitted[1:] - # If module still not in modules, return None - if module not in self.modules: - return None - filenode = self.modules[module] - result = filenode.names.get(names[0]) + splitted_modules = fullname.rsplit(".") + names = [] - if result is None and self.is_incomplete_namespace(module): - # TODO: More explicit handling of incomplete refs? - self.record_incomplete_ref() + while splitted_modules and ".".join(splitted_modules) not in self.modules: + ''' + Try to find the module in the modules dictionary. - 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 + If the module is not found, pop the last element of the splitted list and append it to the names list. + + This is to find the longest prefix of the module name that is in the modules dictionary. + ''' + 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(name) + 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: diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 39cf8270bf7c5..c5c8ada1aae11 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1933,3 +1933,19 @@ class Z: 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] From ec773b2c57e7766dea7d299e613ecede8267f7d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:54:45 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/semanal.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 10f337b099385..3321d7064f333 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6413,24 +6413,24 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non names = [] while splitted_modules and ".".join(splitted_modules) not in self.modules: - ''' + """ Try to find the module in the modules dictionary. If the module is not found, pop the last element of the splitted list and append it to the names list. This is to find the longest prefix of the module name that is in the modules dictionary. - ''' + """ 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) From 5a28ea7d1c9702de0bbf4fff478e3e2a92349212 Mon Sep 17 00:00:00 2001 From: changhoetyng Date: Wed, 9 Oct 2024 19:03:27 +0100 Subject: [PATCH 5/6] minor style and performance modification --- mypy/semanal.py | 68 ++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 3321d7064f333..e525063c30c82 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6407,46 +6407,46 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non # 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 - - splitted_modules = fullname.rsplit(".") - names = [] - - while splitted_modules and ".".join(splitted_modules) not in self.modules: - """ - Try to find the module in the modules dictionary. + module, name = fullname.rsplit(".", maxsplit=1) + + 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 = [] - If the module is not found, pop the last element of the splitted list and append it to the names list. + while splitted_modules and ".".join(splitted_modules) not in self.modules: + names.append(splitted_modules.pop()) - This is to find the longest prefix of the module name that is in the modules dictionary. - """ - names.append(splitted_modules.pop()) + if not splitted_modules or not names: + # If no module or name is found, return None. + return None - 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() - """ - 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]) - 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() + 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 + 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") From d3029ae52ad5dfe4e88ebe57fb0cb2f981a55100 Mon Sep 17 00:00:00 2001 From: changhoetyng Date: Thu, 10 Oct 2024 11:20:31 +0100 Subject: [PATCH 6/6] removed TODOS --- mypy/semanal.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index b175a5ce9780f..95efe2b0f30c1 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6461,8 +6461,6 @@ 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'). module, name = fullname.rsplit(".", maxsplit=1) if module in self.modules: