Skip to content

Commit ad57093

Browse files
authored
Cleanup generic class variable access (#19292)
Fixes #5144 Fixes #15223 This is related to the work on #7724 Now that all attribute access goes through `checkmember.py` there is not much benefit in giving an error at the definition site, especially that it prohibits some valid (and common) use cases, see comments in #5144. While looking at this I discovered a bunch of defects in the _implementation_, that I also fix (I am keeping unsafe self-type related logic as is): * We used to erase type vars of the definition class instead of the use class. This caused type variables leaks. * The erasure was inconsistent, so that in some cases we silently erased type variables to `Any` even in allowed use cases * `TypeVarTuple` and `ParamSpec` were not handled as equal to regular type variables (I guess because of old problems with erasing them)
1 parent d52ce3b commit ad57093

File tree

7 files changed

+81
-36
lines changed

7 files changed

+81
-36
lines changed

mypy/checkmember.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
freeze_all_type_vars,
4646
function_type,
4747
get_all_type_vars,
48-
get_type_vars,
4948
make_simplified_union,
5049
supported_self_type,
5150
tuple_fallback,
@@ -1196,31 +1195,36 @@ def analyze_class_attribute_access(
11961195

11971196
if isinstance(node.node, Var):
11981197
assert isuper is not None
1198+
object_type = get_proper_type(mx.self_type)
11991199
# Check if original variable type has type variables. For example:
12001200
# class C(Generic[T]):
12011201
# x: T
12021202
# C.x # Error, ambiguous access
12031203
# C[int].x # Also an error, since C[int] is same as C at runtime
12041204
# Exception is Self type wrapped in ClassVar, that is safe.
1205+
prohibit_self = not node.node.is_classvar
12051206
def_vars = set(node.node.info.defn.type_vars)
1206-
if not node.node.is_classvar and node.node.info.self_type:
1207+
if prohibit_self and node.node.info.self_type:
12071208
def_vars.add(node.node.info.self_type)
1208-
# TODO: should we include ParamSpec etc. here (i.e. use get_all_type_vars)?
1209-
typ_vars = set(get_type_vars(t))
1210-
if def_vars & typ_vars:
1211-
# Exception: access on Type[...], including first argument of class methods is OK.
1212-
if not isinstance(get_proper_type(mx.original_type), TypeType) or node.implicit:
1213-
if node.node.is_classvar:
1214-
message = message_registry.GENERIC_CLASS_VAR_ACCESS
1215-
else:
1216-
message = message_registry.GENERIC_INSTANCE_VAR_CLASS_ACCESS
1217-
mx.fail(message)
1209+
# Exception: access on Type[...], including first argument of class methods is OK.
1210+
prohibit_generic = not isinstance(object_type, TypeType) or node.implicit
1211+
if prohibit_generic and def_vars & set(get_all_type_vars(t)):
1212+
if node.node.is_classvar:
1213+
message = message_registry.GENERIC_CLASS_VAR_ACCESS
1214+
else:
1215+
message = message_registry.GENERIC_INSTANCE_VAR_CLASS_ACCESS
1216+
mx.fail(message)
12181217
t = expand_self_type_if_needed(t, mx, node.node, itype, is_class=True)
1218+
t = expand_type_by_instance(t, isuper)
12191219
# Erase non-mapped variables, but keep mapped ones, even if there is an error.
12201220
# In the above example this means that we infer following types:
12211221
# C.x -> Any
12221222
# C[int].x -> int
1223-
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})
1223+
if prohibit_generic:
1224+
erase_vars = set(itype.type.defn.type_vars)
1225+
if prohibit_self and itype.type.self_type:
1226+
erase_vars.add(itype.type.self_type)
1227+
t = erase_typevars(t, {tv.id for tv in erase_vars})
12241228

12251229
is_classmethod = (
12261230
(is_decorated and cast(Decorator, node.node).func.is_class)

mypy/message_registry.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
253253
'Cannot override class variable (previously declared on base class "{}") with instance '
254254
"variable"
255255
)
256-
CLASS_VAR_WITH_TYPEVARS: Final = "ClassVar cannot contain type variables"
257256
CLASS_VAR_WITH_GENERIC_SELF: Final = "ClassVar cannot contain Self type in generic classes"
258257
CLASS_VAR_OUTSIDE_OF_CLASS: Final = "ClassVar can only be used for assignments in class body"
259258

mypy/semanal.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5079,14 +5079,6 @@ def check_classvar(self, s: AssignmentStmt) -> None:
50795079
node.is_classvar = True
50805080
analyzed = self.anal_type(s.type)
50815081
assert self.type is not None
5082-
if analyzed is not None and set(get_type_vars(analyzed)) & set(
5083-
self.type.defn.type_vars
5084-
):
5085-
# This means that we have a type var defined inside of a ClassVar.
5086-
# This is not allowed by PEP526.
5087-
# See https://github.com/python/mypy/issues/11538
5088-
5089-
self.fail(message_registry.CLASS_VAR_WITH_TYPEVARS, s)
50905082
if (
50915083
analyzed is not None
50925084
and self.type.self_type in get_type_vars(analyzed)

test-data/unit/check-classvar.test

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ main:3: error: Cannot assign to class variable "x" via instance
285285
from typing import ClassVar, Generic, TypeVar
286286
T = TypeVar('T')
287287
class A(Generic[T]):
288-
x: ClassVar[T] # E: ClassVar cannot contain type variables
288+
x: ClassVar[T] # Error reported at access site
289289
@classmethod
290290
def foo(cls) -> T:
291291
return cls.x # OK
@@ -308,7 +308,7 @@ from typing import ClassVar, Generic, Tuple, TypeVar, Union, Type
308308
T = TypeVar('T')
309309
U = TypeVar('U')
310310
class A(Generic[T, U]):
311-
x: ClassVar[Union[T, Tuple[U, Type[U]]]] # E: ClassVar cannot contain type variables
311+
x: ClassVar[Union[T, Tuple[U, Type[U]]]] # Error reported at access site
312312
@classmethod
313313
def foo(cls) -> Union[T, Tuple[U, Type[U]]]:
314314
return cls.x # OK
@@ -319,7 +319,9 @@ A[int, str].x # E: Access to generic class variables is ambiguous
319319

320320
class Bad(A[int, str]):
321321
pass
322-
Bad.x # E: Access to generic class variables is ambiguous
322+
reveal_type(Bad.x) # E: Access to generic class variables is ambiguous \
323+
# N: Revealed type is "Union[builtins.int, tuple[builtins.str, type[builtins.str]]]"
324+
reveal_type(Bad().x) # N: Revealed type is "Union[builtins.int, tuple[builtins.str, type[builtins.str]]]"
323325

324326
class Good(A[int, str]):
325327
x = 42
@@ -343,3 +345,18 @@ class C:
343345
g: ClassVar[Union[Callable[[C], int], int]] = f
344346

345347
reveal_type(C().g) # N: Revealed type is "Union[def () -> builtins.int, builtins.int]"
348+
349+
[case testGenericSubclassAccessNoLeak]
350+
from typing import ClassVar, Generic, TypeVar
351+
352+
T = TypeVar("T")
353+
class B(Generic[T]):
354+
x: T
355+
y: ClassVar[T]
356+
357+
class C(B[T]): ...
358+
359+
reveal_type(C.x) # E: Access to generic instance variables via class is ambiguous \
360+
# N: Revealed type is "Any"
361+
reveal_type(C.y) # E: Access to generic class variables is ambiguous \
362+
# N: Revealed type is "Any"

test-data/unit/check-selftype.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,6 +2220,28 @@ class C(A, B): # OK: both methods take Self
22202220
pass
22212221
[builtins fixtures/tuple.pyi]
22222222

2223+
[case testSelfTypeClassMethodNotSilentlyErased]
2224+
from typing import Self, Optional
2225+
2226+
class X:
2227+
_inst: Optional[Self] = None
2228+
@classmethod
2229+
def default(cls) -> Self:
2230+
reveal_type(cls._inst) # N: Revealed type is "Union[Self`0, None]"
2231+
if cls._inst is None:
2232+
cls._inst = cls()
2233+
return cls._inst
2234+
2235+
reveal_type(X._inst) # E: Access to generic instance variables via class is ambiguous \
2236+
# N: Revealed type is "Union[__main__.X, None]"
2237+
reveal_type(X()._inst) # N: Revealed type is "Union[__main__.X, None]"
2238+
2239+
class Y(X): ...
2240+
reveal_type(Y._inst) # E: Access to generic instance variables via class is ambiguous \
2241+
# N: Revealed type is "Union[__main__.Y, None]"
2242+
reveal_type(Y()._inst) # N: Revealed type is "Union[__main__.Y, None]"
2243+
[builtins fixtures/tuple.pyi]
2244+
22232245
[case testSelfInFuncDecoratedClassmethod]
22242246
from collections.abc import Callable
22252247
from typing import Self, TypeVar

test-data/unit/check-typevar-tuple.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2629,6 +2629,28 @@ def test(*args: Unpack[tuple[T]]) -> int: ...
26292629
reveal_type(fn(test)) # N: Revealed type is "def [T] (T`1) -> builtins.int"
26302630
[builtins fixtures/tuple.pyi]
26312631

2632+
[case testNoGenericTypeVarTupleClassVarAccess]
2633+
from typing import Generic, Tuple, TypeVarTuple, Unpack
2634+
2635+
Ts = TypeVarTuple("Ts")
2636+
class C(Generic[Unpack[Ts]]):
2637+
x: Tuple[Unpack[Ts]]
2638+
2639+
reveal_type(C.x) # E: Access to generic instance variables via class is ambiguous \
2640+
# N: Revealed type is "builtins.tuple[Any, ...]"
2641+
2642+
class Bad(C[int, int]):
2643+
pass
2644+
reveal_type(Bad.x) # E: Access to generic instance variables via class is ambiguous \
2645+
# N: Revealed type is "tuple[builtins.int, builtins.int]"
2646+
reveal_type(Bad().x) # N: Revealed type is "tuple[builtins.int, builtins.int]"
2647+
2648+
class Good(C[int, int]):
2649+
x = (1, 1)
2650+
reveal_type(Good.x) # N: Revealed type is "tuple[builtins.int, builtins.int]"
2651+
reveal_type(Good().x) # N: Revealed type is "tuple[builtins.int, builtins.int]"
2652+
[builtins fixtures/tuple.pyi]
2653+
26322654
[case testConstraintsIncludeTupleFallback]
26332655
from typing import Generic, TypeVar
26342656
from typing_extensions import TypeVarTuple, Unpack

test-data/unit/semanal-classvar.test

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,3 @@ class B:
207207
pass
208208
[out]
209209
main:4: error: ClassVar can only be used for assignments in class body
210-
211-
[case testClassVarWithTypeVariable]
212-
from typing import ClassVar, TypeVar, Generic, List
213-
214-
T = TypeVar('T')
215-
216-
class Some(Generic[T]):
217-
error: ClassVar[T] # E: ClassVar cannot contain type variables
218-
nested: ClassVar[List[List[T]]] # E: ClassVar cannot contain type variables
219-
ok: ClassVar[int]
220-
[out]

0 commit comments

Comments
 (0)