Skip to content

Commit 20e41cb

Browse files
NurdokJukkaL
authored andcommitted
Add containing union and offending type information when reporting on missing attributes for unions (#3402)
Print the union in question when an attribute doesn't exist in one of its elements. Print the offending element in the Union for which the attribute does not exist. Fixes #2561.
1 parent 9cfbb0a commit 20e41cb

File tree

6 files changed

+53
-33
lines changed

6 files changed

+53
-33
lines changed

mypy/checkmember.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def analyze_member_access(name: str,
202202

203203
if chk and chk.should_suppress_optional_error([typ]):
204204
return AnyType()
205-
return msg.has_no_attr(original_type, name, node)
205+
return msg.has_no_attr(original_type, typ, name, node)
206206

207207

208208
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,
256256
else:
257257
if chk and chk.should_suppress_optional_error([itype]):
258258
return AnyType()
259-
return msg.has_no_attr(original_type, name, node)
259+
return msg.has_no_attr(original_type, itype, name, node)
260260

261261

262262
def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Context,

mypy/messages.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -361,72 +361,84 @@ def format_distinctly(self, type1: Type, type2: Type) -> Tuple[str, str]:
361361
# get some information as arguments, and they build an error message based
362362
# on them.
363363

364-
def has_no_attr(self, typ: Type, member: str, context: Context) -> Type:
364+
def has_no_attr(self, original_type: Type, typ: Type, member: str, context: Context) -> Type:
365365
"""Report a missing or non-accessible member.
366366
367-
The type argument is the base type. If member corresponds to
368-
an operator, use the corresponding operator name in the
369-
messages. Return type Any.
367+
original_type is the top-level type on which the error occurred.
368+
typ is the actual type that is missing the member. These can be
369+
different, e.g., in a union, original_type will be the union and typ
370+
will be the specific item in the union that does not have the member
371+
attribute.
372+
373+
If member corresponds to an operator, use the corresponding operator
374+
name in the messages. Return type Any.
370375
"""
371-
if (isinstance(typ, Instance) and
372-
typ.type.has_readable_member(member)):
376+
if (isinstance(original_type, Instance) and
377+
original_type.type.has_readable_member(member)):
373378
self.fail('Member "{}" is not assignable'.format(member), context)
374379
elif member == '__contains__':
375380
self.fail('Unsupported right operand type for in ({})'.format(
376-
self.format(typ)), context)
381+
self.format(original_type)), context)
377382
elif member in op_methods.values():
378383
# Access to a binary operator member (e.g. _add). This case does
379384
# not handle indexing operations.
380385
for op, method in op_methods.items():
381386
if method == member:
382-
self.unsupported_left_operand(op, typ, context)
387+
self.unsupported_left_operand(op, original_type, context)
383388
break
384389
elif member == '__neg__':
385390
self.fail('Unsupported operand type for unary - ({})'.format(
386-
self.format(typ)), context)
391+
self.format(original_type)), context)
387392
elif member == '__pos__':
388393
self.fail('Unsupported operand type for unary + ({})'.format(
389-
self.format(typ)), context)
394+
self.format(original_type)), context)
390395
elif member == '__invert__':
391396
self.fail('Unsupported operand type for ~ ({})'.format(
392-
self.format(typ)), context)
397+
self.format(original_type)), context)
393398
elif member == '__getitem__':
394399
# Indexed get.
395400
# TODO: Fix this consistently in self.format
396-
if isinstance(typ, CallableType) and typ.is_type_obj():
401+
if isinstance(original_type, CallableType) and original_type.is_type_obj():
397402
self.fail('The type {} is not generic and not indexable'.format(
398-
self.format(typ)), context)
403+
self.format(original_type)), context)
399404
else:
400405
self.fail('Value of type {} is not indexable'.format(
401-
self.format(typ)), context)
406+
self.format(original_type)), context)
402407
elif member == '__setitem__':
403408
# Indexed set.
404409
self.fail('Unsupported target for indexed assignment', context)
405410
elif member == '__call__':
406-
if isinstance(typ, Instance) and (typ.type.fullname() == 'builtins.function'):
411+
if isinstance(original_type, Instance) and \
412+
(original_type.type.fullname() == 'builtins.function'):
407413
# "'function' not callable" is a confusing error message.
408414
# Explain that the problem is that the type of the function is not known.
409415
self.fail('Cannot call function of unknown type', context)
410416
else:
411-
self.fail('{} not callable'.format(self.format(typ)), context)
417+
self.fail('{} not callable'.format(self.format(original_type)), context)
412418
else:
413419
# The non-special case: a missing ordinary attribute.
414420
if not self.disable_type_names:
415421
failed = False
416-
if isinstance(typ, Instance) and typ.type.names:
417-
alternatives = set(typ.type.names.keys())
422+
if isinstance(original_type, Instance) and original_type.type.names:
423+
alternatives = set(original_type.type.names.keys())
418424
matches = [m for m in COMMON_MISTAKES.get(member, []) if m in alternatives]
419425
matches.extend(best_matches(member, alternatives)[:3])
420426
if matches:
421427
self.fail('{} has no attribute "{}"; maybe {}?'.format(
422-
self.format(typ), member, pretty_or(matches)), context)
428+
self.format(original_type), member, pretty_or(matches)), context)
423429
failed = True
424430
if not failed:
425-
self.fail('{} has no attribute "{}"'.format(self.format(typ),
431+
self.fail('{} has no attribute "{}"'.format(self.format(original_type),
426432
member), context)
427-
else:
428-
self.fail('Some element of union has no attribute "{}"'.format(
429-
member), context)
433+
elif isinstance(original_type, UnionType):
434+
# The checker passes "object" in lieu of "None" for attribute
435+
# checks, so we manually convert it back.
436+
typ_format = self.format(typ)
437+
if typ_format == '"object"' and \
438+
any(type(item) == NoneTyp for item in original_type.items):
439+
typ_format = '"None"'
440+
self.fail('Item {} of {} has no attribute "{}"'.format(
441+
typ_format, self.format(original_type), member), context)
430442
return AnyType()
431443

432444
def unsupported_operand_types(self, op: str, left_type: Any,

test-data/unit/check-generics.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ if not isinstance(s, str):
778778

779779
z = None # type: TNode # Same as TNode[Any]
780780
z.x
781-
z.foo() # E: Some element of union has no attribute "foo"
781+
z.foo() # E: Item Node[int] of "Union[Any, Node[int]]" has no attribute "foo"
782782

783783
[builtins fixtures/isinstance.pyi]
784784

test-data/unit/check-isinstance.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ v = A() # type: Union[A, B, C]
548548

549549
if isinstance(v, (B, C)):
550550
v.method2(123)
551-
v.method3('xyz') # E: Some element of union has no attribute "method3"
551+
v.method3('xyz') # E: Item "B" of "Union[B, C]" has no attribute "method3"
552552
[builtins fixtures/isinstance.pyi]
553553

554554
[case testIsinstanceNeverWidens]
@@ -945,7 +945,7 @@ def bar() -> None:
945945
if isinstance(x, int):
946946
x + 1
947947
else:
948-
x.a # E: Some element of union has no attribute "a"
948+
x.a # E: Item "str" of "Union[str, A]" has no attribute "a"
949949
x = 'a'
950950

951951
[builtins fixtures/isinstancelist.pyi]

test-data/unit/check-optional.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ x = None # type: ONode[int]
534534
x = f(1)
535535
x = f('x') # E: Argument 1 to "f" has incompatible type "str"; expected "int"
536536

537-
x.x = 1 # E: Some element of union has no attribute "x"
537+
x.x = 1 # E: Item "None" of "Optional[Node[int]]" has no attribute "x"
538538
if x is not None:
539539
x.x = 1 # OK here
540540

@@ -572,7 +572,7 @@ A = None # type: Any
572572
class C(A):
573573
pass
574574
x = None # type: Optional[C]
575-
x.foo() # E: Some element of union has no attribute "foo"
575+
x.foo() # E: Item "None" of "Optional[C]" has no attribute "foo"
576576

577577
[case testIsinstanceAndOptionalAndAnyBase]
578578
from typing import Any, Optional

test-data/unit/check-unions.test

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,24 @@ from typing import Union
5151
class A: y = 1
5252
class B: y = 2
5353
class C: pass
54+
class D: pass
5455

56+
u = None # type: Union[A, C, D]
57+
v = None # type: Union[C, D]
5558
w = None # type: Union[A, B]
5659
x = None # type: Union[A, C]
5760
y = None # type: int
5861
z = None # type: str
5962

6063
y = w.y
64+
v.y # E: Item "C" of "Union[C, D]" has no attribute "y" \
65+
# E: Item "D" of "Union[C, D]" has no attribute "y"
66+
u.y # E: Item "C" of "Union[A, C, D]" has no attribute "y" \
67+
# E: Item "D" of "Union[A, C, D]" has no attribute "y"
6168
z = w.y # E: Incompatible types in assignment (expression has type "int", variable has type "str")
6269
w.y = 'a' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
63-
y = x.y # E: Some element of union has no attribute "y"
64-
zz = x.y # E: Some element of union has no attribute "y"
70+
y = x.y # E: Item "C" of "Union[A, C]" has no attribute "y"
71+
zz = x.y # E: Item "C" of "Union[A, C]" has no attribute "y"
6572
z = zz # E: Incompatible types in assignment (expression has type "Union[int, Any]", variable has type "str")
6673

6774
[builtins fixtures/isinstance.pyi]
@@ -296,7 +303,8 @@ def foo(a: Union[A, B, C]):
296303
if isinstance(a, (B, C)):
297304
reveal_type(a) # E: Revealed type is 'Union[Tuple[builtins.int, fallback=__main__.B], Tuple[builtins.int, fallback=__main__.C]]'
298305
a.x
299-
a.y # E: Some element of union has no attribute "y"
306+
a.y # E: Item "B" of "Union[B, C]" has no attribute "y" \
307+
# E: Item "C" of "Union[B, C]" has no attribute "y"
300308
b = a # type: Union[B, C]
301309
[builtins fixtures/isinstance.pyi]
302310

0 commit comments

Comments
 (0)