Skip to content

Commit 9003e64

Browse files
committed
Allow class variables in subsubclasses to only match base class type
Related python#10375 and python#10506. For an unhinted assignment to a class variable defined in a base class, this allows subderived classes to only match the type in the base class, rather than the one inferred from the assignment value.
1 parent cc1bcc9 commit 9003e64

File tree

4 files changed

+106
-34
lines changed

4 files changed

+106
-34
lines changed

mypy/checker.py

+40-31
Original file line numberDiff line numberDiff line change
@@ -2952,39 +2952,49 @@ def check_compatibility_all_supers(
29522952
# Show only one error per variable
29532953
break
29542954

2955-
direct_bases = lvalue_node.info.direct_base_classes()
2956-
last_immediate_base = direct_bases[-1] if direct_bases else None
2955+
if is_private(lvalue_node.name):
2956+
return False
29572957

2958-
for base in lvalue_node.info.mro[1:]:
2959-
# The type of "__slots__" and some other attributes usually doesn't need to
2960-
# be compatible with a base class. We'll still check the type of "__slots__"
2961-
# against "object" as an exception.
2962-
if lvalue_node.allow_incompatible_override and not (
2963-
lvalue_node.name == "__slots__" and base.fullname == "builtins.object"
2958+
for base, base_type, base_node in self.classvar_base_types(lvalue_node):
2959+
if not self.check_compatibility_super(
2960+
lvalue, lvalue_type, rvalue, base, base_type, base_node
29642961
):
2965-
continue
2966-
2967-
if is_private(lvalue_node.name):
2968-
continue
2969-
2970-
base_type, base_node = self.lvalue_type_from_base(lvalue_node, base)
2971-
if isinstance(base_type, PartialType):
2972-
base_type = None
2973-
2974-
if base_type:
2975-
assert base_node is not None
2976-
if not self.check_compatibility_super(
2977-
lvalue, lvalue_type, rvalue, base, base_type, base_node
2978-
):
2979-
# Only show one error per variable; even if other
2980-
# base classes are also incompatible
2981-
return True
2982-
if base is last_immediate_base:
2983-
# At this point, the attribute was found to be compatible with all
2984-
# immediate parents.
2985-
break
2962+
# Only show one error per variable; even if other
2963+
# base classes are also incompatible
2964+
return True
29862965
return False
29872966

2967+
def classvar_base_types(self, node: Var) -> list[tuple[TypeInfo, Type, Node]]:
2968+
"""Determine base classes that a class variable should be checked against."""
2969+
base_types = []
2970+
direct_bases = node.info.direct_base_classes()
2971+
last_immediate_base = direct_bases[-1] if direct_bases else None
2972+
for base in node.info.mro[1:]:
2973+
# The type of "__slots__" and some other attributes usually doesn't need to
2974+
# be compatible with a base class. We'll still check the type of "__slots__"
2975+
# against "object" as an exception.
2976+
if node.allow_incompatible_override and not (
2977+
node.name == "__slots__" and base.fullname == "builtins.object"
2978+
):
2979+
continue
2980+
base_type, base_node = self.lvalue_type_from_base(node, base)
2981+
if not base_type or isinstance(base_type, PartialType):
2982+
continue
2983+
assert base_node is not None
2984+
if isinstance(base_node, Var) and base_node.is_inferred:
2985+
# Skip the type inferred from the value if there is a superclass
2986+
# with an annotation or a (possibly more general) inferred type.
2987+
base_node_base_types = self.classvar_base_types(base_node)
2988+
if base_node_base_types:
2989+
base_types.extend(base_node_base_types)
2990+
else:
2991+
base_types.append((base, base_type, base_node))
2992+
else:
2993+
base_types.append((base, base_type, base_node))
2994+
if base is last_immediate_base:
2995+
break
2996+
return base_types
2997+
29882998
def check_compatibility_super(
29892999
self,
29903000
lvalue: RefExpr,
@@ -3053,8 +3063,7 @@ def check_compatibility_super(
30533063
def lvalue_type_from_base(
30543064
self, expr_node: Var, base: TypeInfo
30553065
) -> tuple[Type | None, Node | None]:
3056-
"""For a NameExpr that is part of a class, walk all base classes and try
3057-
to find the first class that defines a Type for the same name."""
3066+
"""Get a NameExpr type from a given base class."""
30583067
expr_name = expr_node.name
30593068
base_var = base.names.get(expr_name)
30603069

test-data/unit/check-classes.test

+1-3
Original file line numberDiff line numberDiff line change
@@ -4107,7 +4107,7 @@ from typing import Union
41074107
class A:
41084108
a = None # type: Union[int, str]
41094109
class B(A):
4110-
a = 1
4110+
a = 1 # type: int
41114111
class C(B):
41124112
a = "str"
41134113
class D(A):
@@ -4344,8 +4344,6 @@ class B(A):
43444344
x = 1
43454345
class C(B):
43464346
x = ''
4347-
[out]
4348-
main:6: error: Incompatible types in assignment (expression has type "str", base class "B" defined the type as "int")
43494347

43504348
[case testSlots]
43514349
class A:

test-data/unit/check-classvar.test

+41
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,44 @@ class C:
334334
c:C
335335
c.foo() # E: Too few arguments \
336336
# N: "foo" is considered instance variable, to make it class variable use ClassVar[...]
337+
338+
[case testClassVarVariableLengthTuple]
339+
from typing import ClassVar, Tuple
340+
class A:
341+
x: ClassVar[Tuple[int, ...]]
342+
class B(A):
343+
x = (1,)
344+
class C(B):
345+
x = (2, 3)
346+
class D(B):
347+
x = ("a",) # E: Incompatible types in assignment (expression has type "Tuple[str]", base class "A" defined the type as "Tuple[int, ...]")
348+
[builtins fixtures/tuple.pyi]
349+
350+
[case testClassVarVariableLengthTupleTwoBases]
351+
from typing import ClassVar, Tuple, Union
352+
class A:
353+
x: ClassVar[Tuple[Union[int, str], ...]]
354+
class B:
355+
x: ClassVar[Tuple[int, ...]]
356+
class C(A, B):
357+
x = (1,)
358+
class D(C):
359+
x = (2, 3)
360+
class E(A, B):
361+
x = ("a",) # E: Incompatible types in assignment (expression has type "Tuple[str]", base class "B" defined the type as "Tuple[int, ...]")
362+
[builtins fixtures/tuple.pyi]
363+
364+
[case testClassVarVariableLengthTupleGeneric]
365+
from typing import ClassVar, Generic, Tuple, TypeVar
366+
T = TypeVar('T')
367+
class A(Generic[T]):
368+
x: ClassVar[Tuple[T, ...]] # E: ClassVar cannot contain type variables
369+
class B(A[int]):
370+
pass
371+
class C(B):
372+
x = (1,)
373+
class D(C):
374+
x = (2, 3)
375+
class E(B):
376+
x = ("a",) # E: Incompatible types in assignment (expression has type "Tuple[str]", base class "A" defined the type as "Tuple[int, ...]")
377+
[builtins fixtures/tuple.pyi]

test-data/unit/check-inference.test

+24
Original file line numberDiff line numberDiff line change
@@ -3302,6 +3302,30 @@ class C(P):
33023302
x = ['a'] # E: List item 0 has incompatible type "str"; expected "int"
33033303
[builtins fixtures/list.pyi]
33043304

3305+
[case testUseSupertypeForUntypedList]
3306+
from typing import List
3307+
class A:
3308+
x: List[int] = []
3309+
class B(A):
3310+
x = []
3311+
class C(B):
3312+
x: List[str] = [] # E: Incompatible types in assignment (expression has type "List[str]", base class "A" defined the type as "List[int]")
3313+
class D(B):
3314+
x = ["a"] # E: List item 0 has incompatible type "str"; expected "int"
3315+
[builtins fixtures/list.pyi]
3316+
3317+
[case testUseSupertypeForUntypedTuple]
3318+
from typing import Tuple
3319+
class A:
3320+
x = (1,)
3321+
class B(A):
3322+
x = (2,)
3323+
class C(A):
3324+
x = (2, 3) # E: Incompatible types in assignment (expression has type "Tuple[int, int]", base class "A" defined the type as "Tuple[int]")
3325+
class D(B):
3326+
x = (2, 3) # E: Incompatible types in assignment (expression has type "Tuple[int, int]", base class "A" defined the type as "Tuple[int]")
3327+
[builtins fixtures/tuple.pyi]
3328+
33053329
[case testUseSupertypeAsInferenceContextPartial]
33063330
from typing import List
33073331

0 commit comments

Comments
 (0)