Skip to content

Commit fb0917d

Browse files
miedzinskiJukkaL
authored andcommitted
Implement ClassVar semantics (fixes #2771) (#2873)
Implements ClassVar introduced in PEP 526. Resolves #2771, #2879. * Support annotating class attributes with ClassVar[...]. * Prohibit ClassVar annotations outside class bodies (regular variable annotations, function and method signatures). * Prohibit assignments to class variables accessed from instances (including assignments on self). * Add checks for overriding class variables with instance variables and vice versa when subclassing. * Prohibit ClassVars nested inside other types. * Add is_classvar flag to Var. * Add nesting_level attribute to TypeAnalyser and use it inside helper methods anal_type and anal_array. Move analyzing implicit TupleTypes from SemanticAnalyzer to TypeAnalyser. * Analyze ClassVar[T] as T and bare ClassVar as Any. * Prohibit generic ClassVars and accessing generic instance variables via class (see #2878 comments). * Add types.get_type_vars helper, which returns all type variables present in analyzed type, similar to TypeAnalyser.get_type_var_names.
1 parent 597d02a commit fb0917d

File tree

14 files changed

+705
-35
lines changed

14 files changed

+705
-35
lines changed

mypy/checker.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,15 @@ def check_compatibility_all_supers(self, lvalue: NameExpr, lvalue_type: Type,
11701170
lvalue.kind == MDEF and
11711171
len(lvalue_node.info.bases) > 0):
11721172

1173+
for base in lvalue_node.info.mro[1:]:
1174+
tnode = base.names.get(lvalue_node.name())
1175+
if tnode is not None:
1176+
if not self.check_compatibility_classvar_super(lvalue_node,
1177+
base,
1178+
tnode.node):
1179+
# Show only one error per variable
1180+
break
1181+
11731182
for base in lvalue_node.info.mro[1:]:
11741183
# Only check __slots__ against the 'object'
11751184
# If a base class defines a Tuple of 3 elements, a child of
@@ -1278,6 +1287,22 @@ def lvalue_type_from_base(self, expr_node: Var,
12781287

12791288
return None, None
12801289

1290+
def check_compatibility_classvar_super(self, node: Var,
1291+
base: TypeInfo, base_node: Node) -> bool:
1292+
if not isinstance(base_node, Var):
1293+
return True
1294+
if node.is_classvar and not base_node.is_classvar:
1295+
self.fail('Cannot override instance variable '
1296+
'(previously declared on base class "%s") '
1297+
'with class variable' % base.name(), node)
1298+
return False
1299+
elif not node.is_classvar and base_node.is_classvar:
1300+
self.fail('Cannot override class variable '
1301+
'(previously declared on base class "%s") '
1302+
'with instance variable' % base.name(), node)
1303+
return False
1304+
return True
1305+
12811306
def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression,
12821307
context: Context,
12831308
infer_lvalue_type: bool = True) -> None:

mypy/checkmember.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from mypy.types import (
66
Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef,
77
Overloaded, TypeVarType, UnionType, PartialType,
8-
DeletedType, NoneTyp, TypeType, function_type
8+
DeletedType, NoneTyp, TypeType, function_type, get_type_vars,
99
)
1010
from mypy.nodes import (
1111
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr,
@@ -270,6 +270,8 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont
270270
if is_lvalue and var.is_property and not var.is_settable_property:
271271
# TODO allow setting attributes in subclass (although it is probably an error)
272272
msg.read_only_property(name, info, node)
273+
if is_lvalue and var.is_classvar:
274+
msg.cant_assign_to_classvar(name, node)
273275
if var.is_initialized_in_class and isinstance(t, FunctionLike) and not t.is_type_obj():
274276
if is_lvalue:
275277
if var.is_property:
@@ -394,6 +396,8 @@ def analyze_class_attribute_access(itype: Instance,
394396
if t:
395397
if isinstance(t, PartialType):
396398
return handle_partial_attribute_type(t, is_lvalue, msg, node.node)
399+
if not is_method and (isinstance(t, TypeVarType) or get_type_vars(t)):
400+
msg.fail(messages.GENERIC_INSTANCE_VAR_CLASS_ACCESS, context)
397401
is_classmethod = is_decorated and cast(Decorator, node.node).func.is_class
398402
return add_class_tvars(t, itype, is_classmethod, builtin_type, original_type)
399403
elif isinstance(node.node, Var):

mypy/messages.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
MALFORMED_ASSERT = 'Assertion is always true, perhaps remove parentheses?'
8484
NON_BOOLEAN_IN_CONDITIONAL = 'Condition must be a boolean'
8585
DUPLICATE_TYPE_SIGNATURES = 'Function has duplicate type signatures'
86+
GENERIC_INSTANCE_VAR_CLASS_ACCESS = 'Access to generic instance variables via class is ambiguous'
8687

8788
ARG_CONSTRUCTOR_NAMES = {
8889
ARG_POS: "Arg",
@@ -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, name: str, context: Context) -> None:
819+
self.fail('Cannot assign to class variable "%s" via instance' % name, 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
@@ -642,13 +642,15 @@ class Var(SymbolNode):
642642
is_classmethod = False
643643
is_property = False
644644
is_settable_property = False
645+
is_classvar = False
645646
# Set to true when this variable refers to a module we were unable to
646647
# parse for some reason (eg a silenced module)
647648
is_suppressed_import = False
648649

649650
FLAGS = [
650651
'is_self', 'is_ready', 'is_initialized_in_class', 'is_staticmethod',
651-
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import'
652+
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import',
653+
'is_classvar'
652654
]
653655

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

mypy/semanal.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ def analyze_function(self, defn: FuncItem) -> None:
484484
if defn.type:
485485
# Signature must be analyzed in the surrounding scope so that
486486
# class-level imported names and type variables are in scope.
487+
self.check_classvar_in_signature(defn.type)
487488
defn.type = self.anal_type(defn.type)
488489
self.check_function_signature(defn)
489490
if isinstance(defn, FuncDef):
@@ -524,6 +525,20 @@ def analyze_function(self, defn: FuncItem) -> None:
524525
self.leave()
525526
self.function_stack.pop()
526527

528+
def check_classvar_in_signature(self, typ: Type) -> None:
529+
t = None # type: Type
530+
if isinstance(typ, Overloaded):
531+
for t in typ.items():
532+
self.check_classvar_in_signature(t)
533+
return
534+
if not isinstance(typ, CallableType):
535+
return
536+
for t in typ.arg_types + [typ.ret_type]:
537+
if self.is_classvar(t):
538+
self.fail_invalid_classvar(t)
539+
# Show only one error per signature
540+
break
541+
527542
def add_func_type_variables_to_symbol_table(
528543
self, defn: FuncItem) -> List[SymbolTableNode]:
529544
nodes = [] # type: List[SymbolTableNode]
@@ -1345,30 +1360,19 @@ def visit_block_maybe(self, b: Block) -> None:
13451360
def anal_type(self, t: Type, allow_tuple_literal: bool = False,
13461361
aliasing: bool = False) -> Type:
13471362
if t:
1348-
if allow_tuple_literal:
1349-
# Types such as (t1, t2, ...) only allowed in assignment statements. They'll
1350-
# generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead.
1351-
if isinstance(t, TupleType):
1352-
# Unlike TypeAnalyser, also allow implicit tuple types (without Tuple[...]).
1353-
star_count = sum(1 for item in t.items if isinstance(item, StarType))
1354-
if star_count > 1:
1355-
self.fail('At most one star type allowed in a tuple', t)
1356-
return TupleType([AnyType() for _ in t.items],
1357-
self.builtin_type('builtins.tuple'), t.line)
1358-
items = [self.anal_type(item, True)
1359-
for item in t.items]
1360-
return TupleType(items, self.builtin_type('builtins.tuple'), t.line)
13611363
a = TypeAnalyser(self.lookup_qualified,
13621364
self.lookup_fully_qualified,
13631365
self.fail,
1364-
aliasing=aliasing)
1366+
aliasing=aliasing,
1367+
allow_tuple_literal=allow_tuple_literal)
13651368
return t.accept(a)
13661369
else:
13671370
return None
13681371

13691372
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
13701373
for lval in s.lvalues:
13711374
self.analyze_lvalue(lval, explicit_type=s.type is not None)
1375+
self.check_classvar(s)
13721376
s.rvalue.accept(self)
13731377
if s.type:
13741378
allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr))
@@ -2194,6 +2198,32 @@ def build_typeddict_typeinfo(self, name: str, items: List[str],
21942198

21952199
return info
21962200

2201+
def check_classvar(self, s: AssignmentStmt) -> None:
2202+
lvalue = s.lvalues[0]
2203+
if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr):
2204+
return
2205+
if not self.is_classvar(s.type):
2206+
return
2207+
if self.is_class_scope() and isinstance(lvalue, NameExpr):
2208+
node = lvalue.node
2209+
if isinstance(node, Var):
2210+
node.is_classvar = True
2211+
elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue):
2212+
# In case of member access, report error only when assigning to self
2213+
# Other kinds of member assignments should be already reported
2214+
self.fail_invalid_classvar(lvalue)
2215+
2216+
def is_classvar(self, typ: Type) -> bool:
2217+
if not isinstance(typ, UnboundType):
2218+
return False
2219+
sym = self.lookup_qualified(typ.name, typ)
2220+
if not sym or not sym.node:
2221+
return False
2222+
return sym.node.fullname() == 'typing.ClassVar'
2223+
2224+
def fail_invalid_classvar(self, context: Context) -> None:
2225+
self.fail('ClassVar can only be used for assignments in class body', context)
2226+
21972227
def visit_decorator(self, dec: Decorator) -> None:
21982228
for d in dec.decorators:
21992229
d.accept(self)
@@ -2295,6 +2325,8 @@ def visit_for_stmt(self, s: ForStmt) -> None:
22952325
# Bind index variables and check if they define new names.
22962326
self.analyze_lvalue(s.index, explicit_type=s.index_type is not None)
22972327
if s.index_type:
2328+
if self.is_classvar(s.index_type):
2329+
self.fail_invalid_classvar(s.index)
22982330
allow_tuple_literal = isinstance(s.index, (TupleExpr, ListExpr))
22992331
s.index_type = self.anal_type(s.index_type, allow_tuple_literal)
23002332
self.store_declared_types(s.index, s.index_type)
@@ -2370,6 +2402,8 @@ def visit_with_stmt(self, s: WithStmt) -> None:
23702402
# Since we have a target, pop the next type from types
23712403
if types:
23722404
t = types.pop(0)
2405+
if self.is_classvar(t):
2406+
self.fail_invalid_classvar(n)
23732407
allow_tuple_literal = isinstance(n, (TupleExpr, ListExpr))
23742408
t = self.anal_type(t, allow_tuple_literal)
23752409
new_types.append(t)

mypy/test/testcheck.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
'check-varargs.test',
7575
'check-newsyntax.test',
7676
'check-underscores.test',
77+
'check-classvar.test',
7778
]
7879

7980
files.extend(fast_parser_files)

mypy/test/testsemanal.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'semanal-abstractclasses.test',
3131
'semanal-namedtuple.test',
3232
'semanal-typeddict.test',
33+
'semanal-classvar.test',
3334
'semanal-python2.test']
3435

3536

mypy/typeanal.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Semantic analysis of types"""
22

33
from collections import OrderedDict
4-
from typing import Callable, cast, List, Optional
4+
from typing import Callable, List, Optional, Set
55

66
from mypy.types import (
7-
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance,
7+
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, TypeVarId,
88
AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor,
99
StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args,
10+
get_type_vars,
1011
)
1112
from mypy.nodes import (
1213
BOUND_TVAR, UNBOUND_TVAR, TYPE_ALIAS, UNBOUND_IMPORTED,
@@ -81,11 +82,15 @@ def __init__(self,
8182
lookup_func: Callable[[str, Context], SymbolTableNode],
8283
lookup_fqn_func: Callable[[str], SymbolTableNode],
8384
fail_func: Callable[[str, Context], None], *,
84-
aliasing: bool = False) -> None:
85+
aliasing: bool = False,
86+
allow_tuple_literal: bool = False) -> None:
8587
self.lookup = lookup_func
8688
self.lookup_fqn_func = lookup_fqn_func
8789
self.fail = fail_func
8890
self.aliasing = aliasing
91+
self.allow_tuple_literal = allow_tuple_literal
92+
# Positive if we are analyzing arguments of another (outer) type
93+
self.nesting_level = 0
8994

9095
def visit_unbound_type(self, t: UnboundType) -> Type:
9196
if t.optional:
@@ -120,7 +125,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
120125
return self.builtin_type('builtins.tuple')
121126
if len(t.args) == 2 and isinstance(t.args[1], EllipsisType):
122127
# Tuple[T, ...] (uniform, variable-length tuple)
123-
instance = self.builtin_type('builtins.tuple', [t.args[0].accept(self)])
128+
instance = self.builtin_type('builtins.tuple', [self.anal_type(t.args[0])])
124129
instance.line = t.line
125130
return instance
126131
return self.tuple_type(self.anal_array(t.args))
@@ -132,22 +137,34 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
132137
if len(t.args) != 1:
133138
self.fail('Optional[...] must have exactly one type argument', t)
134139
return AnyType()
135-
items = self.anal_array(t.args)
140+
item = self.anal_type(t.args[0])
136141
if experiments.STRICT_OPTIONAL:
137-
return UnionType.make_simplified_union([items[0], NoneTyp()])
142+
return UnionType.make_simplified_union([item, NoneTyp()])
138143
else:
139144
# Without strict Optional checking Optional[t] is just an alias for t.
140-
return items[0]
145+
return item
141146
elif fullname == 'typing.Callable':
142147
return self.analyze_callable_type(t)
143148
elif fullname == 'typing.Type':
144149
if len(t.args) == 0:
145150
return TypeType(AnyType(), line=t.line)
146151
if len(t.args) != 1:
147152
self.fail('Type[...] must have exactly one type argument', t)
148-
items = self.anal_array(t.args)
149-
item = items[0]
153+
item = self.anal_type(t.args[0])
150154
return TypeType(item, line=t.line)
155+
elif fullname == 'typing.ClassVar':
156+
if self.nesting_level > 0:
157+
self.fail('Invalid type: ClassVar nested inside other type', t)
158+
if len(t.args) == 0:
159+
return AnyType(line=t.line)
160+
if len(t.args) != 1:
161+
self.fail('ClassVar[...] must have at most one type argument', t)
162+
return AnyType()
163+
item = self.anal_type(t.args[0])
164+
if isinstance(item, TypeVarType) or get_type_vars(item):
165+
self.fail('Invalid type: ClassVar cannot be generic', t)
166+
return AnyType()
167+
return item
151168
elif fullname == 'mypy_extensions.NoReturn':
152169
return UninhabitedType(is_noreturn=True)
153170
elif sym.kind == TYPE_ALIAS:
@@ -290,31 +307,38 @@ def visit_type_var(self, t: TypeVarType) -> Type:
290307
return t
291308

292309
def visit_callable_type(self, t: CallableType) -> Type:
293-
return t.copy_modified(arg_types=self.anal_array(t.arg_types),
294-
ret_type=t.ret_type.accept(self),
310+
return t.copy_modified(arg_types=self.anal_array(t.arg_types, nested=False),
311+
ret_type=self.anal_type(t.ret_type, nested=False),
295312
fallback=t.fallback or self.builtin_type('builtins.function'),
296313
variables=self.anal_var_defs(t.variables))
297314

298315
def visit_tuple_type(self, t: TupleType) -> Type:
299-
if t.implicit:
316+
# Types such as (t1, t2, ...) only allowed in assignment statements. They'll
317+
# generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead.
318+
if t.implicit and not self.allow_tuple_literal:
300319
self.fail('Invalid tuple literal type', t)
301320
return AnyType()
302321
star_count = sum(1 for item in t.items if isinstance(item, StarType))
303322
if star_count > 1:
304323
self.fail('At most one star type allowed in a tuple', t)
305-
return AnyType()
324+
if t.implicit:
325+
return TupleType([AnyType() for _ in t.items],
326+
self.builtin_type('builtins.tuple'),
327+
t.line)
328+
else:
329+
return AnyType()
306330
fallback = t.fallback if t.fallback else self.builtin_type('builtins.tuple', [AnyType()])
307331
return TupleType(self.anal_array(t.items), fallback, t.line)
308332

309333
def visit_typeddict_type(self, t: TypedDictType) -> Type:
310334
items = OrderedDict([
311-
(item_name, item_type.accept(self))
335+
(item_name, self.anal_type(item_type))
312336
for (item_name, item_type) in t.items.items()
313337
])
314338
return TypedDictType(items, t.fallback)
315339

316340
def visit_star_type(self, t: StarType) -> Type:
317-
return StarType(t.type.accept(self), t.line)
341+
return StarType(self.anal_type(t.type), t.line)
318342

319343
def visit_union_type(self, t: UnionType) -> Type:
320344
return UnionType(self.anal_array(t.items), t.line)
@@ -327,7 +351,7 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type:
327351
return AnyType()
328352

329353
def visit_type_type(self, t: TypeType) -> Type:
330-
return TypeType(t.item.accept(self), line=t.line)
354+
return TypeType(self.anal_type(t.item), line=t.line)
331355

332356
def analyze_callable_type(self, t: UnboundType) -> Type:
333357
fallback = self.builtin_type('builtins.function')
@@ -340,7 +364,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
340364
fallback=fallback,
341365
is_ellipsis_args=True)
342366
elif len(t.args) == 2:
343-
ret_type = t.args[1].accept(self)
367+
ret_type = self.anal_type(t.args[1])
344368
if isinstance(t.args[0], TypeList):
345369
# Callable[[ARG, ...], RET] (ordinary callable type)
346370
args = t.args[0].items
@@ -364,12 +388,21 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
364388
self.fail('Invalid function type', t)
365389
return AnyType()
366390

367-
def anal_array(self, a: List[Type]) -> List[Type]:
391+
def anal_array(self, a: List[Type], nested: bool = True) -> List[Type]:
368392
res = [] # type: List[Type]
369393
for t in a:
370-
res.append(t.accept(self))
394+
res.append(self.anal_type(t, nested))
371395
return res
372396

397+
def anal_type(self, t: Type, nested: bool = True) -> Type:
398+
if nested:
399+
self.nesting_level += 1
400+
try:
401+
return t.accept(self)
402+
finally:
403+
if nested:
404+
self.nesting_level -= 1
405+
373406
def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]:
374407
a = [] # type: List[TypeVarDef]
375408
for vd in var_defs:

0 commit comments

Comments
 (0)