Skip to content

Commit 11f6e54

Browse files
authored
Fix member access on generic classes (#6418)
Fixes #3645 Fixes #1337 Fixes #5664 The fix is straightforward, I just add/propagate the bound type variable values by mapping to supertype. I didn't find any corner cases with class methods, and essentially follow the same logic as when we generate the callable from `__init__` for generic classes in calls like `C()` or `C[int]()`. For class attributes there are two things I fixed. First we used to prohibit ambiguous access: ```python class C(Generic[T]): x: T C.x # Error! C[int].x # Error! ``` but the type variables were leaking after an error, now they are erased to `Any`. Second, I now make an exception and allow accessing attributes on `Type[C]`, this is very similar to how we allow instantiation of `Type[C]` even if it is abstract (because we expect concrete subclasses there), plus this allows accessing variables on `cls` (first argument in class methods), for example: ```python class C(Generic[T]): x: T def get(cls) -> T: return cls.x # OK ``` (I also added a bunch of more detailed comments in this part of code.)
1 parent 69a0560 commit 11f6e54

File tree

4 files changed

+275
-12
lines changed

4 files changed

+275
-12
lines changed

mypy/checkmember.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from mypy.messages import MessageBuilder
1616
from mypy.maptype import map_instance_to_supertype
1717
from mypy.expandtype import expand_type_by_instance, expand_type, freshen_function_type_vars
18+
from mypy.erasetype import erase_typevars
1819
from mypy.infer import infer_type_arguments
1920
from mypy.typevars import fill_typevars
2021
from mypy.plugin import AttributeContext
@@ -621,11 +622,47 @@ def analyze_class_attribute_access(itype: Instance,
621622
symnode = node.node
622623
assert isinstance(symnode, Var)
623624
return mx.chk.handle_partial_var_type(t, mx.is_lvalue, symnode, mx.context)
624-
if not is_method and (isinstance(t, TypeVarType) or get_type_vars(t)):
625-
mx.msg.fail(message_registry.GENERIC_INSTANCE_VAR_CLASS_ACCESS, mx.context)
625+
626+
# Find the class where method/variable was defined.
627+
if isinstance(node.node, Decorator):
628+
super_info = node.node.var.info # type: Optional[TypeInfo]
629+
elif isinstance(node.node, (Var, FuncBase)):
630+
super_info = node.node.info
631+
else:
632+
super_info = None
633+
634+
# Map the type to how it would look as a defining class. For example:
635+
# class C(Generic[T]): ...
636+
# class D(C[Tuple[T, S]]): ...
637+
# D[int, str].method()
638+
# Here itype is D[int, str], isuper is C[Tuple[int, str]].
639+
if not super_info:
640+
isuper = None
641+
else:
642+
isuper = map_instance_to_supertype(itype, super_info)
643+
644+
if isinstance(node.node, Var):
645+
assert isuper is not None
646+
# Check if original variable type has type variables. For example:
647+
# class C(Generic[T]):
648+
# x: T
649+
# C.x # Error, ambiguous access
650+
# C[int].x # Also an error, since C[int] is same as C at runtime
651+
if isinstance(t, TypeVarType) or get_type_vars(t):
652+
# Exception: access on Type[...], including first argument of class methods is OK.
653+
if not isinstance(mx.original_type, TypeType):
654+
mx.msg.fail(message_registry.GENERIC_INSTANCE_VAR_CLASS_ACCESS, mx.context)
655+
656+
# Erase non-mapped variables, but keep mapped ones, even if there is an error.
657+
# In the above example this means that we infer following types:
658+
# C.x -> Any
659+
# C[int].x -> int
660+
t = erase_typevars(expand_type_by_instance(t, isuper))
661+
626662
is_classmethod = ((is_decorated and cast(Decorator, node.node).func.is_class)
627663
or (isinstance(node.node, FuncBase) and node.node.is_class))
628-
result = add_class_tvars(t, itype, is_classmethod, mx.builtin_type, mx.original_type)
664+
result = add_class_tvars(t, itype, isuper, is_classmethod, mx.builtin_type,
665+
mx.original_type)
629666
if not mx.is_lvalue:
630667
result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,
631668
mx.msg, mx.context, chk=mx.chk)
@@ -660,33 +697,54 @@ def analyze_class_attribute_access(itype: Instance,
660697
return function_type(cast(FuncBase, node.node), mx.builtin_type('builtins.function'))
661698

662699

663-
def add_class_tvars(t: Type, itype: Instance, is_classmethod: bool,
700+
def add_class_tvars(t: Type, itype: Instance, isuper: Optional[Instance], is_classmethod: bool,
664701
builtin_type: Callable[[str], Instance],
665702
original_type: Type) -> Type:
666703
"""Instantiate type variables during analyze_class_attribute_access,
667704
e.g T and Q in the following:
668705
669-
def A(Generic(T)):
706+
class A(Generic[T]):
670707
@classmethod
671708
def foo(cls: Type[Q]) -> Tuple[T, Q]: ...
672709
673-
class B(A): pass
710+
class B(A[str]): pass
674711
675712
B.foo()
676713
677714
original_type is the value of the type B in the expression B.foo()
678715
"""
679716
# TODO: verify consistency between Q and T
680717
info = itype.type # type: TypeInfo
718+
if is_classmethod:
719+
assert isuper is not None
720+
t = expand_type_by_instance(t, isuper)
721+
# We add class type variables if the class method is accessed on class object
722+
# without applied type arguments, this matches the behavior of __init__().
723+
# For example (continuing the example in docstring):
724+
# A # The type of callable is def [T] () -> A[T], _not_ def () -> A[Any]
725+
# A[int] # The type of callable is def () -> A[int]
726+
# and
727+
# A.foo # The type is generic def [T] () -> Tuple[T, A[T]]
728+
# A[int].foo # The type is non-generic def () -> Tuple[int, A[int]]
729+
#
730+
# This behaviour is useful for defining alternative constructors for generic classes.
731+
# To achieve such behaviour, we add the class type variables that are still free
732+
# (i.e. appear in the return type of the class object on which the method was accessed).
733+
free_ids = {t.id for t in itype.args if isinstance(t, TypeVarType)}
734+
681735
if isinstance(t, CallableType):
682-
# TODO: Should we propagate type variable values?
736+
# NOTE: in practice either all or none of the variables are free, since
737+
# visit_type_application() will detect any type argument count mismatch and apply
738+
# a correct number of Anys.
683739
tvars = [TypeVarDef(n, n, i + 1, [], builtin_type('builtins.object'), tv.variance)
684-
for (i, n), tv in zip(enumerate(info.type_vars), info.defn.type_vars)]
740+
for (i, n), tv in zip(enumerate(info.type_vars), info.defn.type_vars)
741+
# use 'is' to avoid id clashes with unrelated variables
742+
if any(tv.id is id for id in free_ids)]
685743
if is_classmethod:
686744
t = bind_self(t, original_type, is_classmethod=True)
687745
return t.copy_modified(variables=tvars + t.variables)
688746
elif isinstance(t, Overloaded):
689-
return Overloaded([cast(CallableType, add_class_tvars(item, itype, is_classmethod,
747+
return Overloaded([cast(CallableType, add_class_tvars(item, itype, isuper, is_classmethod,
690748
builtin_type, original_type))
691749
for item in t.items()])
692750
return t

mypy/newsemanal/semanal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ def analyze_class(self, defn: ClassDef) -> None:
923923
self.prepare_class_def(defn)
924924

925925
defn.type_vars = tvar_defs
926-
defn.info.type_vars = [tvar.fullname for tvar in tvar_defs]
926+
defn.info.type_vars = [tvar.name for tvar in tvar_defs]
927927
if base_error:
928928
defn.info.fallback_to_any = True
929929

test-data/unit/check-generics.test

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,3 +1845,208 @@ def g(x: T) -> T: return x
18451845
[out]
18461846
main:3: error: Revealed type is 'def [b.T] (x: b.T`-1) -> b.T`-1'
18471847
main:4: error: Revealed type is 'def [T] (x: T`-1) -> T`-1'
1848+
1849+
[case testGenericClassMethodSimple]
1850+
from typing import Generic, TypeVar
1851+
T = TypeVar('T')
1852+
1853+
class C(Generic[T]):
1854+
@classmethod
1855+
def get(cls) -> T: ...
1856+
1857+
class D(C[str]): ...
1858+
1859+
reveal_type(D.get()) # E: Revealed type is 'builtins.str*'
1860+
reveal_type(D().get()) # E: Revealed type is 'builtins.str*'
1861+
[builtins fixtures/classmethod.pyi]
1862+
1863+
[case testGenericClassMethodExpansion]
1864+
from typing import Generic, TypeVar, Tuple
1865+
T = TypeVar('T')
1866+
1867+
class C(Generic[T]):
1868+
@classmethod
1869+
def get(cls) -> T: ...
1870+
class D(C[Tuple[T, T]]): ...
1871+
class E(D[str]): ...
1872+
1873+
reveal_type(E.get()) # E: Revealed type is 'Tuple[builtins.str*, builtins.str*]'
1874+
reveal_type(E().get()) # E: Revealed type is 'Tuple[builtins.str*, builtins.str*]'
1875+
[builtins fixtures/classmethod.pyi]
1876+
1877+
[case testGenericClassMethodExpansionReplacingTypeVar]
1878+
from typing import Generic, TypeVar
1879+
T = TypeVar('T')
1880+
S = TypeVar('S')
1881+
1882+
class C(Generic[T]):
1883+
@classmethod
1884+
def get(cls) -> T: ...
1885+
1886+
class D(C[S]): ...
1887+
class E(D[int]): ...
1888+
1889+
reveal_type(E.get()) # E: Revealed type is 'builtins.int*'
1890+
reveal_type(E().get()) # E: Revealed type is 'builtins.int*'
1891+
[builtins fixtures/classmethod.pyi]
1892+
1893+
[case testGenericClassMethodUnboundOnClass]
1894+
from typing import Generic, TypeVar
1895+
T = TypeVar('T')
1896+
1897+
class C(Generic[T]):
1898+
@classmethod
1899+
def get(cls) -> T: ...
1900+
@classmethod
1901+
def make_one(cls, x: T) -> C[T]: ...
1902+
1903+
reveal_type(C.get) # E: Revealed type is 'def [T] () -> T`1'
1904+
reveal_type(C[int].get) # E: Revealed type is 'def () -> builtins.int*'
1905+
reveal_type(C.make_one) # E: Revealed type is 'def [T] (x: T`1) -> __main__.C[T`1]'
1906+
reveal_type(C[int].make_one) # E: Revealed type is 'def (x: builtins.int*) -> __main__.C[builtins.int*]'
1907+
[builtins fixtures/classmethod.pyi]
1908+
1909+
[case testGenericClassMethodUnboundOnSubClass]
1910+
from typing import Generic, TypeVar, Tuple
1911+
T = TypeVar('T')
1912+
S = TypeVar('S')
1913+
1914+
class C(Generic[T]):
1915+
@classmethod
1916+
def get(cls) -> T: ...
1917+
@classmethod
1918+
def make_one(cls, x: T) -> C[T]: ...
1919+
class D(C[Tuple[T, S]]): ...
1920+
class E(D[S, str]): ...
1921+
1922+
reveal_type(D.make_one) # E: Revealed type is 'def [T, S] (x: Tuple[T`1, S`2]) -> __main__.C[Tuple[T`1, S`2]]'
1923+
reveal_type(D[int, str].make_one) # E: Revealed type is 'def (x: Tuple[builtins.int*, builtins.str*]) -> __main__.C[Tuple[builtins.int*, builtins.str*]]'
1924+
reveal_type(E.make_one) # E: Revealed type is 'def [S] (x: Tuple[S`1, builtins.str*]) -> __main__.C[Tuple[S`1, builtins.str*]]'
1925+
reveal_type(E[int].make_one) # E: Revealed type is 'def (x: Tuple[builtins.int*, builtins.str*]) -> __main__.C[Tuple[builtins.int*, builtins.str*]]'
1926+
[builtins fixtures/classmethod.pyi]
1927+
1928+
[case testGenericClassMethodUnboundOnClassNonMatchingIdNonGeneric]
1929+
from typing import Generic, TypeVar, Any, Tuple, Type
1930+
1931+
T = TypeVar('T')
1932+
S = TypeVar('S')
1933+
Q = TypeVar('Q', bound=A[Any])
1934+
1935+
class A(Generic[T]):
1936+
@classmethod
1937+
def foo(cls: Type[Q]) -> Tuple[T, Q]: ...
1938+
1939+
class B(A[T], Generic[T, S]):
1940+
def meth(self) -> None:
1941+
reveal_type(A[T].foo) # E: Revealed type is 'def () -> Tuple[T`1, __main__.A*[T`1]]'
1942+
@classmethod
1943+
def other(cls) -> None:
1944+
reveal_type(cls.foo) # E: Revealed type is 'def [T, S] () -> Tuple[T`1, __main__.B*[T`1, S`2]]'
1945+
reveal_type(B.foo) # E: Revealed type is 'def [T, S] () -> Tuple[T`1, __main__.B*[T`1, S`2]]'
1946+
[builtins fixtures/classmethod.pyi]
1947+
1948+
[case testGenericClassAttrUnboundOnClass]
1949+
from typing import Generic, TypeVar
1950+
T = TypeVar('T')
1951+
1952+
class C(Generic[T]):
1953+
x: T
1954+
@classmethod
1955+
def get(cls) -> T:
1956+
return cls.x # OK
1957+
1958+
x = C.x # E: Access to generic instance variables via class is ambiguous
1959+
reveal_type(x) # E: Revealed type is 'Any'
1960+
xi = C[int].x # E: Access to generic instance variables via class is ambiguous
1961+
reveal_type(xi) # E: Revealed type is 'builtins.int'
1962+
[builtins fixtures/classmethod.pyi]
1963+
1964+
[case testGenericClassAttrUnboundOnSubClass]
1965+
from typing import Generic, TypeVar, Tuple
1966+
T = TypeVar('T')
1967+
1968+
class C(Generic[T]):
1969+
x: T
1970+
class D(C[int]): ...
1971+
class E(C[int]):
1972+
x = 42
1973+
1974+
x = D.x # E: Access to generic instance variables via class is ambiguous
1975+
reveal_type(x) # E: Revealed type is 'builtins.int'
1976+
E.x # OK
1977+
1978+
[case testGenericClassMethodOverloaded]
1979+
from typing import Generic, TypeVar, overload, Tuple
1980+
T = TypeVar('T')
1981+
1982+
class C(Generic[T]):
1983+
@overload
1984+
@classmethod
1985+
def get(cls) -> T: ...
1986+
@overload
1987+
@classmethod
1988+
def get(cls, n: int) -> Tuple[T, ...]: ...
1989+
@classmethod
1990+
def get(cls, n: int = 0):
1991+
pass
1992+
1993+
class D(C[str]): ...
1994+
1995+
reveal_type(D.get()) # E: Revealed type is 'builtins.str'
1996+
reveal_type(D.get(42)) # E: Revealed type is 'builtins.tuple[builtins.str]'
1997+
[builtins fixtures/classmethod.pyi]
1998+
1999+
[case testGenericClassMethodAnnotation]
2000+
from typing import Generic, TypeVar, Type
2001+
T = TypeVar('T')
2002+
2003+
class Maker(Generic[T]):
2004+
x: T
2005+
@classmethod
2006+
def get(cls) -> T: ...
2007+
2008+
class B(Maker[B]): ...
2009+
2010+
def f(o: Maker[T]) -> T:
2011+
if bool():
2012+
return o.x
2013+
return o.get()
2014+
b = f(B())
2015+
reveal_type(b) # E: Revealed type is '__main__.B*'
2016+
2017+
def g(t: Type[Maker[T]]) -> T:
2018+
if bool():
2019+
return t.x
2020+
return t.get()
2021+
bb = g(B)
2022+
reveal_type(bb) # E: Revealed type is '__main__.B*'
2023+
[builtins fixtures/classmethod.pyi]
2024+
2025+
[case testGenericClassMethodAnnotationDecorator]
2026+
from typing import Generic, Callable, TypeVar, Iterator
2027+
2028+
T = TypeVar('T')
2029+
2030+
class Box(Generic[T]):
2031+
@classmethod
2032+
def wrap(cls, generator: Callable[[], T]) -> Box[T]: ...
2033+
2034+
class IteratorBox(Box[Iterator[T]]): ...
2035+
2036+
@IteratorBox.wrap # E: Argument 1 to "wrap" of "Box" has incompatible type "Callable[[], int]"; expected "Callable[[], Iterator[<nothing>]]"
2037+
def g() -> int:
2038+
...
2039+
[builtins fixtures/classmethod.pyi]
2040+
2041+
[case testGenericClassMethodInGenericFunction]
2042+
from typing import Generic, TypeVar
2043+
T = TypeVar('T')
2044+
S = TypeVar('S')
2045+
2046+
class C(Generic[T]):
2047+
@classmethod
2048+
def get(cls) -> T: ...
2049+
2050+
def func(x: S) -> S:
2051+
return C[S].get()
2052+
[builtins fixtures/classmethod.pyi]

test-data/unit/check-inference.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,8 +2602,8 @@ class C(A):
26022602
x = ['12']
26032603

26042604
reveal_type(A.x) # E: Revealed type is 'builtins.list[Any]'
2605-
reveal_type(B.x) # E: Revealed type is 'builtins.list[builtins.int*]'
2606-
reveal_type(C.x) # E: Revealed type is 'builtins.list[builtins.str*]'
2605+
reveal_type(B.x) # E: Revealed type is 'builtins.list[builtins.int]'
2606+
reveal_type(C.x) # E: Revealed type is 'builtins.list[builtins.str]'
26072607

26082608
[builtins fixtures/list.pyi]
26092609

0 commit comments

Comments
 (0)