Skip to content

Commit 54f4954

Browse files
authored
[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
1 parent 46c108e commit 54f4954

File tree

3 files changed

+135
-11
lines changed

3 files changed

+135
-11
lines changed

mypy/semanal.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6461,18 +6461,46 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non
64616461
Note that this can't be used for names nested in class namespaces.
64626462
"""
64636463
# TODO: unify/clean-up/simplify lookup methods, see #4157.
6464-
# TODO: support nested classes (but consider performance impact,
6465-
# we might keep the module level only lookup for thing like 'builtins.int').
6466-
assert "." in fullname
64676464
module, name = fullname.rsplit(".", maxsplit=1)
6468-
if module not in self.modules:
6469-
return None
6470-
filenode = self.modules[module]
6471-
result = filenode.names.get(name)
6472-
if result is None and self.is_incomplete_namespace(module):
6473-
# TODO: More explicit handling of incomplete refs?
6474-
self.record_incomplete_ref()
6475-
return result
6465+
6466+
if module in self.modules:
6467+
# If the module exists, look up the name in the module.
6468+
# This is the common case.
6469+
filenode = self.modules[module]
6470+
result = filenode.names.get(name)
6471+
if result is None and self.is_incomplete_namespace(module):
6472+
# TODO: More explicit handling of incomplete refs?
6473+
self.record_incomplete_ref()
6474+
return result
6475+
else:
6476+
# Else, try to find the longest prefix of the module name that is in the modules dictionary.
6477+
splitted_modules = fullname.split(".")
6478+
names = []
6479+
6480+
while splitted_modules and ".".join(splitted_modules) not in self.modules:
6481+
names.append(splitted_modules.pop())
6482+
6483+
if not splitted_modules or not names:
6484+
# If no module or name is found, return None.
6485+
return None
6486+
6487+
# Reverse the names list to get the correct order of names.
6488+
names.reverse()
6489+
6490+
module = ".".join(splitted_modules)
6491+
filenode = self.modules[module]
6492+
result = filenode.names.get(names[0])
6493+
6494+
if result is None and self.is_incomplete_namespace(module):
6495+
# TODO: More explicit handling of incomplete refs?
6496+
self.record_incomplete_ref()
6497+
6498+
for part in names[1:]:
6499+
if result is not None and isinstance(result.node, TypeInfo):
6500+
result = result.node.names.get(part)
6501+
else:
6502+
return None
6503+
return result
64766504

64776505
def object_type(self) -> Instance:
64786506
return self.named_type("builtins.object")

test-data/unit/check-python312.test

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,3 +1873,79 @@ d1: Multi[int, str] = Multi[float, str]() # E: Incompatible types in assignment
18731873
d2: Multi[float, str] = Multi[int, str]() # E: Incompatible types in assignment (expression has type "Multi[int, str]", variable has type "Multi[float, str]")
18741874
d3: Multi[str, int] = Multi[str, float]()
18751875
d4: Multi[str, float] = Multi[str, int]() # E: Incompatible types in assignment (expression has type "Multi[str, int]", variable has type "Multi[str, float]")
1876+
1877+
[case testPEP695MultipleNestedGenericClass1]
1878+
# flags: --enable-incomplete-feature=NewGenericSyntax
1879+
class A:
1880+
class B:
1881+
class C:
1882+
class D[Q]:
1883+
def g(self, x: Q): ...
1884+
d: D[str]
1885+
1886+
x: A.B.C.D[int]
1887+
x.g('a') # E: Argument 1 to "g" of "D" has incompatible type "str"; expected "int"
1888+
reveal_type(x) # N: Revealed type is "__main__.A.B.C.D[builtins.int]"
1889+
reveal_type(A.B.C.d) # N: Revealed type is "__main__.A.B.C.D[builtins.str]"
1890+
1891+
[case testPEP695MultipleNestedGenericClass2]
1892+
# flags: --enable-incomplete-feature=NewGenericSyntax
1893+
class A:
1894+
class B:
1895+
def m(self) -> None:
1896+
class C[T]:
1897+
def f(self) -> T: ...
1898+
x: C[int]
1899+
reveal_type(x.f()) # N: Revealed type is "builtins.int"
1900+
self.a = C[str]()
1901+
1902+
reveal_type(A().B().a) # N: Revealed type is "__main__.C@5[builtins.str]"
1903+
1904+
[case testPEP695MultipleNestedGenericClass3]
1905+
# flags: --enable-incomplete-feature=NewGenericSyntax
1906+
class A:
1907+
class C[T]:
1908+
def f(self) -> T: ...
1909+
class D[S]:
1910+
x: T # E: Name "T" is not defined
1911+
def g(self) -> S: ...
1912+
1913+
a: A.C[int]
1914+
reveal_type(a.f()) # N: Revealed type is "builtins.int"
1915+
b: A.C.D[str]
1916+
reveal_type(b.g()) # N: Revealed type is "builtins.str"
1917+
1918+
class B:
1919+
class E[T]:
1920+
class F[T]: # E: "T" already defined as a type parameter
1921+
x: T
1922+
1923+
c: B.E.F[int]
1924+
1925+
[case testPEP695MultipleNestedGenericClass4]
1926+
# flags: --enable-incomplete-feature=NewGenericSyntax
1927+
class Z:
1928+
class A:
1929+
class B[T]:
1930+
def __get__(self, instance: Z.A, owner: type[Z.A]) -> T:
1931+
return None # E: Incompatible return value type (got "None", expected "T")
1932+
f = B[int]()
1933+
1934+
a = Z.A()
1935+
v = a.f
1936+
1937+
[case testPEP695MultipleNestedGenericClass5]
1938+
# flags: --enable-incomplete-feature=NewGenericSyntax
1939+
from a.b.c import d
1940+
x: d.D.E.F.G[int]
1941+
x.g('a') # E: Argument 1 to "g" of "G" has incompatible type "str"; expected "int"
1942+
reveal_type(x) # N: Revealed type is "a.b.c.d.D.E.F.G[builtins.int]"
1943+
reveal_type(d.D.E.F.d) # N: Revealed type is "a.b.c.d.D.E.F.G[builtins.str]"
1944+
1945+
[file a/b/c/d.py]
1946+
class D:
1947+
class E:
1948+
class F:
1949+
class G[Q]:
1950+
def g(self, x: Q): ...
1951+
d: G[str]

test-data/unit/fine-grained-python312.test

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,23 @@ def f(x: int) -> None: pass
9595
[out]
9696
==
9797
main:7: error: Missing positional argument "x" in call to "f"
98+
99+
[case testPEP695MultipleNestedGenericClassMethodUpdated]
100+
from a import f
101+
102+
class A:
103+
class C:
104+
class D[T]:
105+
x: T
106+
def m(self) -> T:
107+
f()
108+
return self.x
109+
110+
[file a.py]
111+
def f() -> None: pass
112+
113+
[file a.py.2]
114+
def f(x: int) -> None: pass
115+
[out]
116+
==
117+
main:8: error: Missing positional argument "x" in call to "f"

0 commit comments

Comments
 (0)