From 9f12405980f3f25363eb53acd2002c5a377f812c Mon Sep 17 00:00:00 2001 From: Jeremy Paige Date: Mon, 6 May 2019 10:44:15 -0400 Subject: [PATCH 1/3] Ignore virtualenv for commits and tests --- .gitignore | 6 ++++++ setup.cfg | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 11f711928cdbf..789c7589c3622 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,11 @@ htmlcov # pytest cache .pytest_cache/ +# virtualenv +.Python +bin/ +lib/ +include/ + .tox pip-wheel-metadata diff --git a/setup.cfg b/setup.cfg index 50bddd792125a..8eaca2e968522 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,9 @@ max-line-length = 99 exclude = # from .gitignore: directories, and file patterns that intersect with *.py build, + bin, + lib, + include, @*, env, docs/build, @@ -27,6 +30,7 @@ exclude = tmp-test-dirs/* .tox .eggs + .Python # Things to ignore: # E251: spaces around default arg value (against our style) From 46cf5345a8b44c2c5100d4f43f4205773cb0add0 Mon Sep 17 00:00:00 2001 From: Jeremy Paige Date: Wed, 8 May 2019 19:22:25 -0400 Subject: [PATCH 2/3] Show class that originally defined attribute in output If a subclass or its instance attempts to assign an incompatible type display the class that originally defined the type. --- mypy/checker.py | 68 +++++++++++- mypy/message_registry.py | 2 + test-data/unit/check-classes.test | 4 +- test-data/unit/check-functions.test | 105 ++++++++++++++++++ .../unit/check-multiple-inheritance.test | 18 +-- test-data/unit/fine-grained.test | 8 +- 6 files changed, 184 insertions(+), 21 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6390b381d918c..00e260cc29e28 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1980,7 +1980,7 @@ def check_compatibility_super(self, lvalue: RefExpr, lvalue_type: Optional[Type] return self.check_subtype(compare_type, base_type, lvalue, message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, 'expression has type', - 'base class "%s" defined the type as' % base.name()) + message_registry.PARENT_CLASS_MISMATCH.format(base.name())) return True def lvalue_type_from_base(self, expr_node: Var, @@ -2554,7 +2554,8 @@ def check_simple_assignment(self, lvalue_type: Optional[Type], rvalue: Expressio return rvalue_type def check_member_assignment(self, instance_type: Type, attribute_type: Type, - rvalue: Expression, context: Context) -> Tuple[Type, Type, bool]: + rvalue: Expression, context: MemberExpr + ) -> Tuple[Type, Type, bool]: """Type member assignment. This defers to check_simple_assignment, unless the member expression @@ -2567,9 +2568,17 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, care about interaction between binder and __set__(). """ # Descriptors don't participate in class-attribute access - if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or - isinstance(instance_type, TypeType)): - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + if (isinstance(instance_type, CallableType) and instance_type.is_type_obj() and + isinstance(instance_type.ret_type, Instance)): + rvalue_type = self.check_parent_member_assignment(instance_type.ret_type, + attribute_type, attribute_type, + rvalue, context) + return rvalue_type, attribute_type, True + + elif isinstance(instance_type, TypeType) and isinstance(instance_type.item, Instance): + rvalue_type = self.check_parent_member_assignment(instance_type.item, + attribute_type, attribute_type, + rvalue, context) return rvalue_type, attribute_type, True if not isinstance(attribute_type, Instance): @@ -2585,7 +2594,8 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, # the return type of __get__. This doesn't match the python semantics, # (which allow you to override the descriptor with any value), but preserves # the type of accessing the attribute (even after the override). - rvalue_type = self.check_simple_assignment(get_type, rvalue, context) + rvalue_type = self.check_parent_member_assignment(instance_type, get_type, + attribute_type, rvalue, context) return rvalue_type, get_type, True dunder_set = attribute_type.type.get_method('__set__') @@ -2629,6 +2639,52 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, infer = is_subtype(rvalue_type, get_type) and is_subtype(get_type, set_type) return rvalue_type if infer else set_type, get_type, infer + def check_parent_member_assignment(self, lvalue_type: Type, + lvalue_attribute_type: Type, rvalue_type: Type, + rvalue: Expression, context: MemberExpr) -> Type: + """Type inherited member assignment. + + This defers to check_simple_assignment if the attribute is local to the instance_type, + otherwise it will inform the user where the attribute type was originally defined. + + Return the inferred rvalue_type. + """ + if isinstance(lvalue_type, (Instance)): + attribute_is_defined = lvalue_type.has_readable_member(context.name) + if isinstance(lvalue_type, UnionType): + direct_lvalue_vars = list(itertools.chain.from_iterable( + i.type.names for i in lvalue_type.items)) + lvalue_bases = list(itertools.chain.from_iterable( + i.type.bases for i in lvalue_type.items)) + else: + direct_lvalue_vars = lvalue_type.type.names + lvalue_bases = lvalue_type.type.bases + if '__metaclass__' in lvalue_type.type.names: + # if the assigned metaclass is not class itself we have other problems + metacls = lvalue_type.type.names['__metaclass__'].type.ret_type # type: ignore + lvalue_bases.append(metacls) + attribute_is_defined = (attribute_is_defined or + metacls.has_readable_member(context.name)) + if attribute_is_defined and context.name not in direct_lvalue_vars: + for base in lvalue_bases: + if context.name in base.type.names: + parent_context = base.type.names[context.name].node + if isinstance(parent_context, Var) and parent_context.info.is_generic(): + # attribute may have been defined in this class but its type is + # probably defined elsewhere, possibly in the instance itself. + continue + always_allow_any = (rvalue_type is not None and + not isinstance(rvalue_type, AnyType)) + rvalue_type_accept = self.expr_checker.accept(rvalue, rvalue_type, + always_allow_any) + self.check_subtype(rvalue_type_accept, rvalue_type, context, + message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, + 'expression has type', + message_registry.PARENT_CLASS_MISMATCH.format( + base.type.defn.name)) + return rvalue_type_accept + return self.check_simple_assignment(lvalue_attribute_type, rvalue, context) + def check_indexed_assignment(self, lvalue: IndexExpr, rvalue: Expression, context: Context) -> None: """Type check indexed assignment base[index] = rvalue. diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 311e06e2a3ae1..f4555513a8151 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -138,6 +138,8 @@ CANNOT_OVERRIDE_CLASS_VAR = \ 'Cannot override class variable (previously declared on base class "{}") with instance ' \ 'variable' # type: Final +PARENT_CLASS_MISMATCH = \ + 'base class "{}" defined the type as' # type: Final # Protocol RUNTIME_PROTOCOL_EXPECTED = \ diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index f40373c302116..d86eb633a3b84 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -188,7 +188,7 @@ class A: self.x = 0 class B(A): def f(self) -> None: - self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + self.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [out] [case testAssignmentToAttributeInMultipleMethods] @@ -3730,7 +3730,7 @@ class B(A): def __init__(self) -> None: self.a = "a" [out] -main:5: error: Incompatible types in assignment (expression has type "str", variable has type "int") +main:5: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [case testVariableSubclassTypeOverwrite] class A: diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index fea7b17b0ad61..620fc1169888c 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -2505,3 +2505,108 @@ def f() -> int: ... [file p/d.py] import p def f() -> int: ... + +[case testFactory] +from typing import Type +class A: + x = "foo" +def f() -> Type[A]: + return A +def g() -> A: + return A() + +f().x = "bar" +f().x = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +f()().x = "baz" +f()().x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +g().x = "" +g().x = 2 # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testFactoryWithInheritance] +from typing import Type +class A: + x = "foo" +class B(A): + pass +def f() -> Type[B]: + return B +def g() -> B: + return B() + +f().x = "bar" +f().x = 0 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") +f()().x = "baz" +f()().x = 1 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") +g().x = "" +g().x = 2 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") + +[case testRecursiveCallable] +from typing import Optional +class A: + x = 0 # Type: int + def __init__(self, b: bool = True) -> None: + self.b = b + def __call__(self, b: Optional[bool] = None) -> A: + if b is not None: + self.b = b + return self + +A.x = 1 +A().x = 2 +A(True).x = 3 +A()().x = 4 +A(True)().x = 5 +A()()().x = 6 +A(True)()().x = 7 +a = A() +a().x = 8 +a = A(True) +a().x = 9 + +A.x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +A().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +A(False).x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +A()().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +A(False)().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +A()()().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +A(False)()().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +a = A() +a().x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +a = A() +a(False).x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +A.b = True +A(True) +A(True).b = False +A(True)(False) +A(True)(False)(True).b = False +A(True)(None) +A(True)().b = False +A(True)(None).b = True +A(True)(None).b = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "bool") +A(True)()(False).b = "" # E: Incompatible types in assignment (expression has type "str", variable has type "bool") + +[builtins fixtures/bool.pyi] + +[case testMultiLayerCallable] +class A: + y = 0 # Type: int +class B(A): + def __init__(self, z: int) -> None: + self.z = z +def foo(x: int) -> B: + return B(x) +def bar(x: int) -> B: + return foo(x) + +a = bar(1) +a.z = 2 +a.z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +a.y = 2 +a.y = "" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") + +b = foo(3) +a.z = 4 +a.z = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +a.y = 4 +a.y = "" # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index dc69a4187cead..809c7da0aa4b6 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -44,9 +44,9 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") c.y = '' -c.y = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +c.y = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") [case testSimpleMultipleInheritanceAndInstanceVariableInClassBody] import typing @@ -57,9 +57,9 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") c.y = '' -c.y = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +c.y = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") [case testSimpleMultipleInheritanceAndClassVariable] import typing @@ -69,9 +69,9 @@ class B: y = '' class C(A, B): pass C.x = 1 -C.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +C.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") C.y = '' -C.y = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") +C.y = 1 # E: Incompatible types in assignment (expression has type "int", base class "B" defined the type as "str") -- Name collisions @@ -100,7 +100,7 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [case testClassVarNameOverlapInMultipleInheritanceWithCompatibleTypes] import typing @@ -111,9 +111,9 @@ class B: class C(A, B): pass c = C() c.x = 1 -c.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +c.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") C.x = 1 -C.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +C.x = '' # E: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int") [case testMethodNameCollisionInMultipleInheritanceWithIncompatibleSigs] import typing diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 232bafc52e593..94a707f3ebaa2 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1105,7 +1105,7 @@ class A: [out] == main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") -main:9: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:9: error: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") main:14: error: Incompatible types in assignment (expression has type "str", variable has type "int") [case testChangeBaseClassAttributeType] @@ -1123,7 +1123,7 @@ class A: self.x = 'a' [out] == -main:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:4: error: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str") [case testRemoveAttributeInBaseClass] import m @@ -2912,7 +2912,7 @@ class M(type): x = None # type: int [out] == -a.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +a.py:4: error: Incompatible types in assignment (expression has type "int", base class "M" defined the type as "str") == a.py:4: error: "Type[C]" has no attribute "x" == @@ -4220,7 +4220,7 @@ class C: y: str [out] == -main:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +main:4: error: Incompatible types in assignment (expression has type "int", base class "C" defined the type as "str") [case testNewTypeDependencies2] from a import N From 2969caaf3c797176d70d5ad646a67ea073a187f2 Mon Sep 17 00:00:00 2001 From: Jeremy Paige Date: Tue, 14 May 2019 18:27:08 -0700 Subject: [PATCH 3/3] better naming in check_parent_member_assignment --- mypy/checker.py | 115 ++++++++++++++---------------- mypy/checkexpr.py | 4 +- mypy/message_registry.py | 2 + test-data/unit/check-classes.test | 28 ++++++++ test-data/unit/fine-grained.test | 6 +- 5 files changed, 90 insertions(+), 65 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 382e73247e879..d2b390f8386cb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -929,8 +929,8 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str]) msg += "tuple argument {}".format(name[12:]) else: msg += 'argument "{}"'.format(name) - self.check_simple_assignment(arg.variable.type, arg.initializer, - context=arg, msg=msg, lvalue_name='argument', rvalue_name='default') + self.check_simple_assignment(arg.variable.type, arg.initializer, context=arg, + msg=msg, lvalue_name='argument has type', rvalue_name='default has type') # Type check body in a new scope. with self.binder.top_frame_context(): @@ -1765,8 +1765,8 @@ def check_import(self, node: ImportBase) -> None: message = '{} "{}"'.format(message_registry.INCOMPATIBLE_IMPORT_OF, cast(NameExpr, assign.rvalue).name) self.check_simple_assignment(lvalue_type, assign.rvalue, node, - msg=message, lvalue_name='local name', - rvalue_name='imported name') + msg=message, lvalue_name='local name has type', + rvalue_name='imported name has type') # # Statements @@ -2579,8 +2579,8 @@ def set_inference_error_fallback_type(self, var: Var, lvalue: Lvalue, type: Type def check_simple_assignment(self, lvalue_type: Optional[Type], rvalue: Expression, context: Context, msg: str = message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, - lvalue_name: str = 'variable', - rvalue_name: str = 'expression') -> Type: + lvalue_name: str = 'variable has type', + rvalue_name: str = 'expression has type') -> Type: if self.is_stub and isinstance(rvalue, EllipsisExpr): # '...' is always a valid initializer in a stub. return AnyType(TypeOfAny.special_form) @@ -2593,13 +2593,12 @@ def check_simple_assignment(self, lvalue_type: Optional[Type], rvalue: Expressio if isinstance(lvalue_type, DeletedType): self.msg.deleted_as_lvalue(lvalue_type, context) elif lvalue_type: - self.check_subtype(rvalue_type, lvalue_type, context, msg, - '{} has type'.format(rvalue_name), - '{} has type'.format(lvalue_name)) + self.check_subtype(rvalue_type, lvalue_type, context, msg, rvalue_name, + lvalue_name) return rvalue_type def check_member_assignment(self, instance_type: Type, attribute_type: Type, - rvalue: Expression, context: MemberExpr + rvalue: Expression, lvalue: MemberExpr ) -> Tuple[Type, Type, bool]: """Type member assignment. @@ -2613,40 +2612,39 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, care about interaction between binder and __set__(). """ # Descriptors don't participate in class-attribute access - if (isinstance(instance_type, CallableType) and instance_type.is_type_obj() and - isinstance(instance_type.ret_type, Instance)): + if (isinstance(instance_type, CallableType) and instance_type.is_type_obj()): rvalue_type = self.check_parent_member_assignment(instance_type.ret_type, - attribute_type, attribute_type, - rvalue, context) + attribute_type, lvalue, + attribute_type, rvalue) return rvalue_type, attribute_type, True - elif isinstance(instance_type, TypeType) and isinstance(instance_type.item, Instance): + elif isinstance(instance_type, TypeType): rvalue_type = self.check_parent_member_assignment(instance_type.item, - attribute_type, attribute_type, - rvalue, context) + attribute_type, lvalue, + attribute_type, rvalue) return rvalue_type, attribute_type, True if not isinstance(attribute_type, Instance): # TODO: support __set__() for union types. - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + rvalue_type = self.check_simple_assignment(attribute_type, rvalue, lvalue) return rvalue_type, attribute_type, True get_type = analyze_descriptor_access( instance_type, attribute_type, self.named_type, - self.msg, context, chk=self) + self.msg, lvalue, chk=self) if not attribute_type.type.has_readable_member('__set__'): # If there is no __set__, we type-check that the assigned value matches # the return type of __get__. This doesn't match the python semantics, # (which allow you to override the descriptor with any value), but preserves # the type of accessing the attribute (even after the override). rvalue_type = self.check_parent_member_assignment(instance_type, get_type, - attribute_type, rvalue, context) + lvalue, attribute_type, rvalue) return rvalue_type, get_type, True dunder_set = attribute_type.type.get_method('__set__') if dunder_set is None: self.msg.fail(message_registry.DESCRIPTOR_SET_NOT_CALLABLE.format(attribute_type), - context) + lvalue) return AnyType(TypeOfAny.from_error), get_type, False function = function_type(dunder_set, self.named_type('builtins.function')) @@ -2655,18 +2653,18 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, dunder_set_type = expand_type_by_instance(bound_method, typ) # Here we just infer the type, the result should be type-checked like a normal assignment. - # For this we use the rvalue as type context. + # For this we use the rvalue as type lvalue. self.msg.disable_errors() _, inferred_dunder_set_type = self.expr_checker.check_call( dunder_set_type, [TempNode(instance_type), rvalue], - [nodes.ARG_POS, nodes.ARG_POS], context) + [nodes.ARG_POS, nodes.ARG_POS], lvalue) self.msg.enable_errors() # And now we type check the call second time, to show errors related # to wrong arguments count, etc. self.expr_checker.check_call( dunder_set_type, [TempNode(instance_type), TempNode(AnyType(TypeOfAny.special_form))], - [nodes.ARG_POS, nodes.ARG_POS], context) + [nodes.ARG_POS, nodes.ARG_POS], lvalue) # should be handled by get_method above assert isinstance(inferred_dunder_set_type, CallableType) @@ -2680,13 +2678,13 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, # and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects. - rvalue_type = self.check_simple_assignment(set_type, rvalue, context) + rvalue_type = self.check_simple_assignment(set_type, rvalue, lvalue) infer = is_subtype(rvalue_type, get_type) and is_subtype(get_type, set_type) return rvalue_type if infer else set_type, get_type, infer - def check_parent_member_assignment(self, lvalue_type: Type, - lvalue_attribute_type: Type, rvalue_type: Type, - rvalue: Expression, context: MemberExpr) -> Type: + def check_parent_member_assignment(self, lvalue_base_type: Type, + lvalue_type: Type, lvalue: MemberExpr, + rvalue_type: Type, rvalue: Expression) -> Type: """Type inherited member assignment. This defers to check_simple_assignment if the attribute is local to the instance_type, @@ -2694,41 +2692,38 @@ def check_parent_member_assignment(self, lvalue_type: Type, Return the inferred rvalue_type. """ - if isinstance(lvalue_type, (Instance)): - attribute_is_defined = lvalue_type.has_readable_member(context.name) - if isinstance(lvalue_type, UnionType): - direct_lvalue_vars = list(itertools.chain.from_iterable( - i.type.names for i in lvalue_type.items)) - lvalue_bases = list(itertools.chain.from_iterable( - i.type.bases for i in lvalue_type.items)) - else: - direct_lvalue_vars = lvalue_type.type.names - lvalue_bases = lvalue_type.type.bases - if '__metaclass__' in lvalue_type.type.names: - # if the assigned metaclass is not class itself we have other problems - metacls = lvalue_type.type.names['__metaclass__'].type.ret_type # type: ignore - lvalue_bases.append(metacls) - attribute_is_defined = (attribute_is_defined or - metacls.has_readable_member(context.name)) - if attribute_is_defined and context.name not in direct_lvalue_vars: - for base in lvalue_bases: - if context.name in base.type.names: - parent_context = base.type.names[context.name].node + if (isinstance(lvalue_base_type, Instance) and + lvalue.name not in lvalue_base_type.type.names): + lvalue_warn = message_registry.PARENT_CLASS_MISMATCH + metaclass = lvalue_base_type.type.metaclass_type + if metaclass and metaclass.has_readable_member(lvalue.name): + if lvalue.name in metaclass.type.names: + meta_attribute = metaclass.type.names[lvalue.name].node + if not (isinstance(meta_attribute, Var) and meta_attribute.info.is_generic()): + return self.check_simple_assignment( + lvalue_type, rvalue, lvalue, + msg=message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, + lvalue_name=message_registry.METACLASS_MISMATCH.format( + metaclass.type.defn.name)) + else: + # attribute is defined further up in the metaclass MRO + lvalue_warn = 'base class "{{}}" of {}'.format( + message_registry.METACLASS_MISMATCH.format( + metaclass.type.defn.name)) + lvalue_base_type = metaclass + if lvalue_base_type.has_readable_member(lvalue.name): + for base in lvalue_base_type.type.bases: + if lvalue.name in base.type.names: + parent_context = base.type.names[lvalue.name].node if isinstance(parent_context, Var) and parent_context.info.is_generic(): - # attribute may have been defined in this class but its type is + # Attribute may have been defined in this class but its type is # probably defined elsewhere, possibly in the instance itself. continue - always_allow_any = (rvalue_type is not None and - not isinstance(rvalue_type, AnyType)) - rvalue_type_accept = self.expr_checker.accept(rvalue, rvalue_type, - always_allow_any) - self.check_subtype(rvalue_type_accept, rvalue_type, context, - message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, - 'expression has type', - message_registry.PARENT_CLASS_MISMATCH.format( - base.type.defn.name)) - return rvalue_type_accept - return self.check_simple_assignment(lvalue_attribute_type, rvalue, context) + return self.check_simple_assignment( + lvalue_type, rvalue, lvalue, + msg=message_registry.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, + lvalue_name=lvalue_warn.format(base.type.defn.name)) + return self.check_simple_assignment(lvalue_type, rvalue, lvalue) def check_indexed_assignment(self, lvalue: IndexExpr, rvalue: Expression, context: Context) -> None: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index af5c0fd8c11af..ff2ac74ce4f08 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -467,8 +467,8 @@ def check_typeddict_call_with_kwargs(self, callee: TypedDictType, self.chk.check_simple_assignment( lvalue_type=item_expected_type, rvalue=item_value, context=item_value, msg=message_registry.INCOMPATIBLE_TYPES, - lvalue_name='TypedDict item "{}"'.format(item_name), - rvalue_name='expression') + lvalue_name='TypedDict item "{}" has type'.format(item_name), + rvalue_name='expression has type') return callee diff --git a/mypy/message_registry.py b/mypy/message_registry.py index f4555513a8151..cbdcc9315add5 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -140,6 +140,8 @@ 'variable' # type: Final PARENT_CLASS_MISMATCH = \ 'base class "{}" defined the type as' # type: Final +METACLASS_MISMATCH = \ + 'metaclass "{}" defined the type as' # type: Final # Protocol RUNTIME_PROTOCOL_EXPECTED = \ diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 0973c050e55ac..4b3691d09b06f 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -5930,3 +5930,31 @@ class B: class C(A, B): pass [out] + +[case testMetaclassParentAttribute] +import submod +class A(metaclass=submod.M): + pass +A.x = "" +A.x = 1 +[file submod.py] +class N(type): + x: str +class M(N): + pass +[out] +main:5: error: Incompatible types in assignment (expression has type "int", base class "N" of metaclass "M" defined the type as "str") + +[case testParentMetaclassAttribute] +import submod +class A(metaclass=submod.M): + pass +class B(A): + pass +B.x = "" +B.x = 1 +[file submod.py] +class M(type): + x: str +[out] +main:7: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 5da10b15565fd..182c622727a97 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2807,7 +2807,7 @@ class M(type): x: int [out] == -a.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") +a.py:4: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") == a.py:4: error: "Type[C]" has no attribute "x" == @@ -2836,7 +2836,7 @@ class M(type): x: int [out] == -a.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") +a.py:3: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") == a.py:3: error: "Type[C]" has no attribute "x" == @@ -2912,7 +2912,7 @@ class M(type): x = None # type: int [out] == -a.py:4: error: Incompatible types in assignment (expression has type "int", base class "M" defined the type as "str") +a.py:4: error: Incompatible types in assignment (expression has type "int", metaclass "M" defined the type as "str") == a.py:4: error: "Type[C]" has no attribute "x" ==