From 69b42d6b8347794eba02ec2848e6dfd260049f39 Mon Sep 17 00:00:00 2001 From: David Fisher Date: Thu, 29 Sep 2016 17:03:10 -0700 Subject: [PATCH] Fix make_simplified_union interaction with Any (This is an updated version of #2197 by ddfisher.) make_simplified_union had two incorrect interactions with Any that this fixes: 1) Any unions containing Any were simplified to Any, which is incorrect. 2) Any classes that inherited from Any would be removed from the union by the subclass simplification (because subclasses of Any are considered subclasses of all other classes due to subtype/compatible with confusion). Note that `Union`s call make_union and not make_simplified_union, so this doesn't change their behavior directly (unless they interact with something else which causes them to be simplified, like being part of an `Optional`). --- mypy/checkmember.py | 6 +++ mypy/meet.py | 17 ++++-- mypy/messages.py | 3 ++ mypy/types.py | 19 +++++-- test-data/unit/check-dynamic-typing.test | 8 +-- test-data/unit/check-generics.test | 2 +- test-data/unit/check-optional.test | 23 ++++++++ test-data/unit/check-statements.test | 6 +-- test-data/unit/check-unions.test | 68 +++++++++++++++++++++++- 9 files changed, 133 insertions(+), 19 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index dd477cd87fbe..41cd1e97ba6d 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -107,11 +107,17 @@ def analyze_member_access(name: str, elif isinstance(typ, UnionType): # The base object has dynamic type. msg.disable_type_names += 1 + old_num_messages = msg.num_messages() results = [analyze_member_access(name, subtype, node, is_lvalue, is_super, is_operator, builtin_type, not_ready_callback, msg, original_type=original_type, chk=chk) for subtype in typ.items] msg.disable_type_names -= 1 + if msg.num_messages() != old_num_messages and any(isinstance(t, AnyType) + for t in results): + # If there was an error, return AnyType to avoid generating multiple messages for the + # same error. + return AnyType() return UnionType.make_simplified_union(results) elif isinstance(typ, TupleType): # Actually look up from the fallback instance type. diff --git a/mypy/meet.py b/mypy/meet.py index 7aa479c0eefc..26dc64d805c7 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -25,9 +25,19 @@ def meet_types(s: Type, t: Type) -> Type: return t.accept(TypeMeetVisitor(s)) -def meet_simple(s: Type, t: Type, default_right: bool = True) -> Type: +def meet_simple(s: Type, t: Type) -> Type: + """Return the type s "narrowed down" to t. + + Note that this is not symmetric with respect to s and t. + + TODO: Explain what this does in more detail and how this is + different from meet_types. + """ if s == t: return s + if isinstance(t, AnyType): + # Anything can be narrowed down to Any. + return t if isinstance(s, UnionType): return UnionType.make_simplified_union([meet_types(x, t) for x in s.items]) elif not is_overlapping_types(s, t, use_promotions=True): @@ -36,10 +46,7 @@ def meet_simple(s: Type, t: Type, default_right: bool = True) -> Type: else: return NoneTyp() else: - if default_right: - return t - else: - return s + return t def is_overlapping_types(t: Type, s: Type, use_promotions: bool = False) -> bool: diff --git a/mypy/messages.py b/mypy/messages.py index 53c46ac14332..8d58f6128dd8 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -141,6 +141,9 @@ def enable_errors(self) -> None: def is_errors(self) -> bool: return self.errors.is_errors() + def num_messages(self) -> int: + return self.errors.num_messages() + def report(self, msg: str, context: Context, severity: str, file: str = None, origin: Context = None) -> None: """Report an error or note (unless disabled).""" diff --git a/mypy/types.py b/mypy/types.py index 32d7c8340a6c..8afd76aa3893 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1030,17 +1030,28 @@ def make_simplified_union(items: List[Type], line: int = -1, column: int = -1) - all_items.append(typ) items = all_items - if any(isinstance(typ, AnyType) for typ in items): - return AnyType() - from mypy.subtypes import is_subtype + from mypy.sametypes import is_same_type + + def is_any_like(typ: Type) -> bool: + return (isinstance(typ, AnyType) or + (isinstance(typ, Instance) and typ.type.fallback_to_any)) + removed = set() # type: Set[int] for i, ti in enumerate(items): if i in removed: continue # Keep track of the truishness info for deleted subtypes which can be relevant cbt = cbf = False for j, tj in enumerate(items): - if i != j and is_subtype(tj, ti): + # Attempt to only combine true subtypes by avoiding types containing Any. + # TODO: Properly exclude generics and functions. + is_either_anylike = is_any_like(ti) or is_any_like(tj) + if (i != j + and is_subtype(tj, ti) + and (not is_either_anylike or + is_same_type(ti, tj) or + (isinstance(tj, NoneTyp) + and not experiments.STRICT_OPTIONAL))): removed.add(j) cbt = cbt or tj.can_be_true cbf = cbf or tj.can_be_false diff --git a/test-data/unit/check-dynamic-typing.test b/test-data/unit/check-dynamic-typing.test index 997a9961eed7..5b001fe2fb32 100644 --- a/test-data/unit/check-dynamic-typing.test +++ b/test-data/unit/check-dynamic-typing.test @@ -79,8 +79,8 @@ n = 0 d in a # E: Unsupported right operand type for in ("A") d and a d or a -c = d and b # Unintuitive type inference? -c = d or b # Unintuitive type inference? +c = d and b # E: Incompatible types in assignment (expression has type "Union[Any, bool]", variable has type "C") +c = d or b # E: Incompatible types in assignment (expression has type "Union[Any, bool]", variable has type "C") c = d + a c = d - a @@ -123,8 +123,8 @@ n = 0 a and d a or d c = a in d -c = b and d # Unintuitive type inference? -c = b or d # Unintuitive type inference? +c = b and d # E: Incompatible types in assignment (expression has type "Union[bool, Any]", variable has type "C") +c = b or d # E: Incompatible types in assignment (expression has type "Union[bool, Any]", variable has type "C") b = a + d b = a / d diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 225d66f421de..5de45ea06c8b 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -777,7 +777,7 @@ if not isinstance(s, str): z = None # type: TNode # Same as TNode[Any] z.x -z.foo() # Any simplifies Union to Any now. This test should be updated after #2197 +z.foo() # E: Some element of union has no attribute "foo" [builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index eca81bf8d37f..7ae1aa2f14d6 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -525,3 +525,26 @@ f = None # type: Optional[Callable[[int], None]] f = lambda x: None f(0) [builtins fixtures/function.pyi] + +[case testOptionalAndAnyBaseClass] +from typing import Any, Optional +class C(Any): + pass +x = None # type: Optional[C] +x.foo() # E: Some element of union has no attribute "foo" + +[case testUnionSimplificationWithStrictOptional] +from typing import Any, TypeVar, Union +class C(Any): pass +T = TypeVar('T') +S = TypeVar('S') +def u(x: T, y: S) -> Union[S, T]: pass +a = None # type: Any + +# Test both orders +reveal_type(u(C(), None)) # E: Revealed type is 'Union[builtins.None, __main__.C*]' +reveal_type(u(None, C())) # E: Revealed type is 'Union[__main__.C*, builtins.None]' +reveal_type(u(a, None)) # E: Revealed type is 'Union[builtins.None, Any]' +reveal_type(u(None, a)) # E: Revealed type is 'Union[Any, builtins.None]' +reveal_type(u(1, None)) # E: Revealed type is 'Union[builtins.None, builtins.int*]' +reveal_type(u(None, 1)) # E: Revealed type is 'Union[builtins.int*, builtins.None]' diff --git a/test-data/unit/check-statements.test b/test-data/unit/check-statements.test index b945fd1e8dad..0f1263e00234 100644 --- a/test-data/unit/check-statements.test +++ b/test-data/unit/check-statements.test @@ -630,11 +630,11 @@ try: except BaseException as e1: reveal_type(e1) # E: Revealed type is 'builtins.BaseException' except (E1, BaseException) as e2: - reveal_type(e2) # E: Revealed type is 'Any' + reveal_type(e2) # E: Revealed type is 'Union[Any, builtins.BaseException]' except (E1, E2) as e3: - reveal_type(e3) # E: Revealed type is 'Any' + reveal_type(e3) # E: Revealed type is 'Union[Any, __main__.E2]' except (E1, E2, BaseException) as e4: - reveal_type(e4) # E: Revealed type is 'Any' + reveal_type(e4) # E: Revealed type is 'Union[Any, builtins.BaseException]' try: pass except E1 as e1: diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index e1a30434bffe..38c7008d3fe7 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -37,7 +37,7 @@ def f(x: Union[int, str]) -> None: [case testUnionAnyIsInstance] from typing import Any, Union -def func(v:Union[int, Any]) -> None: +def func(v: Union[int, Any]) -> None: if isinstance(v, int): reveal_type(v) # E: Revealed type is 'builtins.int' else: @@ -61,7 +61,8 @@ y = w.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" -z = x.y # E: Some element of union has no attribute "y" +v = x.y # E: Some element of union has no attribute "y" +reveal_type(v) # E: Revealed type is 'Any' [builtins fixtures/isinstance.pyi] @@ -168,3 +169,66 @@ if foo(): def g(x: Union[int, str, bytes]) -> None: pass else: def g(x: Union[int, str]) -> None: pass # E: All conditional function variants must have identical signatures + +[case testUnionSimplificationSpecialCases] +from typing import Any, TypeVar, Union + +class C(Any): pass + +T = TypeVar('T') +S = TypeVar('S') +def u(x: T, y: S) -> Union[S, T]: pass + +a = None # type: Any + +# Base-class-Any and None, simplify +reveal_type(u(C(), None)) # E: Revealed type is '__main__.C*' +reveal_type(u(None, C())) # E: Revealed type is '__main__.C*' + +# Normal instance type and None, simplify +reveal_type(u(1, None)) # E: Revealed type is 'builtins.int*' +reveal_type(u(None, 1)) # E: Revealed type is 'builtins.int*' + +# Normal instance type and base-class-Any, no simplification +reveal_type(u(C(), 1)) # E: Revealed type is 'Union[builtins.int*, __main__.C*]' +reveal_type(u(1, C())) # E: Revealed type is 'Union[__main__.C*, builtins.int*]' + +# Normal instance type and Any, no simplification +reveal_type(u(1, a)) # E: Revealed type is 'Union[Any, builtins.int*]' +reveal_type(u(a, 1)) # E: Revealed type is 'Union[builtins.int*, Any]' + +# Any and base-class-Any, no simplificaiton +reveal_type(u(C(), a)) # E: Revealed type is 'Union[Any, __main__.C*]' +reveal_type(u(a, C())) # E: Revealed type is 'Union[__main__.C*, Any]' + +# Two normal instance types, simplify +reveal_type(u(1, object())) # E: Revealed type is 'builtins.object*' +reveal_type(u(object(), 1)) # E: Revealed type is 'builtins.object*' + +# Two normal instance types, no simplification +reveal_type(u(1, '')) # E: Revealed type is 'Union[builtins.str*, builtins.int*]' +reveal_type(u('', 1)) # E: Revealed type is 'Union[builtins.int*, builtins.str*]' + +[case testUnionSimplificationWithDuplicateItems] +from typing import Any, TypeVar, Union + +class C(Any): pass + +T = TypeVar('T') +S = TypeVar('S') +R = TypeVar('R') +def u(x: T, y: S, z: R) -> Union[R, S, T]: pass + +a = None # type: Any + +reveal_type(u(1, 1, 1)) # E: Revealed type is 'builtins.int*' +reveal_type(u(C(), C(), None)) # E: Revealed type is '__main__.C*' +reveal_type(u(a, a, 1)) # E: Revealed type is 'Union[builtins.int*, Any]' +reveal_type(u(a, C(), a)) # E: Revealed type is 'Union[Any, __main__.C*]' +reveal_type(u('', 1, 1)) # E: Revealed type is 'Union[builtins.int*, builtins.str*]' + +[case testUnionAndBinaryOperation] +from typing import Union +class A: pass +def f(x: Union[int, str, A]): + x + object() # E: Unsupported left operand type for + (some union)