From d6b370d2035bfe7861e62c43af22272b3d8974d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Wed, 15 Feb 2017 23:46:17 +0100 Subject: [PATCH 01/23] Implement ClassVar semantics (fixes #2771) --- mypy/checker.py | 5 +++- mypy/checkexpr.py | 4 ++- mypy/semanal.py | 7 ++++- mypy/subtypes.py | 6 +++- mypy/test/testcheck.py | 1 + mypy/test/testsemanal.py | 1 + mypy/typeanal.py | 8 ++++- mypy/types.py | 29 ++++++++++++++++++ test-data/unit/check-classvar.test | 44 ++++++++++++++++++++++++++++ test-data/unit/lib-stub/typing.pyi | 1 + test-data/unit/semanal-classvar.test | 40 +++++++++++++++++++++++++ 11 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 test-data/unit/check-classvar.test create mode 100644 test-data/unit/semanal-classvar.test diff --git a/mypy/checker.py b/mypy/checker.py index 5beb52a39e56..7227d362e4f3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -25,7 +25,7 @@ from mypy import nodes from mypy.types import ( Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType, TypedDictType, - Instance, NoneTyp, ErrorType, strip_type, TypeType, + Instance, NoneTyp, ErrorType, strip_type, TypeType, ClassVarType, UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef, true_only, false_only, function_type, is_named_instance ) @@ -1575,6 +1575,9 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, Return the inferred rvalue_type and whether to infer anything about the attribute type """ + if isinstance(instance_type, Instance) and isinstance(attribute_type, ClassVarType): + self.msg.fail("Illegal assignment to class variable", context) + # Descriptors don't participate in class-attribute access if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance(instance_type, TypeType)): diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f54703997b53..c0fd8c35bf23 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -7,7 +7,7 @@ from mypy.types import ( Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef, TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType, - PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, + PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, ClassVarType, true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, get_typ_args, set_typ_args, StarType) @@ -2218,6 +2218,8 @@ def bool_type(self) -> Instance: return self.named_type('builtins.bool') def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type: + if isinstance(known_type, ClassVarType): + known_type = known_type.item if expr.literal >= LITERAL_TYPE: restriction = self.chk.binder.get(expr) if restriction: diff --git a/mypy/semanal.py b/mypy/semanal.py index fe51d2e91476..cb8d6012f0be 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -73,7 +73,7 @@ from mypy.errors import Errors, report_internal_error from mypy.types import ( NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType, - FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, + FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, ClassVarType, TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType, ) from mypy.nodes import implicit_module_attrs @@ -1363,6 +1363,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.process_typevar_declaration(s) self.process_namedtuple_definition(s) self.process_typeddict_definition(s) + self.check_classvar_definition(s) if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and @@ -2159,6 +2160,10 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], return info + def check_classvar_definition(self, s: AssignmentStmt): + if isinstance(s.type, ClassVarType) and not self.is_class_scope(): + self.fail("Invalid ClassVar definition", s) + def visit_decorator(self, dec: Decorator) -> None: for d in dec.decorators: d.accept(self) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4af105898bf3..e0e85ae0aa00 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -3,7 +3,8 @@ from mypy.types import ( Type, AnyType, UnboundType, TypeVisitor, ErrorType, FormalArgument, Void, NoneTyp, Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded, - ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance + ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance, + ClassVarType, ) import mypy.applytype import mypy.constraints @@ -52,6 +53,9 @@ def is_subtype(left: Type, right: Type, return any(is_subtype(left, item, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names) for item in right.items) + elif isinstance(right, ClassVarType): + return is_subtype(left, right.item, type_parameter_checker, + ignore_pos_arg_names=ignore_pos_arg_names) else: return left.accept(SubtypeVisitor(right, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names)) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index c34356c6a619..fd2bc499876a 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -74,6 +74,7 @@ 'check-varargs.test', 'check-newsyntax.test', 'check-underscores.test', + 'check-classvar.test', ] files.extend(fast_parser_files) diff --git a/mypy/test/testsemanal.py b/mypy/test/testsemanal.py index 4870fa8344ed..99c0078e9196 100644 --- a/mypy/test/testsemanal.py +++ b/mypy/test/testsemanal.py @@ -30,6 +30,7 @@ 'semanal-abstractclasses.test', 'semanal-namedtuple.test', 'semanal-typeddict.test', + 'semanal-classvar.test', 'semanal-python2.test'] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 2ddffd8dc3d1..d209853299dd 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -4,7 +4,7 @@ from typing import Callable, cast, List, Optional from mypy.types import ( - Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, + Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, ClassVarType, AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args, ) @@ -148,6 +148,12 @@ def visit_unbound_type(self, t: UnboundType) -> Type: items = self.anal_array(t.args) item = items[0] return TypeType(item, line=t.line) + elif fullname == 'typing.ClassVar': + if len(t.args) != 1: + self.fail('ClassVar[...] must have exactly one type argument', t) + return AnyType() + items = self.anal_array(t.args) + return ClassVarType(items[0]) elif fullname == 'mypy_extensions.NoReturn': return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: diff --git a/mypy/types.py b/mypy/types.py index 5e2d1feda81f..9b6d67542ac4 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1189,6 +1189,29 @@ def deserialize(cls, data: JsonDict) -> 'TypeType': return TypeType(Type.deserialize(data['item'])) +class ClassVarType(Type): + """The ClassVar[T] type. + + In all typechecking scenarios, behaves like T. + """ + item = None # type: Type + + def __init__(self, item: Type, *, line: int = -1, column: int = -1) -> None: + super().__init__(line, column) + self.item = item + + def accept(self, visitor: 'TypeVisitor[T]') -> T: + return visitor.visit_classvar_type(self) + + def serialize(self) -> JsonDict: + return {'.class': 'ClassVarType', 'item': self.item.serialize()} + + @classmethod + def deserialize(cls, data: JsonDict) -> 'TypeType': + assert data['.class'] == 'ClassVarType' + return ClassVarType(Type.deserialize(data['item'])) + + # # Visitor-related classes # @@ -1280,6 +1303,9 @@ def visit_ellipsis_type(self, t: EllipsisType) -> T: def visit_type_type(self, t: TypeType) -> T: pass + def visit_classvar_type(self, t: ClassVarType) -> T: + pass + class TypeTranslator(TypeVisitor[Type]): """Identity type transformation. @@ -1513,6 +1539,9 @@ def visit_ellipsis_type(self, t: EllipsisType) -> str: def visit_type_type(self, t: TypeType) -> str: return 'Type[{}]'.format(t.item.accept(self)) + def visit_classvar_type(self, t: TypeType) -> str: + return 'ClassVar[{}]'.format(t.item.accept(self)) + def list_str(self, a: List[Type]) -> str: """Convert items of an array to strings (pretty-print types) and join the results with commas. diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test new file mode 100644 index 000000000000..19ac1cfb01af --- /dev/null +++ b/test-data/unit/check-classvar.test @@ -0,0 +1,44 @@ +[case testAssignmentOnClass] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +A.x = 2 + +[case testAssignmentOnInstance] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +A().x = 2 +[out] +main:4: error: Illegal assignment to class variable + +[case testAssignmentOnSubclass] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B(A): + pass +B().x = 2 +[out] +main:6: error: Illegal assignment to class variable + +[case testReadingFromInstance] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +A().x + +[case testTypecheck] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +y = A.x # type: int + +[case testInfer] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +y = A.x +reveal_type(y) +[out] +main:5: error: Revealed type is 'builtins.int' diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 77a7b349e4cd..0377f19a1e5d 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -17,6 +17,7 @@ _promote = 0 NamedTuple = 0 Type = 0 no_type_check = 0 +ClassVar = 0 # Type aliases. List = 0 diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test new file mode 100644 index 000000000000..5ddda2e6be8d --- /dev/null +++ b/test-data/unit/semanal-classvar.test @@ -0,0 +1,40 @@ +[case testClassVarDef] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +[out] +MypyFile:1( + ImportFrom:1(typing, [ClassVar]) + ClassDef:2( + A + AssignmentStmt:3( + NameExpr(x [m]) + IntExpr(1) + ClassVar[builtins.int]))) + +[case testClassVarDefInModuleScope] +from typing import ClassVar +x = None # type: ClassVar[int] +[out] +main:2: error: Invalid ClassVar definition + +[case testClassVarDefInFuncScope] +from typing import ClassVar +def f() -> None: + x = None # type: ClassVar[int] +[out] +main:3: error: Invalid ClassVar definition + +[case testClassVarTooManyArguments] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int, str] +[out] +main:3: error: ClassVar[...] must have exactly one type argument + +[case testClassVarWithoutArguments] +from typing import ClassVar +class A: + x = 1 # type: ClassVar +[out] +main:3: error: ClassVar[...] must have exactly one type argument From 950b383fed6141477d8f5c297153e8a01d1b1df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Thu, 16 Feb 2017 00:02:54 +0100 Subject: [PATCH 02/23] Fix type annotations --- mypy/semanal.py | 2 +- mypy/types.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index cb8d6012f0be..043a54744ca0 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2160,7 +2160,7 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], return info - def check_classvar_definition(self, s: AssignmentStmt): + def check_classvar_definition(self, s: AssignmentStmt) -> None: if isinstance(s.type, ClassVarType) and not self.is_class_scope(): self.fail("Invalid ClassVar definition", s) diff --git a/mypy/types.py b/mypy/types.py index 9b6d67542ac4..e7b037424b3d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1207,7 +1207,7 @@ def serialize(self) -> JsonDict: return {'.class': 'ClassVarType', 'item': self.item.serialize()} @classmethod - def deserialize(cls, data: JsonDict) -> 'TypeType': + def deserialize(cls, data: JsonDict) -> 'ClassVarType': assert data['.class'] == 'ClassVarType' return ClassVarType(Type.deserialize(data['item'])) @@ -1539,7 +1539,7 @@ def visit_ellipsis_type(self, t: EllipsisType) -> str: def visit_type_type(self, t: TypeType) -> str: return 'Type[{}]'.format(t.item.accept(self)) - def visit_classvar_type(self, t: TypeType) -> str: + def visit_classvar_type(self, t: ClassVarType) -> str: return 'ClassVar[{}]'.format(t.item.accept(self)) def list_str(self, a: List[Type]) -> str: From cdfdbbc260e38141b90d07016c64b591a170e621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Thu, 16 Feb 2017 21:31:47 +0100 Subject: [PATCH 03/23] Remove ClassVarType, use Var.is_classvar flag --- mypy/checker.py | 5 +- mypy/checkexpr.py | 4 +- mypy/checkmember.py | 2 + mypy/messages.py | 4 ++ mypy/nodes.py | 4 +- mypy/semanal.py | 51 ++++++++++++-- mypy/subtypes.py | 6 +- mypy/typeanal.py | 6 +- mypy/types.py | 29 -------- test-data/unit/check-classvar.test | 100 ++++++++++++++++++++++++++- test-data/unit/semanal-classvar.test | 100 ++++++++++++++++++++++++++- 11 files changed, 258 insertions(+), 53 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7227d362e4f3..5beb52a39e56 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -25,7 +25,7 @@ from mypy import nodes from mypy.types import ( Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType, TypedDictType, - Instance, NoneTyp, ErrorType, strip_type, TypeType, ClassVarType, + Instance, NoneTyp, ErrorType, strip_type, TypeType, UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef, true_only, false_only, function_type, is_named_instance ) @@ -1575,9 +1575,6 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, Return the inferred rvalue_type and whether to infer anything about the attribute type """ - if isinstance(instance_type, Instance) and isinstance(attribute_type, ClassVarType): - self.msg.fail("Illegal assignment to class variable", context) - # Descriptors don't participate in class-attribute access if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance(instance_type, TypeType)): diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c0fd8c35bf23..f54703997b53 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -7,7 +7,7 @@ from mypy.types import ( Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef, TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType, - PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, ClassVarType, + PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, get_typ_args, set_typ_args, StarType) @@ -2218,8 +2218,6 @@ def bool_type(self) -> Instance: return self.named_type('builtins.bool') def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type: - if isinstance(known_type, ClassVarType): - known_type = known_type.item if expr.literal >= LITERAL_TYPE: restriction = self.chk.binder.get(expr) if restriction: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 7390bb7baf93..1cfca649656e 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -268,6 +268,8 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont if is_lvalue and var.is_property and not var.is_settable_property: # TODO allow setting attributes in subclass (although it is probably an error) msg.read_only_property(name, info, node) + if is_lvalue and var.is_classvar: + msg.cant_assign_to_classvar(node) if var.is_initialized_in_class and isinstance(t, FunctionLike) and not t.is_type_obj(): if is_lvalue: if var.is_property: diff --git a/mypy/messages.py b/mypy/messages.py index c8b65047d3af..7abdd8ba3000 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -61,6 +61,7 @@ CANNOT_ACCESS_INIT = 'Cannot access "__init__" directly' CANNOT_ASSIGN_TO_METHOD = 'Cannot assign to a method' CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type' +CANNOT_ASSIGN_TO_CLASSVAR = 'Illegal assignment to class variable' INCONSISTENT_ABSTRACT_OVERLOAD = \ 'Overloaded method has both abstract and non-abstract variants' READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE = \ @@ -814,6 +815,9 @@ def base_class_definitions_incompatible(self, name: str, base1: TypeInfo, def cant_assign_to_method(self, context: Context) -> None: self.fail(CANNOT_ASSIGN_TO_METHOD, context) + def cant_assign_to_classvar(self, context: Context) -> None: + self.fail(CANNOT_ASSIGN_TO_CLASSVAR, context) + def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None: self.fail('Property "{}" defined in "{}" is read-only'.format( diff --git a/mypy/nodes.py b/mypy/nodes.py index 5d39921e315d..fc89c5b57b2a 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -637,13 +637,15 @@ class Var(SymbolNode): is_classmethod = False is_property = False is_settable_property = False + is_classvar = False # Set to true when this variable refers to a module we were unable to # parse for some reason (eg a silenced module) is_suppressed_import = False FLAGS = [ 'is_self', 'is_ready', 'is_initialized_in_class', 'is_staticmethod', - 'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import' + 'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import', + 'is_classvar' ] def __init__(self, name: str, type: 'mypy.types.Type' = None) -> None: diff --git a/mypy/semanal.py b/mypy/semanal.py index 043a54744ca0..57f9529620c0 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -73,7 +73,7 @@ from mypy.errors import Errors, report_internal_error from mypy.types import ( NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType, - FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, ClassVarType, + FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType, ) from mypy.nodes import implicit_module_attrs @@ -1330,6 +1330,7 @@ def anal_type(self, t: Type, allow_tuple_literal: bool = False, def visit_assignment_stmt(self, s: AssignmentStmt) -> None: for lval in s.lvalues: self.analyze_lvalue(lval, explicit_type=s.type is not None) + self.check_classvar(s) s.rvalue.accept(self) if s.type: allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr)) @@ -1363,7 +1364,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.process_typevar_declaration(s) self.process_namedtuple_definition(s) self.process_typeddict_definition(s) - self.check_classvar_definition(s) if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and @@ -2160,9 +2160,50 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], return info - def check_classvar_definition(self, s: AssignmentStmt) -> None: - if isinstance(s.type, ClassVarType) and not self.is_class_scope(): - self.fail("Invalid ClassVar definition", s) + def check_classvar(self, s: AssignmentStmt) -> None: + lvalue = s.lvalues[0] + if len(s.lvalues) != 1 or not isinstance(lvalue, NameExpr): + return + is_classvar = self.check_classvar_definition(lvalue, s.type) + if self.is_class_scope() and isinstance(lvalue.node, Var): + # Assignments to class variables outside class scope are checked later + self.check_classvar_override(lvalue.node, is_classvar) + + def check_classvar_definition(self, lvalue: NameExpr, typ: Type) -> bool: + if not isinstance(typ, UnboundType): + return False + sym = self.lookup_qualified(typ.name, typ) + if not sym or not sym.node: + return False + fullname = sym.node.fullname() + if fullname != 'typing.ClassVar': + return False + if self.is_class_scope() or not isinstance(lvalue.node, Var): + node = cast(Var, lvalue.node) + node.is_classvar = True + return True + else: + self.fail('Invalid ClassVar definition', lvalue) + return False + + def check_classvar_override(self, node: Var, is_classvar: bool) -> None: + name = node.name() + for base in self.type.mro[1:]: + tnode = base.names.get(name) + if tnode is None: + continue + base_node = tnode.node + if isinstance(base_node, Var): + v = base_node + if (is_classvar and not v.is_classvar) or (not is_classvar and v.is_classvar): + self.fail_classvar_base_incompatibility(node, v) + return + + def fail_classvar_base_incompatibility(self, shadowing: Var, original: Var) -> None: + base_name = original.info.name() + self.fail('Invalid class attribute definition ' + '(previously declared on base class "%s")' % base_name, + shadowing) def visit_decorator(self, dec: Decorator) -> None: for d in dec.decorators: diff --git a/mypy/subtypes.py b/mypy/subtypes.py index e0e85ae0aa00..4af105898bf3 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -3,8 +3,7 @@ from mypy.types import ( Type, AnyType, UnboundType, TypeVisitor, ErrorType, FormalArgument, Void, NoneTyp, Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded, - ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance, - ClassVarType, + ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance ) import mypy.applytype import mypy.constraints @@ -53,9 +52,6 @@ def is_subtype(left: Type, right: Type, return any(is_subtype(left, item, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names) for item in right.items) - elif isinstance(right, ClassVarType): - return is_subtype(left, right.item, type_parameter_checker, - ignore_pos_arg_names=ignore_pos_arg_names) else: return left.accept(SubtypeVisitor(right, type_parameter_checker, ignore_pos_arg_names=ignore_pos_arg_names)) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index d209853299dd..6cad2bfbbd38 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -4,7 +4,7 @@ from typing import Callable, cast, List, Optional from mypy.types import ( - Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, ClassVarType, + Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args, ) @@ -149,11 +149,13 @@ def visit_unbound_type(self, t: UnboundType) -> Type: item = items[0] return TypeType(item, line=t.line) elif fullname == 'typing.ClassVar': + if len(t.args) == 0: + return AnyType(line=t.line) if len(t.args) != 1: self.fail('ClassVar[...] must have exactly one type argument', t) return AnyType() items = self.anal_array(t.args) - return ClassVarType(items[0]) + return items[0] elif fullname == 'mypy_extensions.NoReturn': return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: diff --git a/mypy/types.py b/mypy/types.py index e7b037424b3d..5e2d1feda81f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1189,29 +1189,6 @@ def deserialize(cls, data: JsonDict) -> 'TypeType': return TypeType(Type.deserialize(data['item'])) -class ClassVarType(Type): - """The ClassVar[T] type. - - In all typechecking scenarios, behaves like T. - """ - item = None # type: Type - - def __init__(self, item: Type, *, line: int = -1, column: int = -1) -> None: - super().__init__(line, column) - self.item = item - - def accept(self, visitor: 'TypeVisitor[T]') -> T: - return visitor.visit_classvar_type(self) - - def serialize(self) -> JsonDict: - return {'.class': 'ClassVarType', 'item': self.item.serialize()} - - @classmethod - def deserialize(cls, data: JsonDict) -> 'ClassVarType': - assert data['.class'] == 'ClassVarType' - return ClassVarType(Type.deserialize(data['item'])) - - # # Visitor-related classes # @@ -1303,9 +1280,6 @@ def visit_ellipsis_type(self, t: EllipsisType) -> T: def visit_type_type(self, t: TypeType) -> T: pass - def visit_classvar_type(self, t: ClassVarType) -> T: - pass - class TypeTranslator(TypeVisitor[Type]): """Identity type transformation. @@ -1539,9 +1513,6 @@ def visit_ellipsis_type(self, t: EllipsisType) -> str: def visit_type_type(self, t: TypeType) -> str: return 'Type[{}]'.format(t.item.accept(self)) - def visit_classvar_type(self, t: ClassVarType) -> str: - return 'ClassVar[{}]'.format(t.item.accept(self)) - def list_str(self, a: List[Type]) -> str: """Convert items of an array to strings (pretty-print types) and join the results with commas. diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index 19ac1cfb01af..a42f2ef2778d 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -12,7 +12,7 @@ A().x = 2 [out] main:4: error: Illegal assignment to class variable -[case testAssignmentOnSubclass] +[case testAssignmentOnSubclassInstance] from typing import ClassVar class A: x = 1 # type: ClassVar[int] @@ -22,18 +22,64 @@ B().x = 2 [out] main:6: error: Illegal assignment to class variable +[case testOverrideOnSelf] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] + def __init__(self) -> None: + self.x = 0 +[out] +main:5: error: Illegal assignment to class variable + +[case testOverrideOnSelfInSubclass] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] +class B(A): + def __init__(self) -> None: + self.x = 0 +[out] +main:6: error: Illegal assignment to class variable + [case testReadingFromInstance] from typing import ClassVar class A: x = 1 # type: ClassVar[int] A().x -[case testTypecheck] +[case testTypecheckSimple] from typing import ClassVar class A: x = 1 # type: ClassVar[int] y = A.x # type: int +[case testTypecheckWithUserType] +from typing import ClassVar +class A: + pass +class B: + x = A() # type: ClassVar[A] + +[case testTypeCheckOnAssignment] +from typing import ClassVar +class A: + pass +class B: + pass +class C: + x = None # type: ClassVar[A] +C.x = B() +[out] +main:8: error: Incompatible types in assignment (expression has type "B", variable has type "A") + +[case testRevealType] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] +reveal_type(A.x) +[out] +main:4: error: Revealed type is 'builtins.int' + [case testInfer] from typing import ClassVar class A: @@ -42,3 +88,53 @@ y = A.x reveal_type(y) [out] main:5: error: Revealed type is 'builtins.int' + +[case testAssignmentOnUnion] +from typing import ClassVar, Union +class A: + x = None # type: int +class B: + x = None # type: ClassVar[int] +c = A() # type: Union[A, B] +c.x = 1 +[out] +main:7: error: Illegal assignment to class variable + +[case testAssignmentOnInstanceFromType] +from typing import ClassVar, Type +class A: + x = None # type: ClassVar[int] +def f(a: Type[A]) -> None: + a().x = 0 +[out] +main:5: error: Illegal assignment to class variable + +[case testAssignmentOnInstanceFromSubclassType] +from typing import ClassVar, Type +class A: + x = None # type: ClassVar[int] +class B(A): + pass +def f(b: Type[B]) -> None: + b().x = 0 +[out] +main:7: error: Illegal assignment to class variable + +[case testAssignmentWithGeneric] +from typing import ClassVar, List +class A: + x = None # type: ClassVar[List[int]] +A.x = ['a'] +[builtins fixtures/list.pyi] +[out] +main:4: error: List item 0 has incompatible type "str" + +[case testAssignmentToCallableRet] +from typing import ClassVar, Type +class A: + x = None # type: ClassVar[int] +def f() -> A: + return A() +f().x = 0 +[out] +main:6: error: Illegal assignment to class variable diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 5ddda2e6be8d..f69f8d68a659 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -10,7 +10,7 @@ MypyFile:1( AssignmentStmt:3( NameExpr(x [m]) IntExpr(1) - ClassVar[builtins.int]))) + builtins.int))) [case testClassVarDefInModuleScope] from typing import ClassVar @@ -37,4 +37,100 @@ from typing import ClassVar class A: x = 1 # type: ClassVar [out] -main:3: error: ClassVar[...] must have exactly one type argument +MypyFile:1( + ImportFrom:1(typing, [ClassVar]) + ClassDef:2( + A + AssignmentStmt:3( + NameExpr(x [m]) + IntExpr(1) + Any))) + +[case testOverrideWithNormalAttribute] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B(A): + x = 2 # type: int +[out] +main:5: error: Invalid class attribute definition (previously declared on base class "A") + +[case testOverrideWithAttributeWithClassVar] +from typing import ClassVar +class A: + x = 1 # type: int +class B(A): + x = 2 # type: ClassVar[int] +[out] +main:5: error: Invalid class attribute definition (previously declared on base class "A") + +[case testOverrideClassVarWithInstanceVariable] +from typing import ClassVar +class A: + x = 1 # type: int +class B(A): + x = 2 # type: ClassVar[int] +[out] +main:5: error: Invalid class attribute definition (previously declared on base class "A") + +[case testOverrideClassVarManyBases] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B: + x = 2 # type: int +class C(A, B): + x = 3 # type: ClassVar[int] +[out] +main:7: error: Invalid class attribute definition (previously declared on base class "B") + +[case testOverrideClassVarWithClassVar] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B(A): + x = 2 # type: ClassVar[int] +[out] +MypyFile:1( + ImportFrom:1(typing, [ClassVar]) + ClassDef:2( + A + AssignmentStmt:3( + NameExpr(x [m]) + IntExpr(1) + builtins.int)) + ClassDef:4( + B + BaseType( + __main__.A) + AssignmentStmt:5( + NameExpr(x [m]) + IntExpr(2) + builtins.int))) + +[case testOverrideOnABCSubclass] +from abc import ABCMeta +from typing import ClassVar +class A(metaclass=ABCMeta): + x = None # type: ClassVar[int] +class B(A): + x = 0 # type: ClassVar[int] +[out] +MypyFile:1( + ImportFrom:1(abc, [ABCMeta]) + ImportFrom:2(typing, [ClassVar]) + ClassDef:3( + A + Metaclass(ABCMeta) + AssignmentStmt:4( + NameExpr(x [m]) + NameExpr(None [builtins.None]) + builtins.int)) + ClassDef:5( + B + BaseType( + __main__.A) + AssignmentStmt:6( + NameExpr(x [m]) + IntExpr(0) + builtins.int))) From 01a7a69e98d61cf251194bc59364e43be2a39767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Thu, 16 Feb 2017 21:55:23 +0100 Subject: [PATCH 04/23] Add test case against TypeVar --- test-data/unit/semanal-classvar.test | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index f69f8d68a659..05d27b05fe30 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -134,3 +134,11 @@ MypyFile:1( NameExpr(x [m]) IntExpr(0) builtins.int))) + +[case testClassVarWithTypeVar] +from typing import ClassVar, TypeVar +T = TypeVar('T') +class A: + x = None # type: ClassVar[T] +[out] +main:4: error: Invalid type "__main__.T" From d52b4e3f5587c506dd91b0576f5b659019d59370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 14:43:33 +0100 Subject: [PATCH 05/23] Fix if statement in check_classvar_definition --- mypy/semanal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 57f9529620c0..c4f57a99f5f6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2178,8 +2178,8 @@ def check_classvar_definition(self, lvalue: NameExpr, typ: Type) -> bool: fullname = sym.node.fullname() if fullname != 'typing.ClassVar': return False - if self.is_class_scope() or not isinstance(lvalue.node, Var): - node = cast(Var, lvalue.node) + node = lvalue.node + if self.is_class_scope() and isinstance(node, Var): node.is_classvar = True return True else: From 15e1e85597b445641ec8ce172a3122dee2230729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 14:45:26 +0100 Subject: [PATCH 06/23] Change error message about ClassVar arg count --- mypy/typeanal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 6cad2bfbbd38..fef099227090 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -152,7 +152,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: if len(t.args) == 0: return AnyType(line=t.line) if len(t.args) != 1: - self.fail('ClassVar[...] must have exactly one type argument', t) + self.fail('ClassVar[...] must have at most one type argument', t) return AnyType() items = self.anal_array(t.args) return items[0] From 781245f586b05004e869718c364ab437d54ca95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 15:40:35 +0100 Subject: [PATCH 07/23] Add more tests --- test-data/unit/check-classvar.test | 86 +++++++++++++++++++++++++++- test-data/unit/semanal-classvar.test | 14 ++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index a42f2ef2778d..aa219a04a2a4 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -46,6 +46,9 @@ from typing import ClassVar class A: x = 1 # type: ClassVar[int] A().x +reveal_type(A().x) +[out] +main:5: error: Revealed type is 'builtins.int' [case testTypecheckSimple] from typing import ClassVar @@ -72,6 +75,16 @@ C.x = B() [out] main:8: error: Incompatible types in assignment (expression has type "B", variable has type "A") +[case testTypeCheckWithOverridden] +from typing import ClassVar +class A: + pass +class B(A): + pass +class C: + x = A() # type: ClassVar[A] +C.x = B() + [case testRevealType] from typing import ClassVar class A: @@ -120,17 +133,77 @@ def f(b: Type[B]) -> None: [out] main:7: error: Illegal assignment to class variable -[case testAssignmentWithGeneric] +[case testClassVarWithList] from typing import ClassVar, List class A: x = None # type: ClassVar[List[int]] A.x = ['a'] +A().x.append(1) +A().x.append('') [builtins fixtures/list.pyi] [out] main:4: error: List item 0 has incompatible type "str" +main:6: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int" + +[case testClassVarWithUnion] +from typing import ClassVar, Union +class A: + x = None # type: ClassVar[Union[int, str]] +class B: + pass +A.x = 0 +A.x = 'a' +A.x = B() +reveal_type(A().x) +[out] +main:8: error: Incompatible types in assignment (expression has type "B", variable has type "Union[int, str]") +main:9: error: Revealed type is 'Union[builtins.int, builtins.str]' + +[case testOverrideWithNarrowedUnion] +from typing import ClassVar, Union +class A: pass +class B: pass +class C: pass +class D: + x = None # type: ClassVar[Union[A, B, C]] +class E(D): + x = None # type: ClassVar[Union[A, B]] + +[case testOverrideWithExtendedUnion] +from typing import ClassVar, Union +class A: pass +class B: pass +class C: pass +class D: + x = None # type: ClassVar[Union[A, B]] +class E(D): + x = None # type: ClassVar[Union[A, B, C]] +[out] +main:8: error: Incompatible types in assignment (expression has type "Union[A, B, C]", base class "D" defined the type as "Union[A, B]") + +[case testClassVarWithGeneric] +from typing import ClassVar, Generic, TypeVar +T = TypeVar('T') +class A(Generic[T]): + x = None # type: ClassVar[T] +reveal_type(A[int]().x) +[out] +main:5: error: Revealed type is 'builtins.int*' + +[case testClassVarWithGenericList] +from typing import ClassVar, Generic, List, TypeVar +T = TypeVar('T') +class A(Generic[T]): + x = None # type: ClassVar[List[T]] +reveal_type(A[int]().x) +A[int].x.append('a') +[builtins fixtures/list.pyi] +[out] +main:5: error: Revealed type is 'builtins.list[builtins.int*]' +main:6: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "T" [case testAssignmentToCallableRet] -from typing import ClassVar, Type +from typing import ClassVar class A: x = None # type: ClassVar[int] def f() -> A: @@ -138,3 +211,12 @@ def f() -> A: f().x = 0 [out] main:6: error: Illegal assignment to class variable + +[case testOverrideWithIncomatibleType] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] +class B(A): + x = None # type: ClassVar[str] +[out] +main:5: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 05d27b05fe30..3cd240ca1bba 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -25,12 +25,20 @@ def f() -> None: [out] main:3: error: Invalid ClassVar definition +[case testClassVarDefInMethod] +from typing import ClassVar +class A: + def f(self) -> None: + x = None # type: ClassVar +[out] +main:4: error: Invalid ClassVar definition + [case testClassVarTooManyArguments] from typing import ClassVar class A: x = 1 # type: ClassVar[int, str] [out] -main:3: error: ClassVar[...] must have exactly one type argument +main:3: error: ClassVar[...] must have at most one type argument [case testClassVarWithoutArguments] from typing import ClassVar @@ -67,9 +75,9 @@ main:5: error: Invalid class attribute definition (previously declared on base c [case testOverrideClassVarWithInstanceVariable] from typing import ClassVar class A: - x = 1 # type: int + x = 1 # type: ClassVar[int] class B(A): - x = 2 # type: ClassVar[int] + x = 2 # type: int [out] main:5: error: Invalid class attribute definition (previously declared on base class "A") From cd61a5da7b912c603b783a90b4395e134ac90e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 17:37:04 +0100 Subject: [PATCH 08/23] Remove duplicate test case --- test-data/unit/semanal-classvar.test | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 3cd240ca1bba..67f55e8cc5bb 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -72,15 +72,6 @@ class B(A): [out] main:5: error: Invalid class attribute definition (previously declared on base class "A") -[case testOverrideClassVarWithInstanceVariable] -from typing import ClassVar -class A: - x = 1 # type: ClassVar[int] -class B(A): - x = 2 # type: int -[out] -main:5: error: Invalid class attribute definition (previously declared on base class "A") - [case testOverrideClassVarManyBases] from typing import ClassVar class A: From 9c29911cb3ce8299f11dca6ed4018b1eec1dce54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 18:54:13 +0100 Subject: [PATCH 09/23] Move ClassVar override checks to TypeChecker --- mypy/checker.py | 20 +++++++ mypy/semanal.py | 24 +-------- test-data/unit/check-classvar.test | 44 +++++++++++++++ test-data/unit/semanal-classvar.test | 80 ---------------------------- 4 files changed, 65 insertions(+), 103 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5beb52a39e56..1aeb79a9db3e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1149,6 +1149,15 @@ def check_compatibility_all_supers(self, lvalue: NameExpr, lvalue_type: Type, lvalue.kind == MDEF and len(lvalue_node.info.bases) > 0): + for base in lvalue_node.info.mro[1:]: + tnode = base.names.get(lvalue_node.name()) + if tnode is not None: + if not self.check_compatibility_classvar_super(lvalue_node, + base, + tnode.node): + # Show only one error per variable + break + for base in lvalue_node.info.mro[1:]: # Only check __slots__ against the 'object' # If a base class defines a Tuple of 3 elements, a child of @@ -1257,6 +1266,17 @@ def lvalue_type_from_base(self, expr_node: Var, return None, None + def check_compatibility_classvar_super(self, node: Var, + base: TypeInfo, base_node: Node) -> bool: + if (isinstance(base_node, Var) and + ((node.is_classvar and not base_node.is_classvar) or + (not node.is_classvar and base_node.is_classvar))): + self.fail('Invalid class attribute definition ' + '(previously declared on base class "%s")' % base.name(), + node) + return False + return True + def check_assignment_to_multiple_lvalues(self, lvalues: List[Lvalue], rvalue: Expression, context: Context, infer_lvalue_type: bool = True) -> None: diff --git a/mypy/semanal.py b/mypy/semanal.py index c4f57a99f5f6..29bdc374fc8a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2164,10 +2164,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: lvalue = s.lvalues[0] if len(s.lvalues) != 1 or not isinstance(lvalue, NameExpr): return - is_classvar = self.check_classvar_definition(lvalue, s.type) - if self.is_class_scope() and isinstance(lvalue.node, Var): - # Assignments to class variables outside class scope are checked later - self.check_classvar_override(lvalue.node, is_classvar) + self.check_classvar_definition(lvalue, s.type) def check_classvar_definition(self, lvalue: NameExpr, typ: Type) -> bool: if not isinstance(typ, UnboundType): @@ -2186,25 +2183,6 @@ def check_classvar_definition(self, lvalue: NameExpr, typ: Type) -> bool: self.fail('Invalid ClassVar definition', lvalue) return False - def check_classvar_override(self, node: Var, is_classvar: bool) -> None: - name = node.name() - for base in self.type.mro[1:]: - tnode = base.names.get(name) - if tnode is None: - continue - base_node = tnode.node - if isinstance(base_node, Var): - v = base_node - if (is_classvar and not v.is_classvar) or (not is_classvar and v.is_classvar): - self.fail_classvar_base_incompatibility(node, v) - return - - def fail_classvar_base_incompatibility(self, shadowing: Var, original: Var) -> None: - base_name = original.info.name() - self.fail('Invalid class attribute definition ' - '(previously declared on base class "%s")' % base_name, - shadowing) - def visit_decorator(self, dec: Decorator) -> None: for d in dec.decorators: d.accept(self) diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index aa219a04a2a4..790586a7e68e 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -220,3 +220,47 @@ class B(A): x = None # type: ClassVar[str] [out] main:5: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") + +[case testOverrideWithNormalAttribute] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B(A): + x = 2 # type: int +[out] +main:5: error: Invalid class attribute definition (previously declared on base class "A") + +[case testOverrideWithAttributeWithClassVar] +from typing import ClassVar +class A: + x = 1 # type: int +class B(A): + x = 2 # type: ClassVar[int] +[out] +main:5: error: Invalid class attribute definition (previously declared on base class "A") + +[case testOverrideClassVarManyBases] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B: + x = 2 # type: int +class C(A, B): + x = 3 # type: ClassVar[int] +[out] +main:7: error: Invalid class attribute definition (previously declared on base class "B") + +[case testOverrideClassVarWithClassVar] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] +class B(A): + x = 2 # type: ClassVar[int] + +[case testOverrideOnABCSubclass] +from abc import ABCMeta +from typing import ClassVar +class A(metaclass=ABCMeta): + x = None # type: ClassVar[int] +class B(A): + x = 0 # type: ClassVar[int] diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 67f55e8cc5bb..b43def135b02 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -54,86 +54,6 @@ MypyFile:1( IntExpr(1) Any))) -[case testOverrideWithNormalAttribute] -from typing import ClassVar -class A: - x = 1 # type: ClassVar[int] -class B(A): - x = 2 # type: int -[out] -main:5: error: Invalid class attribute definition (previously declared on base class "A") - -[case testOverrideWithAttributeWithClassVar] -from typing import ClassVar -class A: - x = 1 # type: int -class B(A): - x = 2 # type: ClassVar[int] -[out] -main:5: error: Invalid class attribute definition (previously declared on base class "A") - -[case testOverrideClassVarManyBases] -from typing import ClassVar -class A: - x = 1 # type: ClassVar[int] -class B: - x = 2 # type: int -class C(A, B): - x = 3 # type: ClassVar[int] -[out] -main:7: error: Invalid class attribute definition (previously declared on base class "B") - -[case testOverrideClassVarWithClassVar] -from typing import ClassVar -class A: - x = 1 # type: ClassVar[int] -class B(A): - x = 2 # type: ClassVar[int] -[out] -MypyFile:1( - ImportFrom:1(typing, [ClassVar]) - ClassDef:2( - A - AssignmentStmt:3( - NameExpr(x [m]) - IntExpr(1) - builtins.int)) - ClassDef:4( - B - BaseType( - __main__.A) - AssignmentStmt:5( - NameExpr(x [m]) - IntExpr(2) - builtins.int))) - -[case testOverrideOnABCSubclass] -from abc import ABCMeta -from typing import ClassVar -class A(metaclass=ABCMeta): - x = None # type: ClassVar[int] -class B(A): - x = 0 # type: ClassVar[int] -[out] -MypyFile:1( - ImportFrom:1(abc, [ABCMeta]) - ImportFrom:2(typing, [ClassVar]) - ClassDef:3( - A - Metaclass(ABCMeta) - AssignmentStmt:4( - NameExpr(x [m]) - NameExpr(None [builtins.None]) - builtins.int)) - ClassDef:5( - B - BaseType( - __main__.A) - AssignmentStmt:6( - NameExpr(x [m]) - IntExpr(0) - builtins.int))) - [case testClassVarWithTypeVar] from typing import ClassVar, TypeVar T = TypeVar('T') From b618718cf20988b3943010980387475795dc31bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 20:45:46 +0100 Subject: [PATCH 10/23] Prohibit ClassVar inside other types or in signatures --- mypy/semanal.py | 16 +---- mypy/typeanal.py | 33 ++++++++--- test-data/unit/semanal-classvar.test | 89 ++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 23 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 29bdc374fc8a..d08f0ccd75d3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1306,23 +1306,11 @@ def visit_block_maybe(self, b: Block) -> None: def anal_type(self, t: Type, allow_tuple_literal: bool = False, aliasing: bool = False) -> Type: if t: - if allow_tuple_literal: - # Types such as (t1, t2, ...) only allowed in assignment statements. They'll - # generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead. - if isinstance(t, TupleType): - # Unlike TypeAnalyser, also allow implicit tuple types (without Tuple[...]). - star_count = sum(1 for item in t.items if isinstance(item, StarType)) - if star_count > 1: - self.fail('At most one star type allowed in a tuple', t) - return TupleType([AnyType() for _ in t.items], - self.builtin_type('builtins.tuple'), t.line) - items = [self.anal_type(item, True) - for item in t.items] - return TupleType(items, self.builtin_type('builtins.tuple'), t.line) a = TypeAnalyser(self.lookup_qualified, self.lookup_fully_qualified, self.fail, - aliasing=aliasing) + aliasing=aliasing, + allow_tuple_literal=allow_tuple_literal) return t.accept(a) else: return None diff --git a/mypy/typeanal.py b/mypy/typeanal.py index fef099227090..78851ca7cd04 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -81,11 +81,15 @@ def __init__(self, lookup_func: Callable[[str, Context], SymbolTableNode], lookup_fqn_func: Callable[[str], SymbolTableNode], fail_func: Callable[[str, Context], None], *, - aliasing: bool = False) -> None: + aliasing: bool = False, + allow_tuple_literal: bool = False) -> None: self.lookup = lookup_func self.lookup_fqn_func = lookup_fqn_func self.fail = fail_func self.aliasing = aliasing + self.allow_tuple_literal = allow_tuple_literal + # Positive if we are analyzing arguments of another (outer) type + self.nesting_level = 0 def visit_unbound_type(self, t: UnboundType) -> Type: if t.optional: @@ -120,7 +124,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return self.builtin_type('builtins.tuple') if len(t.args) == 2 and isinstance(t.args[1], EllipsisType): # Tuple[T, ...] (uniform, variable-length tuple) - instance = self.builtin_type('builtins.tuple', [t.args[0].accept(self)]) + instance = self.builtin_type('builtins.tuple', [self.anal_nested(t.args[0])]) instance.line = t.line return instance return self.tuple_type(self.anal_array(t.args)) @@ -149,6 +153,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: item = items[0] return TypeType(item, line=t.line) elif fullname == 'typing.ClassVar': + if self.nesting_level > 0: + self.fail('Invalid ClassVar definition', t) if len(t.args) == 0: return AnyType(line=t.line) if len(t.args) != 1: @@ -299,12 +305,14 @@ def visit_type_var(self, t: TypeVarType) -> Type: def visit_callable_type(self, t: CallableType) -> Type: return t.copy_modified(arg_types=self.anal_array(t.arg_types), - ret_type=t.ret_type.accept(self), + ret_type=self.anal_nested(t.ret_type), fallback=t.fallback or self.builtin_type('builtins.function'), variables=self.anal_var_defs(t.variables)) def visit_tuple_type(self, t: TupleType) -> Type: - if t.implicit: + # Types such as (t1, t2, ...) only allowed in assignment statements. They'll + # generate errors elsewhere, and Tuple[t1, t2, ...] must be used instead. + if t.implicit and not self.allow_tuple_literal: self.fail('Invalid tuple literal type', t) return AnyType() star_count = sum(1 for item in t.items if isinstance(item, StarType)) @@ -316,13 +324,13 @@ def visit_tuple_type(self, t: TupleType) -> Type: def visit_typeddict_type(self, t: TypedDictType) -> Type: items = OrderedDict([ - (item_name, item_type.accept(self)) + (item_name, self.anal_nested(item_type)) for (item_name, item_type) in t.items.items() ]) return TypedDictType(items, t.fallback) def visit_star_type(self, t: StarType) -> Type: - return StarType(t.type.accept(self), t.line) + return StarType(self.anal_nested(t.type), t.line) def visit_union_type(self, t: UnionType) -> Type: return UnionType(self.anal_array(t.items), t.line) @@ -335,7 +343,7 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type: return AnyType() def visit_type_type(self, t: TypeType) -> Type: - return TypeType(t.item.accept(self), line=t.line) + return TypeType(self.anal_nested(t.item), line=t.line) def analyze_callable_type(self, t: UnboundType) -> Type: fallback = self.builtin_type('builtins.function') @@ -348,7 +356,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type: fallback=fallback, is_ellipsis_args=True) elif len(t.args) == 2: - ret_type = t.args[1].accept(self) + ret_type = self.anal_nested(t.args[1]) if isinstance(t.args[0], TypeList): # Callable[[ARG, ...], RET] (ordinary callable type) args = t.args[0].items @@ -375,9 +383,16 @@ def analyze_callable_type(self, t: UnboundType) -> Type: def anal_array(self, a: List[Type]) -> List[Type]: res = [] # type: List[Type] for t in a: - res.append(t.accept(self)) + res.append(self.anal_nested(t)) return res + def anal_nested(self, t: Type) -> Type: + self.nesting_level += 1 + try: + return t.accept(self) + finally: + self.nesting_level -= 1 + def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]: a = [] # type: List[TypeVarDef] for vd in var_defs: diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index b43def135b02..09c8e17a7e0c 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -61,3 +61,92 @@ class A: x = None # type: ClassVar[T] [out] main:4: error: Invalid type "__main__.T" + +[case testClassVarInFunctionArgs] +from typing import ClassVar +def f(x: str, y: ClassVar) -> None: pass +[out] +main:2: error: Invalid ClassVar definition + +[case testClassVarFunctionRetType] +from typing import ClassVar +def f() -> ClassVar: pass +[out] +main:2: error: Invalid ClassVar definition + +[case testClassVarInCallableArgs] +from typing import Callable, ClassVar +f = None # type: Callable[[int, ClassVar], Any] +[out] +main:2: error: Invalid ClassVar definition + +[case testClassVarInCallableRet] +from typing import Callable, ClassVar +f = None # type: Callable[..., ClassVar] +[out] +main:2: error: Invalid ClassVar definition + +[case testClassVarInUnion] +from typing import ClassVar, Union +x = None # type: Union[ClassVar, str] +[out] +main:2: error: Invalid ClassVar definition + +[case testClassVarInUnionAsAttribute] +from typing import ClassVar, Union +class A: + x = None # type: Union[ClassVar, str] +[out] +main:3: error: Invalid ClassVar definition + +[case testListWithClassVars] +from typing import ClassVar, List +x = [] # type: List[ClassVar] +[builtins fixtures/list.pyi] +[out] +main:2: error: Invalid ClassVar definition + +[case testTupleClassVar] +from typing import ClassVar, Tuple +x = None # type: Tuple[ClassVar, int] +[out] +main:2: error: Invalid ClassVar definition + +[case testMultipleLvalues] +from typing import ClassVar +class A: + x, y = None, None # type: ClassVar, ClassVar +[out] +main:3: error: Invalid ClassVar definition + +[case testMultipleLvaluesWithList] +from typing import ClassVar, List +class A: + [x, y] = None, None # type: List[ClassVar] +[builtins fixtures/list.pyi] +[out] +main:3: error: Invalid ClassVar definition + +[case testDeeplyNested] +from typing import Callable, ClassVar, Union +class A: pass +class B: + x = None # type: Union[str, Callable[[A, ClassVar], int]] +[out] +main:4: error: Invalid ClassVar definition + +[case testClassVarInClassVar] +from typing import ClassVar +class A: + x = None # type: ClassVar[ClassVar[int]] +[out] +main:3: error: Invalid ClassVar definition + +[case testInsideGeneric] +from typing import ClassVar, Generic, TypeVar +T = TypeVar('T') +class A(Generic[T]): pass +class B: + x = None # type: A[ClassVar] +[out] +main:5: error: Invalid ClassVar definition From cb6786fb6dea99ca9cf7821f7af9e57a1951d89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 22:18:11 +0100 Subject: [PATCH 11/23] Prohibit defining ClassVars on self --- mypy/semanal.py | 26 +++++++++++++------------- test-data/unit/check-classvar.test | 9 +++++++++ test-data/unit/semanal-classvar.test | 8 ++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d08f0ccd75d3..3473d6a0746c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2150,26 +2150,26 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], def check_classvar(self, s: AssignmentStmt) -> None: lvalue = s.lvalues[0] - if len(s.lvalues) != 1 or not isinstance(lvalue, NameExpr): + if len(s.lvalues) != 1 or not isinstance(lvalue, (NameExpr, MemberExpr)): return - self.check_classvar_definition(lvalue, s.type) + if not self.check_classvar_definition(s.type): + return + if self.is_class_scope() and isinstance(lvalue, NameExpr): + node = lvalue.node + if isinstance(node, Var): + node.is_classvar = True + elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue): + # In case of member access, report error only when assigning to self + # Other kinds of member assignments should be already reported + self.fail('Invalid ClassVar definition', lvalue) - def check_classvar_definition(self, lvalue: NameExpr, typ: Type) -> bool: + def check_classvar_definition(self, typ: Type) -> bool: if not isinstance(typ, UnboundType): return False sym = self.lookup_qualified(typ.name, typ) if not sym or not sym.node: return False - fullname = sym.node.fullname() - if fullname != 'typing.ClassVar': - return False - node = lvalue.node - if self.is_class_scope() and isinstance(node, Var): - node.is_classvar = True - return True - else: - self.fail('Invalid ClassVar definition', lvalue) - return False + return sym.node.fullname() == 'typing.ClassVar' def visit_decorator(self, dec: Decorator) -> None: for d in dec.decorators: diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index 790586a7e68e..82d7749e8ee2 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -50,6 +50,15 @@ reveal_type(A().x) [out] main:5: error: Revealed type is 'builtins.int' +[case testReadingFromSelf] +from typing import ClassVar +class A: + x = 1 # type: ClassVar[int] + def __init__(self) -> None: + reveal_type(self.x) +[out] +main:5: error: Revealed type is 'builtins.int' + [case testTypecheckSimple] from typing import ClassVar class A: diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 09c8e17a7e0c..3073eb24ef26 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -150,3 +150,11 @@ class B: x = None # type: A[ClassVar] [out] main:5: error: Invalid ClassVar definition + +[case testDefineOnSelf] +from typing import ClassVar +class A: + def __init__(self) -> None: + self.x = None # type: ClassVar +[out] +main:4: error: Invalid ClassVar definition From a427d61947a3290c9b7a9b291cb26d2da9c9ed8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Fri, 17 Feb 2017 22:36:58 +0100 Subject: [PATCH 12/23] Add more tests --- test-data/unit/check-classvar.test | 12 ++++++++++++ test-data/unit/check-incremental.test | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index 82d7749e8ee2..e2436495f70d 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -273,3 +273,15 @@ class A(metaclass=ABCMeta): x = None # type: ClassVar[int] class B(A): x = 0 # type: ClassVar[int] + +[case testAcrossModules] +import m +reveal_type(m.A().x) +m.A().x = 0 +[file m.py] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] +[out] +main:2: error: Revealed type is 'builtins.int' +main:3: error: Illegal assignment to class variable diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index b4c5de57fb12..8b52776e5037 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -1798,3 +1798,26 @@ warn_no_return = False [[mypy-a] warn_no_return = True [rechecked] + +[case testIncrementalClassVar] +from typing import ClassVar +class A: + x = None # type: ClassVar +A().x = 0 +[out1] +main:4: error: Illegal assignment to class variable +[out2] +main:4: error: Illegal assignment to class variable + +[case testIncrementalClassVarGone] +import m +m.A().x = 0 +[file m.py] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] +[file m.py.next] +class A: + x = None # type: int +[out1] +main:2: error: Illegal assignment to class variable From 35e2c68b814c9b91eb394ed4aebbe30965025948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 13:57:57 +0100 Subject: [PATCH 13/23] Fix analysing implicit tuple types --- mypy/typeanal.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 78851ca7cd04..adfc4601d190 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -318,7 +318,12 @@ def visit_tuple_type(self, t: TupleType) -> Type: star_count = sum(1 for item in t.items if isinstance(item, StarType)) if star_count > 1: self.fail('At most one star type allowed in a tuple', t) - return AnyType() + if t.implicit: + return TupleType([AnyType() for _ in t.items], + self.builtin_type('builtins.tuple'), + t.line) + else: + return AnyType() fallback = t.fallback if t.fallback else self.builtin_type('builtins.tuple', [AnyType()]) return TupleType(self.anal_array(t.items), fallback, t.line) From e12115807e89961f57320ce40ee738cfd7dbeebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 14:07:12 +0100 Subject: [PATCH 14/23] Add more meaningful error message on attribute override --- mypy/checker.py | 17 +++++++++++------ test-data/unit/check-classvar.test | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1aeb79a9db3e..12ebf4673096 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1268,12 +1268,17 @@ def lvalue_type_from_base(self, expr_node: Var, def check_compatibility_classvar_super(self, node: Var, base: TypeInfo, base_node: Node) -> bool: - if (isinstance(base_node, Var) and - ((node.is_classvar and not base_node.is_classvar) or - (not node.is_classvar and base_node.is_classvar))): - self.fail('Invalid class attribute definition ' - '(previously declared on base class "%s")' % base.name(), - node) + if not isinstance(base_node, Var): + return True + if node.is_classvar and not base_node.is_classvar: + self.fail('Cannot override instance variable ' + '(previously declared on base class "%s") ' + 'with class variable' % base.name(), node) + return False + elif not node.is_classvar and base_node.is_classvar: + self.fail('Cannot override class variable ' + '(previously declared on base class "%s") ' + 'with instance variable' % base.name(), node) return False return True diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index e2436495f70d..c0206012f4e6 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -237,7 +237,7 @@ class A: class B(A): x = 2 # type: int [out] -main:5: error: Invalid class attribute definition (previously declared on base class "A") +main:5: error: Cannot override class variable (previously declared on base class "A") with instance variable [case testOverrideWithAttributeWithClassVar] from typing import ClassVar @@ -246,7 +246,7 @@ class A: class B(A): x = 2 # type: ClassVar[int] [out] -main:5: error: Invalid class attribute definition (previously declared on base class "A") +main:5: error: Cannot override instance variable (previously declared on base class "A") with class variable [case testOverrideClassVarManyBases] from typing import ClassVar @@ -257,7 +257,7 @@ class B: class C(A, B): x = 3 # type: ClassVar[int] [out] -main:7: error: Invalid class attribute definition (previously declared on base class "B") +main:7: error: Cannot override instance variable (previously declared on base class "B") with class variable [case testOverrideClassVarWithClassVar] from typing import ClassVar From 5e2aafc26800a685e213280c1417664e0f000d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 14:28:39 +0100 Subject: [PATCH 15/23] Add better error message on assignments --- mypy/checkmember.py | 2 +- mypy/messages.py | 5 ++--- test-data/unit/check-classvar.test | 18 +++++++++--------- test-data/unit/check-incremental.test | 6 +++--- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 1cfca649656e..b4448e4a5ec4 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -269,7 +269,7 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont # TODO allow setting attributes in subclass (although it is probably an error) msg.read_only_property(name, info, node) if is_lvalue and var.is_classvar: - msg.cant_assign_to_classvar(node) + msg.cant_assign_to_classvar(name, node) if var.is_initialized_in_class and isinstance(t, FunctionLike) and not t.is_type_obj(): if is_lvalue: if var.is_property: diff --git a/mypy/messages.py b/mypy/messages.py index 7abdd8ba3000..f918f0605c3e 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -61,7 +61,6 @@ CANNOT_ACCESS_INIT = 'Cannot access "__init__" directly' CANNOT_ASSIGN_TO_METHOD = 'Cannot assign to a method' CANNOT_ASSIGN_TO_TYPE = 'Cannot assign to a type' -CANNOT_ASSIGN_TO_CLASSVAR = 'Illegal assignment to class variable' INCONSISTENT_ABSTRACT_OVERLOAD = \ 'Overloaded method has both abstract and non-abstract variants' READ_ONLY_PROPERTY_OVERRIDES_READ_WRITE = \ @@ -815,8 +814,8 @@ def base_class_definitions_incompatible(self, name: str, base1: TypeInfo, def cant_assign_to_method(self, context: Context) -> None: self.fail(CANNOT_ASSIGN_TO_METHOD, context) - def cant_assign_to_classvar(self, context: Context) -> None: - self.fail(CANNOT_ASSIGN_TO_CLASSVAR, context) + def cant_assign_to_classvar(self, name: str, context: Context) -> None: + self.fail('Cannot assign to class variable "%s" via instance' % name, context) def read_only_property(self, name: str, type: TypeInfo, context: Context) -> None: diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index c0206012f4e6..2c8aa09297d8 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -10,7 +10,7 @@ class A: x = 1 # type: ClassVar[int] A().x = 2 [out] -main:4: error: Illegal assignment to class variable +main:4: error: Cannot assign to class variable "x" via instance [case testAssignmentOnSubclassInstance] from typing import ClassVar @@ -20,7 +20,7 @@ class B(A): pass B().x = 2 [out] -main:6: error: Illegal assignment to class variable +main:6: error: Cannot assign to class variable "x" via instance [case testOverrideOnSelf] from typing import ClassVar @@ -29,7 +29,7 @@ class A: def __init__(self) -> None: self.x = 0 [out] -main:5: error: Illegal assignment to class variable +main:5: error: Cannot assign to class variable "x" via instance [case testOverrideOnSelfInSubclass] from typing import ClassVar @@ -39,7 +39,7 @@ class B(A): def __init__(self) -> None: self.x = 0 [out] -main:6: error: Illegal assignment to class variable +main:6: error: Cannot assign to class variable "x" via instance [case testReadingFromInstance] from typing import ClassVar @@ -120,7 +120,7 @@ class B: c = A() # type: Union[A, B] c.x = 1 [out] -main:7: error: Illegal assignment to class variable +main:7: error: Cannot assign to class variable "x" via instance [case testAssignmentOnInstanceFromType] from typing import ClassVar, Type @@ -129,7 +129,7 @@ class A: def f(a: Type[A]) -> None: a().x = 0 [out] -main:5: error: Illegal assignment to class variable +main:5: error: Cannot assign to class variable "x" via instance [case testAssignmentOnInstanceFromSubclassType] from typing import ClassVar, Type @@ -140,7 +140,7 @@ class B(A): def f(b: Type[B]) -> None: b().x = 0 [out] -main:7: error: Illegal assignment to class variable +main:7: error: Cannot assign to class variable "x" via instance [case testClassVarWithList] from typing import ClassVar, List @@ -219,7 +219,7 @@ def f() -> A: return A() f().x = 0 [out] -main:6: error: Illegal assignment to class variable +main:6: error: Cannot assign to class variable "x" via instance [case testOverrideWithIncomatibleType] from typing import ClassVar @@ -284,4 +284,4 @@ class A: x = None # type: ClassVar[int] [out] main:2: error: Revealed type is 'builtins.int' -main:3: error: Illegal assignment to class variable +main:3: error: Cannot assign to class variable "x" via instance diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 8b52776e5037..439f682e6e32 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -1805,9 +1805,9 @@ class A: x = None # type: ClassVar A().x = 0 [out1] -main:4: error: Illegal assignment to class variable +main:4: error: Cannot assign to class variable "x" via instance [out2] -main:4: error: Illegal assignment to class variable +main:4: error: Cannot assign to class variable "x" via instance [case testIncrementalClassVarGone] import m @@ -1820,4 +1820,4 @@ class A: class A: x = None # type: int [out1] -main:2: error: Illegal assignment to class variable +main:2: error: Cannot assign to class variable "x" via instance From 137215e0540fb895911c83f35d66830e9d62c1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 14:39:16 +0100 Subject: [PATCH 16/23] Add more precise errors on ClassVar definitions --- mypy/semanal.py | 2 +- mypy/typeanal.py | 2 +- test-data/unit/semanal-classvar.test | 53 ++++++++++++++++------------ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 3473d6a0746c..263e51938ecc 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2161,7 +2161,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue): # In case of member access, report error only when assigning to self # Other kinds of member assignments should be already reported - self.fail('Invalid ClassVar definition', lvalue) + self.fail('ClassVar can only be used for assignments in class body', lvalue) def check_classvar_definition(self, typ: Type) -> bool: if not isinstance(typ, UnboundType): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index adfc4601d190..0d76192c7b14 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -154,7 +154,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return TypeType(item, line=t.line) elif fullname == 'typing.ClassVar': if self.nesting_level > 0: - self.fail('Invalid ClassVar definition', t) + self.fail('Invalid type: ClassVar nested inside other type', t) if len(t.args) == 0: return AnyType(line=t.line) if len(t.args) != 1: diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 3073eb24ef26..0a977a157e32 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -16,14 +16,14 @@ MypyFile:1( from typing import ClassVar x = None # type: ClassVar[int] [out] -main:2: error: Invalid ClassVar definition +main:2: error: ClassVar can only be used for assignments in class body [case testClassVarDefInFuncScope] from typing import ClassVar def f() -> None: x = None # type: ClassVar[int] [out] -main:3: error: Invalid ClassVar definition +main:3: error: ClassVar can only be used for assignments in class body [case testClassVarDefInMethod] from typing import ClassVar @@ -31,7 +31,7 @@ class A: def f(self) -> None: x = None # type: ClassVar [out] -main:4: error: Invalid ClassVar definition +main:4: error: ClassVar can only be used for assignments in class body [case testClassVarTooManyArguments] from typing import ClassVar @@ -66,58 +66,65 @@ main:4: error: Invalid type "__main__.T" from typing import ClassVar def f(x: str, y: ClassVar) -> None: pass [out] -main:2: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type + +[case testClassVarInMethodArgs] +from typing import ClassVar +class A: + def f(x: str, y: ClassVar) -> None: pass +[out] +main:3: error: Invalid type: ClassVar nested inside other type [case testClassVarFunctionRetType] from typing import ClassVar def f() -> ClassVar: pass [out] -main:2: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type + +[case testClassVarMethodRetType] +from typing import ClassVar +class A: + def f(self) -> ClassVar: pass +[out] +main:3: error: Invalid type: ClassVar nested inside other type [case testClassVarInCallableArgs] from typing import Callable, ClassVar f = None # type: Callable[[int, ClassVar], Any] [out] -main:2: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type [case testClassVarInCallableRet] from typing import Callable, ClassVar f = None # type: Callable[..., ClassVar] [out] -main:2: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type [case testClassVarInUnion] from typing import ClassVar, Union x = None # type: Union[ClassVar, str] [out] -main:2: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type [case testClassVarInUnionAsAttribute] from typing import ClassVar, Union class A: x = None # type: Union[ClassVar, str] [out] -main:3: error: Invalid ClassVar definition +main:3: error: Invalid type: ClassVar nested inside other type [case testListWithClassVars] from typing import ClassVar, List x = [] # type: List[ClassVar] [builtins fixtures/list.pyi] [out] -main:2: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type [case testTupleClassVar] from typing import ClassVar, Tuple x = None # type: Tuple[ClassVar, int] [out] -main:2: error: Invalid ClassVar definition - -[case testMultipleLvalues] -from typing import ClassVar -class A: - x, y = None, None # type: ClassVar, ClassVar -[out] -main:3: error: Invalid ClassVar definition +main:2: error: Invalid type: ClassVar nested inside other type [case testMultipleLvaluesWithList] from typing import ClassVar, List @@ -125,7 +132,7 @@ class A: [x, y] = None, None # type: List[ClassVar] [builtins fixtures/list.pyi] [out] -main:3: error: Invalid ClassVar definition +main:3: error: Invalid type: ClassVar nested inside other type [case testDeeplyNested] from typing import Callable, ClassVar, Union @@ -133,14 +140,14 @@ class A: pass class B: x = None # type: Union[str, Callable[[A, ClassVar], int]] [out] -main:4: error: Invalid ClassVar definition +main:4: error: Invalid type: ClassVar nested inside other type [case testClassVarInClassVar] from typing import ClassVar class A: x = None # type: ClassVar[ClassVar[int]] [out] -main:3: error: Invalid ClassVar definition +main:3: error: Invalid type: ClassVar nested inside other type [case testInsideGeneric] from typing import ClassVar, Generic, TypeVar @@ -149,7 +156,7 @@ class A(Generic[T]): pass class B: x = None # type: A[ClassVar] [out] -main:5: error: Invalid ClassVar definition +main:5: error: Invalid type: ClassVar nested inside other type [case testDefineOnSelf] from typing import ClassVar @@ -157,4 +164,4 @@ class A: def __init__(self) -> None: self.x = None # type: ClassVar [out] -main:4: error: Invalid ClassVar definition +main:4: error: ClassVar can only be used for assignments in class body From 5dd131ab6e2585cbd3da717bfc371c8ce75ef056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 14:46:04 +0100 Subject: [PATCH 17/23] Minor style improvements --- mypy/semanal.py | 2 +- mypy/typeanal.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 263e51938ecc..1cde78d31136 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2150,7 +2150,7 @@ def build_typeddict_typeinfo(self, name: str, items: List[str], def check_classvar(self, s: AssignmentStmt) -> None: lvalue = s.lvalues[0] - if len(s.lvalues) != 1 or not isinstance(lvalue, (NameExpr, MemberExpr)): + if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr): return if not self.check_classvar_definition(s.type): return diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0d76192c7b14..b3f37f1ed0b8 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -136,12 +136,12 @@ def visit_unbound_type(self, t: UnboundType) -> Type: if len(t.args) != 1: self.fail('Optional[...] must have exactly one type argument', t) return AnyType() - items = self.anal_array(t.args) + item = self.anal_nested(t.args[0]) if experiments.STRICT_OPTIONAL: - return UnionType.make_simplified_union([items[0], NoneTyp()]) + return UnionType.make_simplified_union([item, NoneTyp()]) else: # Without strict Optional checking Optional[t] is just an alias for t. - return items[0] + return item elif fullname == 'typing.Callable': return self.analyze_callable_type(t) elif fullname == 'typing.Type': @@ -149,8 +149,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return TypeType(AnyType(), line=t.line) if len(t.args) != 1: self.fail('Type[...] must have exactly one type argument', t) - items = self.anal_array(t.args) - item = items[0] + item = self.anal_nested(t.args[0]) return TypeType(item, line=t.line) elif fullname == 'typing.ClassVar': if self.nesting_level > 0: @@ -160,8 +159,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: if len(t.args) != 1: self.fail('ClassVar[...] must have at most one type argument', t) return AnyType() - items = self.anal_array(t.args) - return items[0] + return self.anal_nested(t.args[0]) elif fullname == 'mypy_extensions.NoReturn': return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: From 30a4185245e1d6a99e1b7db5c013b54baa979680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 15:09:12 +0100 Subject: [PATCH 18/23] Prohibit ClassVars in for and with statements --- mypy/semanal.py | 9 +++++++- test-data/unit/semanal-classvar.test | 32 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1cde78d31136..0970df9ac57d 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2161,7 +2161,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: elif not isinstance(lvalue, MemberExpr) or self.is_self_member_ref(lvalue): # In case of member access, report error only when assigning to self # Other kinds of member assignments should be already reported - self.fail('ClassVar can only be used for assignments in class body', lvalue) + self.fail_invalid_classvar(lvalue) def check_classvar_definition(self, typ: Type) -> bool: if not isinstance(typ, UnboundType): @@ -2171,6 +2171,9 @@ def check_classvar_definition(self, typ: Type) -> bool: return False return sym.node.fullname() == 'typing.ClassVar' + def fail_invalid_classvar(self, context: Context) -> None: + self.fail('ClassVar can only be used for assignments in class body', context) + def visit_decorator(self, dec: Decorator) -> None: for d in dec.decorators: d.accept(self) @@ -2272,6 +2275,8 @@ def visit_for_stmt(self, s: ForStmt) -> None: # Bind index variables and check if they define new names. self.analyze_lvalue(s.index, explicit_type=s.index_type is not None) if s.index_type: + if self.check_classvar_definition(s.index_type): + self.fail_invalid_classvar(s.index) allow_tuple_literal = isinstance(s.index, (TupleExpr, ListExpr)) s.index_type = self.anal_type(s.index_type, allow_tuple_literal) self.store_declared_types(s.index, s.index_type) @@ -2347,6 +2352,8 @@ def visit_with_stmt(self, s: WithStmt) -> None: # Since we have a target, pop the next type from types if types: t = types.pop(0) + if self.check_classvar_definition(t): + self.fail_invalid_classvar(n) allow_tuple_literal = isinstance(n, (TupleExpr, ListExpr)) t = self.anal_type(t, allow_tuple_literal) new_types.append(t) diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 0a977a157e32..d4fdb68cba6d 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -165,3 +165,35 @@ class A: self.x = None # type: ClassVar [out] main:4: error: ClassVar can only be used for assignments in class body + +[case testForIndex] +from typing import ClassVar +for i in []: # type: ClassVar + pass +[out] +main:2: error: ClassVar can only be used for assignments in class body + +[case testForIndexInClassBody] +from typing import ClassVar +class A: + for i in []: # type: ClassVar + pass +[out] +main:3: error: ClassVar can only be used for assignments in class body + +[case testWithStmt] +from typing import ClassVar +class A: pass +with A() as x: # type: ClassVar + pass +[out] +main:3: error: ClassVar can only be used for assignments in class body + +[case testWithStmtInClassBody] +from typing import ClassVar +class A: pass +class B: + with A() as x: # type: ClassVar + pass +[out] +main:4: error: ClassVar can only be used for assignments in class body From bcce34d4825bdb5f157de216a95c0dd54048b413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sat, 18 Feb 2017 19:22:37 +0100 Subject: [PATCH 19/23] Rename check_classvar_definition to is_classvar --- mypy/semanal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 0970df9ac57d..df32d0cffb35 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2152,7 +2152,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: lvalue = s.lvalues[0] if len(s.lvalues) != 1 or not isinstance(lvalue, RefExpr): return - if not self.check_classvar_definition(s.type): + if not self.is_classvar(s.type): return if self.is_class_scope() and isinstance(lvalue, NameExpr): node = lvalue.node @@ -2163,7 +2163,7 @@ def check_classvar(self, s: AssignmentStmt) -> None: # Other kinds of member assignments should be already reported self.fail_invalid_classvar(lvalue) - def check_classvar_definition(self, typ: Type) -> bool: + def is_classvar(self, typ: Type) -> bool: if not isinstance(typ, UnboundType): return False sym = self.lookup_qualified(typ.name, typ) @@ -2275,7 +2275,7 @@ def visit_for_stmt(self, s: ForStmt) -> None: # Bind index variables and check if they define new names. self.analyze_lvalue(s.index, explicit_type=s.index_type is not None) if s.index_type: - if self.check_classvar_definition(s.index_type): + if self.is_classvar(s.index_type): self.fail_invalid_classvar(s.index) allow_tuple_literal = isinstance(s.index, (TupleExpr, ListExpr)) s.index_type = self.anal_type(s.index_type, allow_tuple_literal) @@ -2352,7 +2352,7 @@ def visit_with_stmt(self, s: WithStmt) -> None: # Since we have a target, pop the next type from types if types: t = types.pop(0) - if self.check_classvar_definition(t): + if self.is_classvar(t): self.fail_invalid_classvar(n) allow_tuple_literal = isinstance(n, (TupleExpr, ListExpr)) t = self.anal_type(t, allow_tuple_literal) From 7ddca4bee90992031e1162f1667d9002ae6e351a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Sun, 19 Feb 2017 18:52:40 +0100 Subject: [PATCH 20/23] Don't consider types in signatures as nested --- mypy/semanal.py | 15 +++++++++++++ mypy/typeanal.py | 32 +++++++++++++++------------- test-data/unit/semanal-classvar.test | 14 ++++++++---- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index df32d0cffb35..d995f4517ed3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -483,6 +483,7 @@ def analyze_function(self, defn: FuncItem) -> None: if defn.type: # Signature must be analyzed in the surrounding scope so that # class-level imported names and type variables are in scope. + self.check_classvar_in_signature(defn.type) defn.type = self.anal_type(defn.type) self.check_function_signature(defn) if isinstance(defn, FuncDef): @@ -523,6 +524,20 @@ def analyze_function(self, defn: FuncItem) -> None: self.leave() self.function_stack.pop() + def check_classvar_in_signature(self, typ: Type) -> None: + t = None # type: Type + if isinstance(typ, Overloaded): + for t in typ.items(): + self.check_classvar_in_signature(t) + return + if not isinstance(typ, CallableType): + return + for t in typ.arg_types + [typ.ret_type]: + if self.is_classvar(t): + self.fail_invalid_classvar(t) + # Show only one error per signature + break + def add_func_type_variables_to_symbol_table( self, defn: FuncItem) -> List[SymbolTableNode]: nodes = [] # type: List[SymbolTableNode] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index b3f37f1ed0b8..16be2c630fc0 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -124,7 +124,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return self.builtin_type('builtins.tuple') if len(t.args) == 2 and isinstance(t.args[1], EllipsisType): # Tuple[T, ...] (uniform, variable-length tuple) - instance = self.builtin_type('builtins.tuple', [self.anal_nested(t.args[0])]) + instance = self.builtin_type('builtins.tuple', [self.anal_type(t.args[0])]) instance.line = t.line return instance return self.tuple_type(self.anal_array(t.args)) @@ -136,7 +136,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: if len(t.args) != 1: self.fail('Optional[...] must have exactly one type argument', t) return AnyType() - item = self.anal_nested(t.args[0]) + item = self.anal_type(t.args[0]) if experiments.STRICT_OPTIONAL: return UnionType.make_simplified_union([item, NoneTyp()]) else: @@ -149,7 +149,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return TypeType(AnyType(), line=t.line) if len(t.args) != 1: self.fail('Type[...] must have exactly one type argument', t) - item = self.anal_nested(t.args[0]) + item = self.anal_type(t.args[0]) return TypeType(item, line=t.line) elif fullname == 'typing.ClassVar': if self.nesting_level > 0: @@ -159,7 +159,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: if len(t.args) != 1: self.fail('ClassVar[...] must have at most one type argument', t) return AnyType() - return self.anal_nested(t.args[0]) + return self.anal_type(t.args[0]) elif fullname == 'mypy_extensions.NoReturn': return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: @@ -302,8 +302,8 @@ def visit_type_var(self, t: TypeVarType) -> Type: return t def visit_callable_type(self, t: CallableType) -> Type: - return t.copy_modified(arg_types=self.anal_array(t.arg_types), - ret_type=self.anal_nested(t.ret_type), + return t.copy_modified(arg_types=self.anal_array(t.arg_types, nested=False), + ret_type=self.anal_type(t.ret_type, nested=False), fallback=t.fallback or self.builtin_type('builtins.function'), variables=self.anal_var_defs(t.variables)) @@ -327,13 +327,13 @@ def visit_tuple_type(self, t: TupleType) -> Type: def visit_typeddict_type(self, t: TypedDictType) -> Type: items = OrderedDict([ - (item_name, self.anal_nested(item_type)) + (item_name, self.anal_type(item_type)) for (item_name, item_type) in t.items.items() ]) return TypedDictType(items, t.fallback) def visit_star_type(self, t: StarType) -> Type: - return StarType(self.anal_nested(t.type), t.line) + return StarType(self.anal_type(t.type), t.line) def visit_union_type(self, t: UnionType) -> Type: return UnionType(self.anal_array(t.items), t.line) @@ -346,7 +346,7 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type: return AnyType() def visit_type_type(self, t: TypeType) -> Type: - return TypeType(self.anal_nested(t.item), line=t.line) + return TypeType(self.anal_type(t.item), line=t.line) def analyze_callable_type(self, t: UnboundType) -> Type: fallback = self.builtin_type('builtins.function') @@ -359,7 +359,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type: fallback=fallback, is_ellipsis_args=True) elif len(t.args) == 2: - ret_type = self.anal_nested(t.args[1]) + ret_type = self.anal_type(t.args[1]) if isinstance(t.args[0], TypeList): # Callable[[ARG, ...], RET] (ordinary callable type) args = t.args[0].items @@ -383,18 +383,20 @@ def analyze_callable_type(self, t: UnboundType) -> Type: self.fail('Invalid function type', t) return AnyType() - def anal_array(self, a: List[Type]) -> List[Type]: + def anal_array(self, a: List[Type], nested: bool = True) -> List[Type]: res = [] # type: List[Type] for t in a: - res.append(self.anal_nested(t)) + res.append(self.anal_type(t, nested)) return res - def anal_nested(self, t: Type) -> Type: - self.nesting_level += 1 + def anal_type(self, t: Type, nested: bool = True) -> Type: + if nested: + self.nesting_level += 1 try: return t.accept(self) finally: - self.nesting_level -= 1 + if nested: + self.nesting_level -= 1 def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]: a = [] # type: List[TypeVarDef] diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index d4fdb68cba6d..39f09cd93d26 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -66,27 +66,33 @@ main:4: error: Invalid type "__main__.T" from typing import ClassVar def f(x: str, y: ClassVar) -> None: pass [out] -main:2: error: Invalid type: ClassVar nested inside other type +main:2: error: ClassVar can only be used for assignments in class body [case testClassVarInMethodArgs] from typing import ClassVar class A: def f(x: str, y: ClassVar) -> None: pass [out] -main:3: error: Invalid type: ClassVar nested inside other type +main:3: error: ClassVar can only be used for assignments in class body [case testClassVarFunctionRetType] from typing import ClassVar def f() -> ClassVar: pass [out] -main:2: error: Invalid type: ClassVar nested inside other type +main:2: error: ClassVar can only be used for assignments in class body [case testClassVarMethodRetType] from typing import ClassVar class A: def f(self) -> ClassVar: pass [out] -main:3: error: Invalid type: ClassVar nested inside other type +main:3: error: ClassVar can only be used for assignments in class body + +[case testMultipleClassVarInFunctionSig] +from typing import ClassVar +def f(x: ClassVar, y: ClassVar) -> ClassVar: pass +[out] +main:2: error: ClassVar can only be used for assignments in class body [case testClassVarInCallableArgs] from typing import Callable, ClassVar From 92b3532dff7954cd59c8c2b900219d5b64c5f206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Wed, 22 Feb 2017 18:11:07 +0100 Subject: [PATCH 21/23] Prohibit generic class variables --- mypy/typeanal.py | 30 +++++++++++++++++++++++++--- test-data/unit/check-classvar.test | 21 ------------------- test-data/unit/semanal-classvar.test | 18 +++++++++++++++++ 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 16be2c630fc0..05f826f29c50 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1,10 +1,10 @@ """Semantic analysis of types""" from collections import OrderedDict -from typing import Callable, cast, List, Optional +from typing import Callable, List, Optional, Set from mypy.types import ( - Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, + Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, TypeVarId, AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args, ) @@ -159,7 +159,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type: if len(t.args) != 1: self.fail('ClassVar[...] must have at most one type argument', t) return AnyType() - return self.anal_type(t.args[0]) + item = self.anal_type(t.args[0]) + if isinstance(item, TypeVarType) or self.get_type_vars(item): + self.fail('Invalid type: ClassVar cannot be generic', t) + return AnyType() + return item elif fullname == 'mypy_extensions.NoReturn': return UninhabitedType(is_noreturn=True) elif sym.kind == TYPE_ALIAS: @@ -259,6 +263,26 @@ def get_tvar_name(self, t: Type) -> Optional[str]: return t.name return None + def get_type_vars(self, typ: Type) -> List[TypeVarType]: + """Get all type variables that are present in an already analyzed type, + without duplicates, in order of textual appearance. + Similar to get_type_var_names. + """ + all_vars = [] # type: List[TypeVarType] + for t in get_typ_args(typ): + if isinstance(t, TypeVarType): + all_vars.append(t) + else: + all_vars.extend(self.get_type_vars(t)) + # Remove duplicates while preserving order + included = set() # type: Set[TypeVarId] + tvars = [] + for var in all_vars: + if var.id not in included: + tvars.append(var) + included.add(var.id) + return tvars + def replace_alias_tvars(self, tp: Type, vars: List[str], subs: List[Type], newline: int, newcolumn: int) -> Type: """Replace type variables in a generic type alias tp with substitutions subs diff --git a/test-data/unit/check-classvar.test b/test-data/unit/check-classvar.test index 2c8aa09297d8..02ba8f008c30 100644 --- a/test-data/unit/check-classvar.test +++ b/test-data/unit/check-classvar.test @@ -190,27 +190,6 @@ class E(D): [out] main:8: error: Incompatible types in assignment (expression has type "Union[A, B, C]", base class "D" defined the type as "Union[A, B]") -[case testClassVarWithGeneric] -from typing import ClassVar, Generic, TypeVar -T = TypeVar('T') -class A(Generic[T]): - x = None # type: ClassVar[T] -reveal_type(A[int]().x) -[out] -main:5: error: Revealed type is 'builtins.int*' - -[case testClassVarWithGenericList] -from typing import ClassVar, Generic, List, TypeVar -T = TypeVar('T') -class A(Generic[T]): - x = None # type: ClassVar[List[T]] -reveal_type(A[int]().x) -A[int].x.append('a') -[builtins fixtures/list.pyi] -[out] -main:5: error: Revealed type is 'builtins.list[builtins.int*]' -main:6: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "T" - [case testAssignmentToCallableRet] from typing import ClassVar class A: diff --git a/test-data/unit/semanal-classvar.test b/test-data/unit/semanal-classvar.test index 39f09cd93d26..677e1bd8cadc 100644 --- a/test-data/unit/semanal-classvar.test +++ b/test-data/unit/semanal-classvar.test @@ -203,3 +203,21 @@ class B: pass [out] main:4: error: ClassVar can only be used for assignments in class body + +[case testClassVarWithGeneric] +from typing import ClassVar, Generic, TypeVar +T = TypeVar('T') +class A(Generic[T]): + x = None # type: ClassVar[T] +[out] +main:4: error: Invalid type: ClassVar cannot be generic + +[case testClassVarWithNestedGeneric] +from typing import ClassVar, Generic, List, TypeVar, Union +T = TypeVar('T') +U = TypeVar('U') +class A(Generic[T, U]): + x = None # type: ClassVar[Union[T, List[U]]] +[builtins fixtures/list.pyi] +[out] +main:5: error: Invalid type: ClassVar cannot be generic From 20b1abe4df13f88ff03b513950f450937de47650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Tue, 28 Feb 2017 17:02:20 +0100 Subject: [PATCH 22/23] Prohibit access to generic instance variables via class --- mypy/checkmember.py | 4 +++- mypy/messages.py | 1 + mypy/typeanal.py | 23 ++--------------------- mypy/types.py | 21 +++++++++++++++++++++ test-data/unit/check-classes.test | 18 ++++++++++++++++++ 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index b4448e4a5ec4..91b5d7071de7 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -5,7 +5,7 @@ from mypy.types import ( Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef, Overloaded, TypeVarType, UnionType, PartialType, - DeletedType, NoneTyp, TypeType, function_type + DeletedType, NoneTyp, TypeType, function_type, get_type_vars, ) from mypy.nodes import ( TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr, @@ -384,6 +384,8 @@ def analyze_class_attribute_access(itype: Instance, if t: if isinstance(t, PartialType): return handle_partial_attribute_type(t, is_lvalue, msg, node.node) + if not is_method and (isinstance(t, TypeVarType) or get_type_vars(t)): + msg.fail(messages.GENERIC_INSTANCE_VAR_CLASS_ACCESS, context) is_classmethod = is_decorated and cast(Decorator, node.node).func.is_class return add_class_tvars(t, itype, is_classmethod, builtin_type, original_type) elif isinstance(node.node, Var): diff --git a/mypy/messages.py b/mypy/messages.py index f918f0605c3e..782df03f5bd6 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -83,6 +83,7 @@ MALFORMED_ASSERT = 'Assertion is always true, perhaps remove parentheses?' NON_BOOLEAN_IN_CONDITIONAL = 'Condition must be a boolean' DUPLICATE_TYPE_SIGNATURES = 'Function has duplicate type signatures' +GENERIC_INSTANCE_VAR_CLASS_ACCESS = 'Access to generic instance variables via class is ambiguous' ARG_CONSTRUCTOR_NAMES = { ARG_POS: "Arg", diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 05f826f29c50..9c28d2ec5465 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -7,6 +7,7 @@ Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, TypeVarId, AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args, + get_type_vars, ) from mypy.nodes import ( BOUND_TVAR, UNBOUND_TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, @@ -160,7 +161,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: self.fail('ClassVar[...] must have at most one type argument', t) return AnyType() item = self.anal_type(t.args[0]) - if isinstance(item, TypeVarType) or self.get_type_vars(item): + if isinstance(item, TypeVarType) or get_type_vars(item): self.fail('Invalid type: ClassVar cannot be generic', t) return AnyType() return item @@ -263,26 +264,6 @@ def get_tvar_name(self, t: Type) -> Optional[str]: return t.name return None - def get_type_vars(self, typ: Type) -> List[TypeVarType]: - """Get all type variables that are present in an already analyzed type, - without duplicates, in order of textual appearance. - Similar to get_type_var_names. - """ - all_vars = [] # type: List[TypeVarType] - for t in get_typ_args(typ): - if isinstance(t, TypeVarType): - all_vars.append(t) - else: - all_vars.extend(self.get_type_vars(t)) - # Remove duplicates while preserving order - included = set() # type: Set[TypeVarId] - tvars = [] - for var in all_vars: - if var.id not in included: - tvars.append(var) - included.add(var.id) - return tvars - def replace_alias_tvars(self, tp: Type, vars: List[str], subs: List[Type], newline: int, newcolumn: int) -> Type: """Replace type variables in a generic type alias tp with substitutions subs diff --git a/mypy/types.py b/mypy/types.py index 5e2d1feda81f..8b792cfa6bdb 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1771,3 +1771,24 @@ def set_typ_args(tp: Type, new_args: List[Type], line: int = -1, column: int = - return tp.copy_modified(arg_types=new_args[:-1], ret_type=new_args[-1], line=line, column=column) return tp + + +def get_type_vars(typ: Type) -> List[TypeVarType]: + """Get all type variables that are present in an already analyzed type, + without duplicates, in order of textual appearance. + Similar to TypeAnalyser.get_type_var_names. + """ + all_vars = [] # type: List[TypeVarType] + for t in get_typ_args(typ): + if isinstance(t, TypeVarType): + all_vars.append(t) + else: + all_vars.extend(get_type_vars(t)) + # Remove duplicates while preserving order + included = set() # type: Set[TypeVarId] + tvars = [] + for var in all_vars: + if var.id not in included: + tvars.append(var) + included.add(var.id) + return tvars diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 7e46c6c3f6c8..01779392d772 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -585,6 +585,24 @@ x = C.x [out] main:2: error: Need type annotation for variable +[case testAccessingGenericClassAttribute] +from typing import Generic, TypeVar +T = TypeVar('T') +class A(Generic[T]): + x = None # type: T +A.x # E: Access to generic instance variables via class is ambiguous +A[int].x # E: Access to generic instance variables via class is ambiguous + +[case testAccessingNestedGenericClassAttribute] +from typing import Generic, List, TypeVar, Union +T = TypeVar('T') +U = TypeVar('U') +class A(Generic[T, U]): + x = None # type: Union[T, List[U]] +A.x # E: Access to generic instance variables via class is ambiguous +A[int, int].x # E: Access to generic instance variables via class is ambiguous +[builtins fixtures/list.pyi] + -- Nested classes -- -------------- From 5430b329e3e624984ebe0f85bde95aa53f58687c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Miedzi=C5=84ski?= Date: Thu, 9 Mar 2017 19:43:20 +0100 Subject: [PATCH 23/23] Add incremental checking test --- test-data/unit/check-incremental.test | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 267c3e625744..4ddec38618dc 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -1893,6 +1893,20 @@ class A: [out1] main:2: error: Cannot assign to class variable "x" via instance +[case testCachingClassVar] +import b +[file a.py] +from typing import ClassVar +class A: + x = None # type: ClassVar[int] +[file b.py] +import a +[file b.py.next] +import a +a.A().x = 0 +[out2] +tmp/b.py:2: error: Cannot assign to class variable "x" via instance + [case testQuickAndDirty1] # flags: --quick-and-dirty import b, c