Skip to content

Commit 27ddabc

Browse files
committed
Remove ClassVarType, use Var.is_classvar flag
1 parent 950b383 commit 27ddabc

11 files changed

+259
-53
lines changed

mypy/checker.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from mypy import nodes
2626
from mypy.types import (
2727
Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType, TypedDictType,
28-
Instance, NoneTyp, ErrorType, strip_type, TypeType, ClassVarType,
28+
Instance, NoneTyp, ErrorType, strip_type, TypeType,
2929
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef,
3030
true_only, false_only, function_type, is_named_instance
3131
)
@@ -1575,9 +1575,6 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type,
15751575
15761576
Return the inferred rvalue_type and whether to infer anything about the attribute type
15771577
"""
1578-
if isinstance(instance_type, Instance) and isinstance(attribute_type, ClassVarType):
1579-
self.msg.fail("Illegal assignment to class variable", context)
1580-
15811578
# Descriptors don't participate in class-attribute access
15821579
if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or
15831580
isinstance(instance_type, TypeType)):

mypy/checkexpr.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mypy.types import (
88
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
99
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
10-
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, ClassVarType,
10+
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType,
1111
true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike,
1212
get_typ_args, set_typ_args,
1313
StarType)
@@ -2218,8 +2218,6 @@ def bool_type(self) -> Instance:
22182218
return self.named_type('builtins.bool')
22192219

22202220
def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type:
2221-
if isinstance(known_type, ClassVarType):
2222-
known_type = known_type.item
22232221
if expr.literal >= LITERAL_TYPE:
22242222
restriction = self.chk.binder.get(expr)
22252223
if restriction:

mypy/checkmember.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
268268
if is_lvalue and var.is_property and not var.is_settable_property:
269269
# TODO allow setting attributes in subclass (although it is probably an error)
270270
msg.read_only_property(name, info, node)
271+
if is_lvalue and var.is_classvar:
272+
msg.cant_assign_to_classvar(node)
271273
if var.is_initialized_in_class and isinstance(t, FunctionLike) and not t.is_type_obj():
272274
if is_lvalue:
273275
if var.is_property:

mypy/messages.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
CANNOT_ACCESS_INIT = 'Cannot access "__init__" directly'
6262
CANNOT_ASSIGN_TO_METHOD = 'Cannot assign to a method'
6363
CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type'
64+
CANNOT_ASSIGN_TO_CLASSVAR = 'Illegal assignment to class variable'
6465
INCONSISTENT_ABSTRACT_OVERLOAD = \
6566
'Overloaded method has both abstract and non-abstract variants'
6667
READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE = \
@@ -814,6 +815,9 @@ def base_class_definitions_incompatible(self, name: str, base1: TypeInfo,
814815
def cant_assign_to_method(self, context: Context) -> None:
815816
self.fail(CANNOT_ASSIGN_TO_METHOD, context)
816817

818+
def cant_assign_to_classvar(self, context: Context) -> None:
819+
self.fail(CANNOT_ASSIGN_TO_CLASSVAR, context)
820+
817821
def read_only_property(self, name: str, type: TypeInfo,
818822
context: Context) -> None:
819823
self.fail('Property "{}" defined in "{}" is read-only'.format(

mypy/nodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,13 +637,15 @@ class Var(SymbolNode):
637637
is_classmethod = False
638638
is_property = False
639639
is_settable_property = False
640+
is_classvar = False
640641
# Set to true when this variable refers to a module we were unable to
641642
# parse for some reason (eg a silenced module)
642643
is_suppressed_import = False
643644

644645
FLAGS = [
645646
'is_self', 'is_ready', 'is_initialized_in_class', 'is_staticmethod',
646-
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import'
647+
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import',
648+
'is_classvar'
647649
]
648650

649651
def __init__(self, name: str, type: 'mypy.types.Type' = None) -> None:

mypy/semanal.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
from mypy.errors import Errors, report_internal_error
7474
from mypy.types import (
7575
NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType,
76-
FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, ClassVarType,
76+
FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType,
7777
TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType,
7878
)
7979
from mypy.nodes import implicit_module_attrs
@@ -1330,6 +1330,7 @@ def anal_type(self, t: Type, allow_tuple_literal: bool = False,
13301330
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
13311331
for lval in s.lvalues:
13321332
self.analyze_lvalue(lval, explicit_type=s.type is not None)
1333+
self.check_classvar(s)
13331334
s.rvalue.accept(self)
13341335
if s.type:
13351336
allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr))
@@ -1363,7 +1364,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
13631364
self.process_typevar_declaration(s)
13641365
self.process_namedtuple_definition(s)
13651366
self.process_typeddict_definition(s)
1366-
self.check_classvar_definition(s)
13671367

13681368
if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and
13691369
s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and
@@ -2160,9 +2160,51 @@ def build_typeddict_typeinfo(self, name: str, items: List[str],
21602160

21612161
return info
21622162

2163-
def check_classvar_definition(self, s: AssignmentStmt) -> None:
2164-
if isinstance(s.type, ClassVarType) and not self.is_class_scope():
2165-
self.fail("Invalid ClassVar definition", s)
2163+
def check_classvar(self, s: AssignmentStmt) -> None:
2164+
lvalue = s.lvalues[0]
2165+
if len(s.lvalues) != 1 or not isinstance(lvalue, NameExpr):
2166+
return
2167+
is_classvar = self.check_classvar_definition(lvalue, s.type)
2168+
if self.is_class_scope() and isinstance(lvalue.node, Var):
2169+
# Assignments to class variables outside class scope are checked later
2170+
self.check_classvar_override(lvalue.node, is_classvar)
2171+
2172+
def check_classvar_definition(self, lvalue: NameExpr, typ: Type) -> bool:
2173+
if not isinstance(typ, UnboundType):
2174+
return False
2175+
sym = self.lookup_qualified(typ.name, typ)
2176+
if not sym or not sym.node:
2177+
return False
2178+
fullname = sym.node.fullname()
2179+
if fullname != 'typing.ClassVar':
2180+
return False
2181+
if self.is_class_scope() or not isinstance(lvalue.node, Var):
2182+
node = cast(Var, lvalue.node)
2183+
node.is_classvar = True
2184+
return True
2185+
else:
2186+
self.fail('Invalid ClassVar definition', lvalue)
2187+
return False
2188+
2189+
def check_classvar_override(self, node: Var, is_classvar: bool) -> None:
2190+
name = node.name()
2191+
for base in self.type.mro[1:]:
2192+
tnode = base.names.get(name)
2193+
if tnode is None:
2194+
continue
2195+
base_node = tnode.node
2196+
print(base_node)
2197+
if isinstance(base_node, Var):
2198+
v = base_node
2199+
if (is_classvar and not v.is_classvar) or (not is_classvar and v.is_classvar):
2200+
self.fail_classvar_base_incompatibility(node, v)
2201+
return
2202+
2203+
def fail_classvar_base_incompatibility(self, shadowing: Var, original: Var) -> None:
2204+
base_name = original.info.name()
2205+
self.fail('Invalid class attribute definition '
2206+
'(previously declared on base class "%s")' % base_name,
2207+
shadowing)
21662208

21672209
def visit_decorator(self, dec: Decorator) -> None:
21682210
for d in dec.decorators:

mypy/subtypes.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from mypy.types import (
44
Type, AnyType, UnboundType, TypeVisitor, ErrorType, FormalArgument, Void, NoneTyp,
55
Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded,
6-
ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance,
7-
ClassVarType,
6+
ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance
87
)
98
import mypy.applytype
109
import mypy.constraints
@@ -53,9 +52,6 @@ def is_subtype(left: Type, right: Type,
5352
return any(is_subtype(left, item, type_parameter_checker,
5453
ignore_pos_arg_names=ignore_pos_arg_names)
5554
for item in right.items)
56-
elif isinstance(right, ClassVarType):
57-
return is_subtype(left, right.item, type_parameter_checker,
58-
ignore_pos_arg_names=ignore_pos_arg_names)
5955
else:
6056
return left.accept(SubtypeVisitor(right, type_parameter_checker,
6157
ignore_pos_arg_names=ignore_pos_arg_names))

mypy/typeanal.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Callable, cast, List, Optional
55

66
from mypy.types import (
7-
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, ClassVarType,
7+
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance,
88
AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor,
99
StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args,
1010
)
@@ -149,11 +149,13 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
149149
item = items[0]
150150
return TypeType(item, line=t.line)
151151
elif fullname == 'typing.ClassVar':
152+
if len(t.args) == 0:
153+
return AnyType(line=t.line)
152154
if len(t.args) != 1:
153155
self.fail('ClassVar[...] must have exactly one type argument', t)
154156
return AnyType()
155157
items = self.anal_array(t.args)
156-
return ClassVarType(items[0])
158+
return items[0]
157159
elif fullname == 'mypy_extensions.NoReturn':
158160
return UninhabitedType(is_noreturn=True)
159161
elif sym.kind == TYPE_ALIAS:

mypy/types.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,29 +1189,6 @@ def deserialize(cls, data: JsonDict) -> 'TypeType':
11891189
return TypeType(Type.deserialize(data['item']))
11901190

11911191

1192-
class ClassVarType(Type):
1193-
"""The ClassVar[T] type.
1194-
1195-
In all typechecking scenarios, behaves like T.
1196-
"""
1197-
item = None # type: Type
1198-
1199-
def __init__(self, item: Type, *, line: int = -1, column: int = -1) -> None:
1200-
super().__init__(line, column)
1201-
self.item = item
1202-
1203-
def accept(self, visitor: 'TypeVisitor[T]') -> T:
1204-
return visitor.visit_classvar_type(self)
1205-
1206-
def serialize(self) -> JsonDict:
1207-
return {'.class': 'ClassVarType', 'item': self.item.serialize()}
1208-
1209-
@classmethod
1210-
def deserialize(cls, data: JsonDict) -> 'ClassVarType':
1211-
assert data['.class'] == 'ClassVarType'
1212-
return ClassVarType(Type.deserialize(data['item']))
1213-
1214-
12151192
#
12161193
# Visitor-related classes
12171194
#
@@ -1303,9 +1280,6 @@ def visit_ellipsis_type(self, t: EllipsisType) -> T:
13031280
def visit_type_type(self, t: TypeType) -> T:
13041281
pass
13051282

1306-
def visit_classvar_type(self, t: ClassVarType) -> T:
1307-
pass
1308-
13091283

13101284
class TypeTranslator(TypeVisitor[Type]):
13111285
"""Identity type transformation.
@@ -1539,9 +1513,6 @@ def visit_ellipsis_type(self, t: EllipsisType) -> str:
15391513
def visit_type_type(self, t: TypeType) -> str:
15401514
return 'Type[{}]'.format(t.item.accept(self))
15411515

1542-
def visit_classvar_type(self, t: ClassVarType) -> str:
1543-
return 'ClassVar[{}]'.format(t.item.accept(self))
1544-
15451516
def list_str(self, a: List[Type]) -> str:
15461517
"""Convert items of an array to strings (pretty-print types)
15471518
and join the results with commas.

test-data/unit/check-classvar.test

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A().x = 2
1212
[out]
1313
main:4: error: Illegal assignment to class variable
1414

15-
[case testAssignmentOnSubclass]
15+
[case testAssignmentOnSubclassInstance]
1616
from typing import ClassVar
1717
class A:
1818
x = 1 # type: ClassVar[int]
@@ -22,18 +22,64 @@ B().x = 2
2222
[out]
2323
main:6: error: Illegal assignment to class variable
2424

25+
[case testOverrideOnSelf]
26+
from typing import ClassVar
27+
class A:
28+
x = None # type: ClassVar[int]
29+
def __init__(self) -> None:
30+
self.x = 0
31+
[out]
32+
main:5: error: Illegal assignment to class variable
33+
34+
[case testOverrideOnSelfInSubclass]
35+
from typing import ClassVar
36+
class A:
37+
x = None # type: ClassVar[int]
38+
class B(A):
39+
def __init__(self) -> None:
40+
self.x = 0
41+
[out]
42+
main:6: error: Illegal assignment to class variable
43+
2544
[case testReadingFromInstance]
2645
from typing import ClassVar
2746
class A:
2847
x = 1 # type: ClassVar[int]
2948
A().x
3049

31-
[case testTypecheck]
50+
[case testTypecheckSimple]
3251
from typing import ClassVar
3352
class A:
3453
x = 1 # type: ClassVar[int]
3554
y = A.x # type: int
3655

56+
[case testTypecheckWithUserType]
57+
from typing import ClassVar
58+
class A:
59+
pass
60+
class B:
61+
x = A() # type: ClassVar[A]
62+
63+
[case testTypeCheckOnAssignment]
64+
from typing import ClassVar
65+
class A:
66+
pass
67+
class B:
68+
pass
69+
class C:
70+
x = None # type: ClassVar[A]
71+
C.x = B()
72+
[out]
73+
main:8: error: Incompatible types in assignment (expression has type "B", variable has type "A")
74+
75+
[case testRevealType]
76+
from typing import ClassVar
77+
class A:
78+
x = None # type: ClassVar[int]
79+
reveal_type(A.x)
80+
[out]
81+
main:4: error: Revealed type is 'builtins.int'
82+
3783
[case testInfer]
3884
from typing import ClassVar
3985
class A:
@@ -42,3 +88,53 @@ y = A.x
4288
reveal_type(y)
4389
[out]
4490
main:5: error: Revealed type is 'builtins.int'
91+
92+
[case testAssignmentOnUnion]
93+
from typing import ClassVar, Union
94+
class A:
95+
x = None # type: int
96+
class B:
97+
x = None # type: ClassVar[int]
98+
c = A() # type: Union[A, B]
99+
c.x = 1
100+
[out]
101+
main:7: error: Illegal assignment to class variable
102+
103+
[case testAssignmentOnInstanceFromType]
104+
from typing import ClassVar, Type
105+
class A:
106+
x = None # type: ClassVar[int]
107+
def f(a: Type[A]) -> None:
108+
a().x = 0
109+
[out]
110+
main:5: error: Illegal assignment to class variable
111+
112+
[case testAssignmentOnInstanceFromSubclassType]
113+
from typing import ClassVar, Type
114+
class A:
115+
x = None # type: ClassVar[int]
116+
class B(A):
117+
pass
118+
def f(b: Type[B]) -> None:
119+
b().x = 0
120+
[out]
121+
main:7: error: Illegal assignment to class variable
122+
123+
[case testAssignmentWithGeneric]
124+
from typing import ClassVar, List
125+
class A:
126+
x = None # type: ClassVar[List[int]]
127+
A.x = ['a']
128+
[builtins fixtures/list.pyi]
129+
[out]
130+
main:4: error: List item 0 has incompatible type "str"
131+
132+
[case testAssignmentToCallableRet]
133+
from typing import ClassVar, Type
134+
class A:
135+
x = None # type: ClassVar[int]
136+
def f() -> A:
137+
return A()
138+
f().x = 0
139+
[out]
140+
main:6: error: Illegal assignment to class variable

0 commit comments

Comments
 (0)