diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ed3239a2bb46..d4dca6b6441d 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -202,7 +202,7 @@ def analyze_member_access(name: str, if chk and chk.should_suppress_optional_error([typ]): return AnyType() - return msg.has_no_attr(original_type, name, node) + return msg.has_no_attr(original_type, typ, name, node) def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, @@ -256,7 +256,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, else: if chk and chk.should_suppress_optional_error([itype]): return AnyType() - return msg.has_no_attr(original_type, name, node) + return msg.has_no_attr(original_type, itype, name, node) def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Context, diff --git a/mypy/messages.py b/mypy/messages.py index b15562b6268c..dd8165b69bba 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -361,72 +361,84 @@ def format_distinctly(self, type1: Type, type2: Type) -> Tuple[str, str]: # get some information as arguments, and they build an error message based # on them. - def has_no_attr(self, typ: Type, member: str, context: Context) -> Type: + def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Context) -> Type: """Report a missing or non-accessible member. - The type argument is the base type. If member corresponds to - an operator, use the corresponding operator name in the - messages. Return type Any. + original_type is the top-level type on which the error occurred. + typ is the actual type that is missing the member. These can be + different, e.g., in a union, original_type will be the union and typ + will be the specific item in the union that does not have the member + attribute. + + If member corresponds to an operator, use the corresponding operator + name in the messages. Return type Any. """ - if (isinstance(typ, Instance) and - typ.type.has_readable_member(member)): + if (isinstance(original_type, Instance) and + original_type.type.has_readable_member(member)): self.fail('Member "{}" is not assignable'.format(member), context) elif member == '__contains__': self.fail('Unsupported right operand type for in ({})'.format( - self.format(typ)), context) + self.format(original_type)), context) elif member in op_methods.values(): # Access to a binary operator member (e.g. _add). This case does # not handle indexing operations. for op, method in op_methods.items(): if method == member: - self.unsupported_left_operand(op, typ, context) + self.unsupported_left_operand(op, original_type, context) break elif member == '__neg__': self.fail('Unsupported operand type for unary - ({})'.format( - self.format(typ)), context) + self.format(original_type)), context) elif member == '__pos__': self.fail('Unsupported operand type for unary + ({})'.format( - self.format(typ)), context) + self.format(original_type)), context) elif member == '__invert__': self.fail('Unsupported operand type for ~ ({})'.format( - self.format(typ)), context) + self.format(original_type)), context) elif member == '__getitem__': # Indexed get. # TODO: Fix this consistently in self.format - if isinstance(typ, CallableType) and typ.is_type_obj(): + if isinstance(original_type, CallableType) and original_type.is_type_obj(): self.fail('The type {} is not generic and not indexable'.format( - self.format(typ)), context) + self.format(original_type)), context) else: self.fail('Value of type {} is not indexable'.format( - self.format(typ)), context) + self.format(original_type)), context) elif member == '__setitem__': # Indexed set. self.fail('Unsupported target for indexed assignment', context) elif member == '__call__': - if isinstance(typ, Instance) and (typ.type.fullname() == 'builtins.function'): + if isinstance(original_type, Instance) and \ + (original_type.type.fullname() == 'builtins.function'): # "'function' not callable" is a confusing error message. # Explain that the problem is that the type of the function is not known. self.fail('Cannot call function of unknown type', context) else: - self.fail('{} not callable'.format(self.format(typ)), context) + self.fail('{} not callable'.format(self.format(original_type)), context) else: # The non-special case: a missing ordinary attribute. if not self.disable_type_names: failed = False - if isinstance(typ, Instance) and typ.type.names: - alternatives = set(typ.type.names.keys()) + if isinstance(original_type, Instance) and original_type.type.names: + alternatives = set(original_type.type.names.keys()) matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives] matches.extend(best_matches(member, alternatives)[:3]) if matches: self.fail('{} has no attribute "{}"; maybe {}?'.format( - self.format(typ), member, pretty_or(matches)), context) + self.format(original_type), member, pretty_or(matches)), context) failed = True if not failed: - self.fail('{} has no attribute "{}"'.format(self.format(typ), + self.fail('{} has no attribute "{}"'.format(self.format(original_type), member), context) - else: - self.fail('Some element of union has no attribute "{}"'.format( - member), context) + elif isinstance(original_type, UnionType): + # The checker passes "object" in lieu of "None" for attribute + # checks, so we manually convert it back. + typ_format = self.format(typ) + if typ_format == '"object"' and \ + any(type(item) == NoneTyp for item in original_type.items): + typ_format = '"None"' + self.fail('Item {} of {} has no attribute "{}"'.format( + typ_format, self.format(original_type), member), context) return AnyType() def unsupported_operand_types(self, op: str, left_type: Any, diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index cdb19b43cf46..f9e85cceb9f8 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -779,7 +779,7 @@ if not isinstance(s, str): z = None # type: TNode # Same as TNode[Any] z.x -z.foo() # E: Some element of union has no attribute "foo" +z.foo() # E: Item Node[int] of "Union[Any, Node[int]]" has no attribute "foo" [builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index c47d25c3715f..825a5557fc63 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -548,7 +548,7 @@ v = A() # type: Union[A, B, C] if isinstance(v, (B, C)): v.method2(123) - v.method3('xyz') # E: Some element of union has no attribute "method3" + v.method3('xyz') # E: Item "B" of "Union[B, C]" has no attribute "method3" [builtins fixtures/isinstance.pyi] [case testIsinstanceNeverWidens] @@ -945,7 +945,7 @@ def bar() -> None: if isinstance(x, int): x + 1 else: - x.a # E: Some element of union has no attribute "a" + x.a # E: Item "str" of "Union[str, A]" has no attribute "a" x = 'a' [builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index e4fb2a16025c..6ae6df914376 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -534,7 +534,7 @@ x = None # type: ONode[int] x = f(1) x = f('x') # E: Argument 1 to "f" has incompatible type "str"; expected "int" -x.x = 1 # E: Some element of union has no attribute "x" +x.x = 1 # E: Item "None" of "Optional[Node[int]]" has no attribute "x" if x is not None: x.x = 1 # OK here @@ -572,7 +572,7 @@ A = None # type: Any class C(A): pass x = None # type: Optional[C] -x.foo() # E: Some element of union has no attribute "foo" +x.foo() # E: Item "None" of "Optional[C]" has no attribute "foo" [case testIsinstanceAndOptionalAndAnyBase] from typing import Any, Optional diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index f7774d5e5bcb..37a170e7367e 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -51,17 +51,24 @@ from typing import Union class A: y = 1 class B: y = 2 class C: pass +class D: pass +u = None # type: Union[A, C, D] +v = None # type: Union[C, D] w = None # type: Union[A, B] x = None # type: Union[A, C] y = None # type: int z = None # type: str y = w.y +v.y # E: Item "C" of "Union[C, D]" has no attribute "y" \ + # E: Item "D" of "Union[C, D]" has no attribute "y" +u.y # E: Item "C" of "Union[A, C, D]" has no attribute "y" \ + # E: Item "D" of "Union[A, C, D]" has no attribute "y" z = w.y # E: Incompatible types in assignment (expression has type "int", variable has type "str") w.y = 'a' # E: Incompatible types in assignment (expression has type "str", variable has type "int") -y = x.y # E: Some element of union has no attribute "y" -zz = x.y # E: Some element of union has no attribute "y" +y = x.y # E: Item "C" of "Union[A, C]" has no attribute "y" +zz = x.y # E: Item "C" of "Union[A, C]" has no attribute "y" z = zz # E: Incompatible types in assignment (expression has type "Union[int, Any]", variable has type "str") [builtins fixtures/isinstance.pyi] @@ -296,7 +303,8 @@ def foo(a: Union[A, B, C]): if isinstance(a, (B, C)): reveal_type(a) # E: Revealed type is 'Union[Tuple[builtins.int, fallback=__main__.B], Tuple[builtins.int, fallback=__main__.C]]' a.x - a.y # E: Some element of union has no attribute "y" + a.y # E: Item "B" of "Union[B, C]" has no attribute "y" \ + # E: Item "C" of "Union[B, C]" has no attribute "y" b = a # type: Union[B, C] [builtins fixtures/isinstance.pyi]