Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add containing union and offending type information when reporting on missing attributes for unions #3402

Merged
merged 12 commits into from May 23, 2017
4 changes: 2 additions & 2 deletions mypy/checkmember.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
58 changes: 35 additions & 23 deletions mypy/messages.py
Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document original_type in the docstring.

"""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,
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-generics.test
Expand Up @@ -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]

Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-isinstance.test
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-optional.test
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions test-data/unit/check-unions.test
Expand Up @@ -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]
Expand Down Expand Up @@ -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]

Expand Down