Skip to content

Commit

Permalink
[PEP 695] Fix multiple nested classes don't work (#17820)
Browse files Browse the repository at this point in the history
This PR modifies the `lookup_fully_qualified_or_none` method to support
multiple nested classes.

Fixes #17780
  • Loading branch information
changhoetyng authored Oct 11, 2024
1 parent 46c108e commit 54f4954
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 11 deletions.
50 changes: 39 additions & 11 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
76 changes: 76 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
20 changes: 20 additions & 0 deletions test-data/unit/fine-grained-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit 54f4954

Please sign in to comment.