From b8c84cc79f278eee0520f2ada313687358a8c2f3 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 30 Aug 2018 22:44:30 -0700 Subject: [PATCH 1/9] Add support for operators with union operands This pull request resolves https://github.com/python/mypy/issues/2128 -- it modifies how we check operators to add support for operations like `Union[int, float] + Union[int, float]`. This approach basically iterates over all possible variations of the left and right operands when they're unions and uses the union of the resulting inferred type as the type of the overall expression. Some implementation notes: 1. I attempting "destructuring" just the left operand, which is basically the approach proposed here: https://github.com/python/mypy/issues/2128#issuecomment-398015973 Unfortunately, I discovered it became necessary to also destructure the right operand to handle certain edge cases -- see the testOperatorDoubleUnionInterwovenUnionAdd test case. 2. This algorithm varies slightly from what we do for union math in that we don't attempt to "preserve" the union/we always destructure both operands. I'm fairly confident that this is type-safe; I plan on testing this pull request against some internal code bases to help us gain more confidence. --- mypy/checker.py | 2 +- mypy/checkexpr.py | 78 +++++++++++----- mypy/messages.py | 6 ++ test-data/unit/check-callable.test | 9 +- test-data/unit/check-classes.test | 130 ++++++++++++++++++++++++++ test-data/unit/check-generics.test | 3 +- test-data/unit/check-incremental.test | 6 +- test-data/unit/check-isinstance.test | 108 ++++++++++++++------- test-data/unit/check-optional.test | 9 +- test-data/unit/check-unions.test | 9 +- test-data/unit/fixtures/ops.pyi | 5 +- 11 files changed, 292 insertions(+), 73 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 096ec7abade1..66712cbdba1f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2525,7 +2525,7 @@ def visit_operator_assignment_stmt(self, if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op( - method, lvalue_type, s.rvalue, s) + method, s.lvalue, lvalue_type, s.rvalue, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 743a77e74260..1c2db2eb67ea 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1697,7 +1697,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: if e.op in nodes.op_methods: method = self.get_operator_method(e.op) - result, method_type = self.check_op(method, left_type, e.right, e, + result, method_type = self.check_op(method, e.left, left_type, e.right, e, allow_reverse=True) e.method_type = method_type return result @@ -1749,7 +1749,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: sub_result = self.bool_type() elif operator in nodes.op_methods: method = self.get_operator_method(operator) - sub_result, method_type = self.check_op(method, left_type, right, e, + sub_result, method_type = self.check_op(method, left, left_type, right, e, allow_reverse=True) elif operator == 'is' or operator == 'is not': @@ -1820,19 +1820,12 @@ def check_op_reversible(self, left_expr: Expression, right_type: Type, right_expr: Expression, - context: Context) -> Tuple[Type, Type]: - # Note: this kludge exists mostly to maintain compatibility with - # existing error messages. Apparently, if the left-hand-side is a - # union and we have a type mismatch, we print out a special, - # abbreviated error message. (See messages.unsupported_operand_types). - unions_present = isinstance(left_type, UnionType) - + context: Context, + msg: MessageBuilder) -> Tuple[Type, Type]: def make_local_errors() -> MessageBuilder: """Creates a new MessageBuilder object.""" - local_errors = self.msg.clean_copy() + local_errors = msg.clean_copy() local_errors.disable_count = 0 - if unions_present: - local_errors.disable_type_names += 1 return local_errors def lookup_operator(op_name: str, base_type: Type) -> Optional[Type]: @@ -2009,9 +2002,9 @@ def lookup_definer(typ: Instance, attr_name: str) -> Optional[str]: # TODO: Remove this extra case return result - self.msg.add_errors(errors[0]) + msg.add_errors(errors[0]) if warn_about_uncalled_reverse_operator: - self.msg.reverse_operator_method_never_called( + msg.reverse_operator_method_never_called( nodes.op_methods_to_symbols[op_name], op_name, right_type, @@ -2025,8 +2018,8 @@ def lookup_definer(typ: Instance, attr_name: str) -> Optional[str]: result = error_any, error_any return result - def check_op(self, method: str, base_type: Type, arg: Expression, - context: Context, + def check_op(self, method: str, base_expr: Expression, base_type: Type, + arg: Expression, context: Context, allow_reverse: bool = False) -> Tuple[Type, Type]: """Type check a binary operation which maps to a method call. @@ -2034,13 +2027,48 @@ def check_op(self, method: str, base_type: Type, arg: Expression, """ if allow_reverse: - return self.check_op_reversible( - op_name=method, - left_type=base_type, - left_expr=TempNode(base_type), - right_type=self.accept(arg), - right_expr=arg, - context=context) + # Note: We want to pass in the original 'base_expr' and 'arg' for + # 'left_expr' and 'right_expr' whenever possible so that plugins + # and similar things can introspect on the original node if possible. + left_variants = [(base_type, base_expr)] + if isinstance(base_type, UnionType): + left_variants = [(item, TempNode(item)) for item in base_type.relevant_items()] + + right_type = self.accept(arg) + right_variants = [(right_type, arg)] + if isinstance(right_type, UnionType): + right_variants = [(item, TempNode(item)) for item in right_type.relevant_items()] + + msg = self.msg.clean_copy() + msg.disable_count = 0 + all_results = [] + all_inferred = [] + + for left_possible_type, left_expr in left_variants: + for right_possible_type, right_expr in right_variants: + result, inferred = self.check_op_reversible( + op_name=method, + left_type=left_possible_type, + left_expr=left_expr, + right_type=right_possible_type, + right_expr=right_expr, + context=context, + msg=msg) + all_results.append(result) + all_inferred.append(inferred) + + if msg.is_errors(): + self.msg.add_errors(msg) + if len(left_variants) >= 2 and len(right_variants) >= 2: + self.msg.warn_both_operands_are_from_unions(context) + elif len(left_variants) >= 2: + self.msg.warn_operand_was_from_union("Left", base_type, context) + elif len(right_variants) >= 2: + self.msg.warn_operand_was_from_union("Right", right_type, context) + + results_final = UnionType.make_simplified_union(all_results) + inferred_final = UnionType.make_simplified_union(all_inferred) + return results_final, inferred_final else: return self.check_op_local_by_name( method=method, @@ -2125,7 +2153,7 @@ def check_list_multiply(self, e: OpExpr) -> Type: left_type = self.accept(e.left, type_context=self.type_context[-1]) else: left_type = self.accept(e.left) - result, method_type = self.check_op('__mul__', left_type, e.right, e) + result, method_type = self.check_op('__mul__', e.left, left_type, e.right, e) e.method_type = method_type return result @@ -2179,7 +2207,7 @@ def visit_index_expr_helper(self, e: IndexExpr) -> Type: and left_type.is_type_obj() and left_type.type_object().is_enum): return self.visit_enum_index_expr(left_type.type_object(), e.index, e) else: - result, method_type = self.check_op('__getitem__', left_type, e.index, e) + result, method_type = self.check_op('__getitem__', e.base, left_type, e.index, e) e.method_type = method_type return result diff --git a/mypy/messages.py b/mypy/messages.py index 1b277c68f25a..6b721d4c8abc 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1019,6 +1019,12 @@ def reverse_operator_method_never_called(self, ), context=context) + def warn_both_operands_are_from_unions(self, context: Context) -> None: + self.note('Both left and right operands are unions', context) + + def warn_operand_was_from_union(self, side: str, original: Type, context: Context) -> None: + self.note('{} operand is of type {}'.format(side, self.format(original)), context) + def operator_method_signatures_overlap( self, reverse_class: TypeInfo, reverse_method: str, forward_class: Type, forward_method: str, context: Context) -> None: diff --git a/test-data/unit/check-callable.test b/test-data/unit/check-callable.test index 39e7bd77d60f..b7026207fd19 100644 --- a/test-data/unit/check-callable.test +++ b/test-data/unit/check-callable.test @@ -46,7 +46,8 @@ from typing import Callable, Union x = 5 # type: Union[int, Callable[[], str], Callable[[], int]] if callable(x): - y = x() + 2 # E: Unsupported operand types for + (likely involving Union) + y = x() + 2 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[str, int]" else: z = x + 6 @@ -60,7 +61,8 @@ x = 5 # type: Union[int, str, Callable[[], str]] if callable(x): y = x() + 'test' else: - z = x + 6 # E: Unsupported operand types for + (likely involving Union) + z = x + 6 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/callable.pyi] @@ -153,7 +155,8 @@ x = 5 # type: Union[int, Callable[[], str]] if callable(x) and x() == 'test': x() else: - x + 5 # E: Unsupported left operand type for + (some union) + x + 5 # E: Unsupported left operand type for + ("Callable[[], str]") \ + # N: Left operand is of type "Union[int, Callable[[], str]]" [builtins fixtures/callable.pyi] diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 54d43097ff30..30091556d69a 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2019,6 +2019,136 @@ class FractionChild(Fraction): pass class A(metaclass=Real): pass +[case testOperatorDoubleUnionIntFloat] +from typing import Union + +a: Union[int, float] +b: int +c: float + +reveal_type(a + a) # E: Revealed type is 'builtins.float' +reveal_type(a + b) # E: Revealed type is 'builtins.float' +reveal_type(b + a) # E: Revealed type is 'builtins.float' +reveal_type(a + c) # E: Revealed type is 'builtins.float' +reveal_type(c + a) # E: Revealed type is 'builtins.float' +[builtins fixtures/ops.pyi] + +[case testOperatorDoubleUnionStandardSubtyping] +from typing import Union + +class Parent: + def __add__(self, x: Parent) -> Parent: pass + def __radd__(self, x: Parent) -> Parent: pass + +class Child(Parent): + def __add__(self, x: Parent) -> Child: pass + def __radd__(self, x: Parent) -> Child: pass + +a: Union[Parent, Child] +b: Parent +c: Child + +reveal_type(a + a) # E: Revealed type is '__main__.Parent' +reveal_type(a + b) # E: Revealed type is '__main__.Parent' +reveal_type(b + a) # E: Revealed type is '__main__.Parent' +reveal_type(a + c) # E: Revealed type is '__main__.Child' +reveal_type(c + a) # E: Revealed type is '__main__.Child' + +[case testOperatorDoubleUnionNoRelationship1] +from typing import Union + +class Foo: + def __add__(self, x: Foo) -> Foo: pass + def __radd__(self, x: Foo) -> Foo: pass + +class Bar: + def __add__(self, x: Bar) -> Bar: pass + def __radd__(self, x: Bar) -> Bar: pass + +a: Union[Foo, Bar] +b: Foo +c: Bar + +a + a # E: Unsupported operand types for + ("Foo" and "Bar") \ + # E: Unsupported operand types for + ("Bar" and "Foo") \ + # N: Both left and right operands are unions + +a + b # E: Unsupported operand types for + ("Bar" and "Foo") \ + # N: Left operand is of type "Union[Foo, Bar]" + +b + a # E: Unsupported operand types for + ("Foo" and "Bar") \ + # N: Right operand is of type "Union[Foo, Bar]" + +a + c # E: Unsupported operand types for + ("Foo" and "Bar") \ + # N: Left operand is of type "Union[Foo, Bar]" + +c + a # E: Unsupported operand types for + ("Bar" and "Foo") \ + # N: Right operand is of type "Union[Foo, Bar]" + +[case testOperatorDoubleUnionNoRelationship2] +from typing import Union + +class Foo: + def __add__(self, x: Foo) -> Foo: pass + def __radd__(self, x: Foo) -> Foo: pass + +class Bar: + def __add__(self, x: Union[Foo, Bar]) -> Bar: pass + def __radd__(self, x: Union[Foo, Bar]) -> Bar: pass + +a: Union[Foo, Bar] +b: Foo +c: Bar + +reveal_type(a + a) # E: Revealed type is 'Union[__main__.Foo, __main__.Bar]' +reveal_type(a + b) # E: Revealed type is 'Union[__main__.Foo, __main__.Bar]' +reveal_type(b + a) # E: Revealed type is 'Union[__main__.Foo, __main__.Bar]' +reveal_type(a + c) # E: Revealed type is '__main__.Bar' +reveal_type(c + a) # E: Revealed type is '__main__.Bar' + +[case testOperatorDoubleUnionNaiveAdd] +from typing import Union + +class A: pass +class B: pass +class C: + def __radd__(self, x: A) -> int: pass +class D: + def __radd__(self, x: B) -> str: pass + +x: Union[A, B] +y: Union[C, D] + +x + y # E: Unsupported operand types for + ("A" and "D") \ + # E: Unsupported operand types for + ("B" and "C") \ + # N: Both left and right operands are unions + +[case testOperatorDoubleUnionInterwovenUnionAdd] +from typing import Union + +class Out1: pass +class Out2: pass +class Out3: pass +class Out4: pass + +class A: + def __add__(self, x: D) -> Out1: pass +class B: + def __add__(self, x: C) -> Out2: pass +class C: + def __radd__(self, x: A) -> Out3: pass +class D: + def __radd__(self, x: B) -> Out4: pass + +x: Union[A, B] +y: Union[C, D] + +reveal_type(x + y) # E: Revealed type is 'Union[__main__.Out3, __main__.Out1, __main__.Out2, __main__.Out4]' +reveal_type(A() + y) # E: Revealed type is 'Union[__main__.Out3, __main__.Out1]' +reveal_type(B() + y) # E: Revealed type is 'Union[__main__.Out2, __main__.Out4]' +reveal_type(x + C()) # E: Revealed type is 'Union[__main__.Out3, __main__.Out2]' +reveal_type(x + D()) # E: Revealed type is 'Union[__main__.Out1, __main__.Out4]' + [case testAbstractReverseOperatorMethod] import typing from abc import abstractmethod diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 3e9437c6a09f..e6fcda7ae6e2 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -755,7 +755,8 @@ class Node(Generic[T]): UNode = Union[int, Node[T]] x = 1 # type: UNode[int] -x + 1 # E: Unsupported left operand type for + (some union) +x + 1 # E: Unsupported left operand type for + ("Node[int]") \ + # N: Left operand is of type "Union[int, Node[int]]" if not isinstance(x, Node): x + 1 diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 926e05d249da..4c8fa30abe4d 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -3515,7 +3515,8 @@ from typing import Optional def foo() -> Optional[int]: return 0 [out1] [out2] -main:3: error: Unsupported operand types for + ("int" and "Optional[int]") +main:3: error: Unsupported operand types for + ("int" and "None") +main:3: note: Right operand is of type "Optional[int]" [case testAttrsIncrementalSubclassingCached] from a import A @@ -4082,7 +4083,8 @@ class Baz: return 1 [out] [out2] -tmp/a.py:3: error: Unsupported operand types for + ("int" and "Optional[int]") +tmp/a.py:3: error: Unsupported operand types for + ("int" and "None") +tmp/a.py:3: note: Right operand is of type "Optional[int]" [case testIncrementalMetaclassUpdate] import a diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index a7e08ba9a22b..3e155f25c0b8 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -183,7 +183,8 @@ def bar() -> None: while bool(): x + 'a' while bool(): - x + 'a' # E: Unsupported operand types for + (likely involving Union) + x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" x = foo() if bool(): continue @@ -409,12 +410,16 @@ def f(x: Union[List[int], List[str], int]) -> None: # type of a? reveal_type(x) # E: Revealed type is 'Union[builtins.list[builtins.int], builtins.list[builtins.str]]' - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("List[int]" and "int") \ + # E: Unsupported operand types for + ("List[str]" and "int") \ + # N: Left operand is of type "Union[List[int], List[str]]" else: x[0] # E: Value of type "int" is not indexable x + 1 x[0] # E: Value of type "Union[List[int], List[str], int]" is not indexable - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("List[int]" and "int") \ + # E: Unsupported operand types for + ("List[str]" and "int") \ + # N: Left operand is of type "Union[List[int], List[str], int]" [builtins fixtures/isinstancelist.pyi] [case testUnionListIsinstance2] @@ -445,7 +450,8 @@ x = foo() x = 1 x = x + 1 x = foo() -x = x + 1 # E: Unsupported operand types for + (likely involving Union) +x = x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" if isinstance(x, str): x = x + 1 # E: Unsupported operand types for + ("str" and "int") x = 1 @@ -509,8 +515,10 @@ while bool(): if isinstance(h.pet, Dog): if isinstance(h.pet.paws, str): x = h.pet.paws + 'a' - y = h.pet.paws + 1 # E: Unsupported operand types for + (likely involving Union) - z = h.pet.paws + 'a' # E: Unsupported operand types for + (likely involving Union) + y = h.pet.paws + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" + z = h.pet.paws + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" if isinstance(h.pet.paws, str): x = h.pet.paws + 'a' break @@ -652,7 +660,8 @@ def foo() -> None: break else: pass - y = x + 'asdad' # E: Unsupported operand types for + (likely involving Union) + y = x + 'asdad' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" foo() [builtins fixtures/isinstancelist.pyi] @@ -669,8 +678,13 @@ while bool(): x + 'a' else: x + [1] - x + 'a' # E: Unsupported operand types for + (likely involving Union) -x + [1] # E: Unsupported operand types for + (likely involving Union) + x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # E: Unsupported operand types for + ("List[int]" and "str") \ + # N: Left operand is of type "Union[int, str, List[int]]" + +x + [1] # E: Unsupported operand types for + ("int" and "List[int]") \ + # E: Unsupported operand types for + ("str" and "List[int]") \ + # N: Left operand is of type "Union[int, str, List[int]]" [builtins fixtures/isinstancelist.pyi] [case testIsInstanceThreeUnion2] @@ -685,7 +699,9 @@ while bool(): break x + [1] x + 'a' # E: Unsupported operand types for + ("List[int]" and "str") -x + [1] # E: Unsupported operand types for + (likely involving Union) +x + [1] # E: Unsupported operand types for + ("int" and "List[int]") \ + # E: Unsupported operand types for + ("str" and "List[int]") \ + # N: Left operand is of type "Union[int, str, List[int]]" [builtins fixtures/isinstancelist.pyi] [case testIsInstanceThreeUnion3] @@ -702,7 +718,9 @@ while bool(): break x + [1] # These lines aren't reached because x was an int x + 'a' -x + [1] # E: Unsupported operand types for + (likely involving Union) +x + [1] # E: Unsupported operand types for + ("int" and "List[int]") \ + # E: Unsupported operand types for + ("str" and "List[int]") \ + # N: Left operand is of type "Union[int, str, List[int]]" [builtins fixtures/isinstancelist.pyi] [case testRemovingTypeRepeatedly] @@ -712,24 +730,28 @@ def foo() -> Union[int, str]: pass for i in [1, 2]: x = foo() - x + 'a' # E: Unsupported operand types for + (likely involving Union) + x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" if isinstance(x, int): break x + 'a' x = foo() - x + 'a' # E: Unsupported operand types for + (likely involving Union) + x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" if isinstance(x, int): break x + 'a' x = foo() - x + 'a' # E: Unsupported operand types for + (likely involving Union) + x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" if isinstance(x, int): break x + 'a' -x + 'a' # E: Unsupported operand types for + (likely involving Union) +x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/isinstancelist.pyi] [case testModifyRepeatedly] @@ -738,8 +760,10 @@ from typing import Union def foo() -> Union[int, str]: pass x = foo() -x + 1 # E: Unsupported operand types for + (likely involving Union) -x + 'a' # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" +x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" x = 1 x + 1 @@ -750,8 +774,10 @@ x + 1 # E: Unsupported operand types for + ("str" and "int") x + 'a' x = foo() -x + 1 # E: Unsupported operand types for + (likely involving Union) -x + 'a' # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" +x + 'a' # E: Unsupported operand types for + ("int" and "str") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/isinstancelist.pyi] [case testModifyLoop] @@ -760,14 +786,16 @@ from typing import Union def foo() -> Union[int, str]: pass x = foo() -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 'a' x + 1 # E: Unsupported operand types for + ("str" and "int") x = 1 x + 1 while bool(): - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 'a' [builtins fixtures/isinstancelist.pyi] @@ -777,7 +805,8 @@ from typing import Union def foo() -> Union[int, str]: pass x = foo() -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 'a' x + 1 # E: Unsupported operand types for + ("str" and "int") x = 1 @@ -786,7 +815,8 @@ x + 1 for i in [1]: x = 'a' -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/isinstancelist.pyi] [case testModifyLoop3] @@ -803,7 +833,8 @@ while bool(): break else: x + 1 -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 1 for y in [1]: x + 1 @@ -811,7 +842,8 @@ for y in [1]: break else: x + 1 -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/isinstancelist.pyi] [case testModifyLoopWhile4] @@ -833,12 +865,14 @@ else: x + 'a' x = 1 while bool(): - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" if bool(): x = 'a' continue else: - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 'a' x + 'a' [builtins fixtures/isinstancelist.pyi] @@ -862,12 +896,14 @@ else: x + 'a' x = 1 for y in [1]: - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" if bool(): x = 'a' continue else: - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 'a' x + 'a' [builtins fixtures/isinstancelist.pyi] @@ -888,7 +924,8 @@ for y in [1]: break else: x + 1 -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" x = 1 while bool(): while bool(): @@ -898,7 +935,8 @@ while bool(): break else: x + 1 -x + 1 # E: Unsupported operand types for + (likely involving Union) +x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/isinstancelist.pyi] [case testModifyLoopLong] @@ -910,8 +948,9 @@ def foo() -> Union[int, str, A]: pass def bar() -> None: x = foo() - x + 1 # E: Unsupported left operand type for + (some union) \ - # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # E: Unsupported left operand type for + ("A") \ + # N: Left operand is of type "Union[int, str, A]" if isinstance(x, A): x.a else: @@ -1375,7 +1414,8 @@ from typing import Union def f(x: Union[int, str], typ: type) -> None: if isinstance(x, (typ, int)): - x + 1 # E: Unsupported operand types for + (likely involving Union) + x + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]' else: reveal_type(x) # E: Revealed type is 'builtins.str' diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 55345d11b996..9b1c7a694713 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -127,7 +127,8 @@ f(None) [case testInferOptionalFromDefaultNone] def f(x: int = None) -> None: - x + 1 # E: Unsupported left operand type for + (some union) + x + 1 # E: Unsupported left operand type for + ("None") \ + # N: Left operand is of type "Optional[int]" f(None) [out] @@ -140,7 +141,8 @@ def f(x: int = None) -> None: # E: Incompatible default for argument "x" (defau [case testInferOptionalFromDefaultNoneComment] def f(x=None): # type: (int) -> None - x + 1 # E: Unsupported left operand type for + (some union) + x + 1 # E: Unsupported left operand type for + ("None") \ + # N: Left operand is of type "Optional[int]" f(None) [out] @@ -437,7 +439,8 @@ from typing import Optional x = None # type: Optional[int] x + 1 [out] -tmp/a.py:3: error: Unsupported left operand type for + (some union) +tmp/a.py:3: error: Unsupported left operand type for + ("None") +tmp/a.py:3: note: Left operand is of type "Optional[str]" [case testNoneContextInference] from typing import Dict, List diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index 799dea119fc3..1cb274c2c4a8 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -98,7 +98,8 @@ i = y.foo() # E: Incompatible types in assignment (expression has type "Union[ from typing import Union, List x = None # type: Union[List[int], str] x[2] -x[2] + 1 # E: Unsupported operand types for + (likely involving Union) +x[2] + 1 # E: Unsupported operand types for + ("str" and "int") \ + # N: Left operand is of type "Union[int, str]" [builtins fixtures/isinstancelist.pyi] [case testUnionAsOverloadArg] @@ -289,8 +290,10 @@ reveal_type(u('', 1, 1)) # E: Revealed type is 'Union[builtins.int*, builtins.s from typing import Union class A: pass def f(x: Union[int, str, A]): - x + object() # E: Unsupported left operand type for + (some union) \ - # E: Unsupported operand types for + (likely involving Union) + x + object() # E: Unsupported operand types for + ("int" and "object") \ + # E: Unsupported operand types for + ("str" and "object") \ + # E: Unsupported left operand type for + ("A") \ + # N: Left operand is of type "Union[int, str, A]" [case testNarrowingDownNamedTupleUnion] from typing import NamedTuple, Union diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index 8f0eb8b2fcce..f6a85e2166b1 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -36,6 +36,7 @@ class unicode: pass class int: def __add__(self, x: 'int') -> 'int': pass + def __radd__(self, x: 'int') -> 'int': pass def __sub__(self, x: 'int') -> 'int': pass def __mul__(self, x: 'int') -> 'int': pass def __mod__(self, x: 'int') -> 'int': pass @@ -50,7 +51,9 @@ class int: def __gt__(self, x: 'int') -> bool: pass def __ge__(self, x: 'int') -> bool: pass -class float: pass +class float: + def __add__(self, x: 'float') -> 'float': pass + def __radd__(self, x: 'float') -> 'float': pass class BaseException: pass From 4fb3e15d809f5e64c66f38f8d6f4bbf92af81928 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 30 Aug 2018 23:22:30 -0700 Subject: [PATCH 2/9] Revert left expr propagation --- mypy/checker.py | 2 +- mypy/checkexpr.py | 27 +++++++++++++++------------ test-data/unit/check-classes.test | 13 +++++++++++++ test-data/unit/fixtures/floatdict.pyi | 4 ++++ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 66712cbdba1f..096ec7abade1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2525,7 +2525,7 @@ def visit_operator_assignment_stmt(self, if inplace: # There is __ifoo__, treat as x = x.__ifoo__(y) rvalue_type, method_type = self.expr_checker.check_op( - method, s.lvalue, lvalue_type, s.rvalue, s) + method, lvalue_type, s.rvalue, s) if not is_subtype(rvalue_type, lvalue_type): self.msg.incompatible_operator_assignment(s.op, s) else: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1c2db2eb67ea..65d69085af54 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1697,7 +1697,7 @@ def visit_op_expr(self, e: OpExpr) -> Type: if e.op in nodes.op_methods: method = self.get_operator_method(e.op) - result, method_type = self.check_op(method, e.left, left_type, e.right, e, + result, method_type = self.check_op(method, left_type, e.right, e, allow_reverse=True) e.method_type = method_type return result @@ -1749,7 +1749,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: sub_result = self.bool_type() elif operator in nodes.op_methods: method = self.get_operator_method(operator) - sub_result, method_type = self.check_op(method, left, left_type, right, e, + sub_result, method_type = self.check_op(method, left_type, right, e, allow_reverse=True) elif operator == 'is' or operator == 'is not': @@ -2018,7 +2018,7 @@ def lookup_definer(typ: Instance, attr_name: str) -> Optional[str]: result = error_any, error_any return result - def check_op(self, method: str, base_expr: Expression, base_type: Type, + def check_op(self, method: str, base_type: Type, arg: Expression, context: Context, allow_reverse: bool = False) -> Tuple[Type, Type]: """Type check a binary operation which maps to a method call. @@ -2027,13 +2027,16 @@ def check_op(self, method: str, base_expr: Expression, base_type: Type, """ if allow_reverse: - # Note: We want to pass in the original 'base_expr' and 'arg' for - # 'left_expr' and 'right_expr' whenever possible so that plugins - # and similar things can introspect on the original node if possible. - left_variants = [(base_type, base_expr)] + left_variants = [base_type] if isinstance(base_type, UnionType): - left_variants = [(item, TempNode(item)) for item in base_type.relevant_items()] + left_variants = [item for item in base_type.relevant_items()] + # Note: We want to pass in the original 'arg' for 'left_expr' and 'right_expr' + # whenever possible so that plugins and similar things can introspect on the original + # node if possible. + # + # We don't do the same for the base expression because it could lead to weird + # type inference errors -- e.g. see 'testOperatorDoubleUnionSum'. right_type = self.accept(arg) right_variants = [(right_type, arg)] if isinstance(right_type, UnionType): @@ -2044,12 +2047,12 @@ def check_op(self, method: str, base_expr: Expression, base_type: Type, all_results = [] all_inferred = [] - for left_possible_type, left_expr in left_variants: + for left_possible_type in left_variants: for right_possible_type, right_expr in right_variants: result, inferred = self.check_op_reversible( op_name=method, left_type=left_possible_type, - left_expr=left_expr, + left_expr=TempNode(left_possible_type), right_type=right_possible_type, right_expr=right_expr, context=context, @@ -2153,7 +2156,7 @@ def check_list_multiply(self, e: OpExpr) -> Type: left_type = self.accept(e.left, type_context=self.type_context[-1]) else: left_type = self.accept(e.left) - result, method_type = self.check_op('__mul__', e.left, left_type, e.right, e) + result, method_type = self.check_op('__mul__', left_type, e.right, e) e.method_type = method_type return result @@ -2207,7 +2210,7 @@ def visit_index_expr_helper(self, e: IndexExpr) -> Type: and left_type.is_type_obj() and left_type.type_object().is_enum): return self.visit_enum_index_expr(left_type.type_object(), e.index, e) else: - result, method_type = self.check_op('__getitem__', e.base, left_type, e.index, e) + result, method_type = self.check_op('__getitem__', left_type, e.index, e) e.method_type = method_type return result diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 30091556d69a..60352a7ad35e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2149,6 +2149,19 @@ reveal_type(B() + y) # E: Revealed type is 'Union[__main__.Out2, __main__.Out4] reveal_type(x + C()) # E: Revealed type is 'Union[__main__.Out3, __main__.Out2]' reveal_type(x + D()) # E: Revealed type is 'Union[__main__.Out1, __main__.Out4]' +[case testOperatorWithInference] +from typing import TypeVar, List, Iterable, Union + +T = TypeVar('T') +def sum(x: Iterable[T]) -> Union[T, int]: ... + +def len(x: Iterable[T]) -> int: ... + +x = [1.1, 2.2, 3.3] +reveal_type(sum(x)) # E: Revealed type is 'builtins.float*' +reveal_type(sum(x) / len(x)) # E: Revealed type is 'builtins.float' +[builtins fixtures/floatdict.pyi] + [case testAbstractReverseOperatorMethod] import typing from abc import abstractmethod diff --git a/test-data/unit/fixtures/floatdict.pyi b/test-data/unit/fixtures/floatdict.pyi index 54850d7f6e27..7db0c77b9058 100644 --- a/test-data/unit/fixtures/floatdict.pyi +++ b/test-data/unit/fixtures/floatdict.pyi @@ -55,9 +55,13 @@ class int: def __int__(self) -> int: ... def __mul__(self, x: int) -> int: ... def __rmul__(self, x: int) -> int: ... + def __truediv__(self, x: int) -> int: ... + def __rtruediv__(self, x: int) -> int: ... class float: def __float__(self) -> float: ... def __int__(self) -> int: ... def __mul__(self, x: float) -> float: ... def __rmul__(self, x: float) -> float: ... + def __truediv__(self, x: float) -> float: ... + def __rtruediv__(self, x: float) -> float: ... From 54dddc6e269ff92ff0f8a2e450f6fdbd861c18af Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 31 Aug 2018 00:07:01 -0700 Subject: [PATCH 3/9] Add staggered check --- mypy/checkexpr.py | 27 ++++++++++++++++++++++++++- test-data/unit/check-classes.test | 17 ++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 65d69085af54..47896fe5b05d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2030,6 +2030,32 @@ def check_op(self, method: str, base_type: Type, left_variants = [base_type] if isinstance(base_type, UnionType): left_variants = [item for item in base_type.relevant_items()] + right_type = self.accept(arg) + + # Step 1: We first try leaving the right arguments alone and destructure just the left ones + msg = self.msg.clean_copy() + msg.disable_count = 0 + all_results = [] + all_inferred = [] + + for left_possible_type in left_variants: + result, inferred = self.check_op_reversible( + op_name=method, + left_type=left_possible_type, + left_expr=TempNode(left_possible_type), + right_type=right_type, + right_expr=arg, + context=context, + msg=msg) + all_results.append(result) + all_inferred.append(inferred) + + if not msg.is_errors(): + results_final = UnionType.make_simplified_union(all_results) + inferred_final = UnionType.make_simplified_union(all_inferred) + return results_final, inferred_final + + # Step 2: If that fails, we try again but also destructure the right argument. # Note: We want to pass in the original 'arg' for 'left_expr' and 'right_expr' # whenever possible so that plugins and similar things can introspect on the original @@ -2037,7 +2063,6 @@ def check_op(self, method: str, base_type: Type, # # We don't do the same for the base expression because it could lead to weird # type inference errors -- e.g. see 'testOperatorDoubleUnionSum'. - right_type = self.accept(arg) right_variants = [(right_type, arg)] if isinstance(right_type, UnionType): right_variants = [(item, TempNode(item)) for item in right_type.relevant_items()] diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 60352a7ad35e..d0c23c7eed71 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2150,7 +2150,7 @@ reveal_type(x + C()) # E: Revealed type is 'Union[__main__.Out3, __main__.Out2] reveal_type(x + D()) # E: Revealed type is 'Union[__main__.Out1, __main__.Out4]' [case testOperatorWithInference] -from typing import TypeVar, List, Iterable, Union +from typing import TypeVar, Iterable, Union T = TypeVar('T') def sum(x: Iterable[T]) -> Union[T, int]: ... @@ -2162,6 +2162,21 @@ reveal_type(sum(x)) # E: Revealed type is 'builtins.float*' reveal_type(sum(x) / len(x)) # E: Revealed type is 'builtins.float' [builtins fixtures/floatdict.pyi] +[case testOperatorWithEmptyListAndSum] +from typing import TypeVar, Iterable, Union, overload + +T = TypeVar('T') +S = TypeVar('S') +@overload +def sum(x: Iterable[T]) -> Union[T, int]: ... +@overload +def sum(x: Iterable[T], default: S) -> Union[T, S]: ... +def sum(*args): pass + +x = ["a", "b", "c"] +reveal_type(x + sum([x, x, x], [])) # E: Revealed type is 'builtins.list[builtins.str*]' +[builtins fixtures/floatdict.pyi] + [case testAbstractReverseOperatorMethod] import typing from abc import abstractmethod From 49af0ae747f22c3062e7c59da6c2542e028599e2 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Fri, 31 Aug 2018 00:19:31 -0700 Subject: [PATCH 4/9] Fix lint errors --- mypy/checkexpr.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 47896fe5b05d..b50e221b79c3 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2032,7 +2032,9 @@ def check_op(self, method: str, base_type: Type, left_variants = [item for item in base_type.relevant_items()] right_type = self.accept(arg) - # Step 1: We first try leaving the right arguments alone and destructure just the left ones + # Step 1: We first try leaving the right arguments alone and destructure + # just the left ones. (Mypy can sometimes perform some more precise inference + # if we leave the right operands a union -- see testOperatorWithEmptyListAndSum. msg = self.msg.clean_copy() msg.disable_count = 0 all_results = [] @@ -2056,6 +2058,8 @@ def check_op(self, method: str, base_type: Type, return results_final, inferred_final # Step 2: If that fails, we try again but also destructure the right argument. + # This is also necessary to make certain edge cases work -- see + # testOperatorDoubleUnionInterwovenUnionAdd, for example. # Note: We want to pass in the original 'arg' for 'left_expr' and 'right_expr' # whenever possible so that plugins and similar things can introspect on the original From a0d906c3ba8e647e8004221bc5f685b64a02de60 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Tue, 4 Sep 2018 08:20:34 -0700 Subject: [PATCH 5/9] Change how we construct the inferred callable type --- mypy/checkexpr.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b50e221b79c3..e8f19b44d13b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1188,14 +1188,14 @@ def check_overload_call(self, # gives a narrower type. if unioned_return: returns, inferred_types = zip(*unioned_return) - # Note that we use `union_overload_matches` instead of just returning + # Note that we use `combine_function_signatures` instead of just returning # a union of inferred callables because for example a call # Union[int -> int, str -> str](Union[int, str]) is invalid and # we don't want to introduce internal inconsistencies. unioned_result = (UnionType.make_simplified_union(list(returns), context.line, context.column), - self.union_overload_matches(inferred_types)) + self.combine_function_signatures(inferred_types)) # Step 3: We try checking each branch one-by-one. inferred_result = self.infer_overload_return_type(plausible_targets, args, arg_types, @@ -1492,8 +1492,8 @@ def type_overrides_set(self, exprs: Sequence[Expression], for expr in exprs: del self.type_overrides[expr] - def union_overload_matches(self, types: Sequence[Type]) -> Union[AnyType, CallableType]: - """Accepts a list of overload signatures and attempts to combine them together into a + def combine_function_signatures(self, types: Sequence[Type]) -> Union[AnyType, CallableType]: + """Accepts a list of function signatures and attempts to combine them together into a new CallableType consisting of the union of all of the given arguments and return types. If there is at least one non-callable type, return Any (this can happen if there is @@ -1507,7 +1507,7 @@ def union_overload_matches(self, types: Sequence[Type]) -> Union[AnyType, Callab return callables[0] # Note: we are assuming here that if a user uses some TypeVar 'T' in - # two different overloads, they meant for that TypeVar to mean the + # two different functions, they meant for that TypeVar to mean the # same thing. # # This function will make sure that all instances of that TypeVar 'T' @@ -1525,7 +1525,7 @@ def union_overload_matches(self, types: Sequence[Type]) -> Union[AnyType, Callab too_complex = False for target in callables: - # We fall back to Callable[..., Union[]] if the overloads do not have + # We fall back to Callable[..., Union[]] if the functions do not have # the exact same signature. The only exception is if one arg is optional and # the other is positional: in that case, we continue unioning (and expect a # positional arg). @@ -2098,8 +2098,11 @@ def check_op(self, method: str, base_type: Type, elif len(right_variants) >= 2: self.msg.warn_operand_was_from_union("Right", right_type, context) + # See the comment in 'check_overload_call' for more details on why + # we call 'combine_function_signature' instead of just unioning the inferred + # callable types. results_final = UnionType.make_simplified_union(all_results) - inferred_final = UnionType.make_simplified_union(all_inferred) + inferred_final = self.combine_function_signatures(all_inferred) return results_final, inferred_final else: return self.check_op_local_by_name( From ca5e0acaebf43ede4cb4c82b0e77eedccc94b090 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Tue, 4 Sep 2018 08:54:17 -0700 Subject: [PATCH 6/9] WIP --- test-data/unit/check-classes.test | 19 +++++++++++++++++++ test-data/unit/fixtures/ops.pyi | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index d0c23c7eed71..23293c2278ad 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2149,6 +2149,25 @@ reveal_type(B() + y) # E: Revealed type is 'Union[__main__.Out2, __main__.Out4] reveal_type(x + C()) # E: Revealed type is 'Union[__main__.Out3, __main__.Out2]' reveal_type(x + D()) # E: Revealed type is 'Union[__main__.Out1, __main__.Out4]' +[case testOperatorDoubleUnionDivisionPython2] +# flags: --py2 +from typing import Union +def f(a): + # type: (Union[int, float]) -> None + a /= 1.1 + b = a / 1.1 + reveal_type(b) # E: Revealed type is 'builtins.float' +[builtins fixtures/ops.pyi] + +[case testOperatorDoubleUnionDivisionPython3] +from typing import Union +def f(a): + # type: (Union[int, float]) -> None + a /= 1.1 + b = a / 1.1 + reveal_type(b) # E: Revealed type is 'builtins.float' +[builtins fixtures/ops.pyi] + [case testOperatorWithInference] from typing import TypeVar, Iterable, Union diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index f6a85e2166b1..279e20c021ab 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -39,6 +39,10 @@ class int: def __radd__(self, x: 'int') -> 'int': pass def __sub__(self, x: 'int') -> 'int': pass def __mul__(self, x: 'int') -> 'int': pass + def __div__(self, x: 'int') -> 'int': pass + def __rdiv__(self, x: 'int') -> 'int': pass + def __truediv__(self, x: 'int') -> 'int': pass + def __rtruediv__(self, x: 'int') -> 'int': pass def __mod__(self, x: 'int') -> 'int': pass def __floordiv__(self, x: 'int') -> 'int': pass def __pow__(self, x: 'int') -> Any: pass @@ -54,6 +58,10 @@ class int: class float: def __add__(self, x: 'float') -> 'float': pass def __radd__(self, x: 'float') -> 'float': pass + def __div__(self, x: 'float') -> 'float': pass + def __rdiv__(self, x: 'float') -> 'float': pass + def __truediv__(self, x: 'float') -> 'float': pass + def __rtruediv__(self, x: 'float') -> 'float': pass class BaseException: pass From 4a3bda962a86e90416470b92e1fb4d1d3af24815 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Tue, 4 Sep 2018 13:02:40 -0700 Subject: [PATCH 7/9] Fix failing test --- test-data/unit/check-classes.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 23293c2278ad..cb01805d8e63 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2150,14 +2150,14 @@ reveal_type(x + C()) # E: Revealed type is 'Union[__main__.Out3, __main__.Out2] reveal_type(x + D()) # E: Revealed type is 'Union[__main__.Out1, __main__.Out4]' [case testOperatorDoubleUnionDivisionPython2] -# flags: --py2 +# flags: --python-version 2.7 from typing import Union def f(a): # type: (Union[int, float]) -> None a /= 1.1 b = a / 1.1 reveal_type(b) # E: Revealed type is 'builtins.float' -[builtins fixtures/ops.pyi] +[builtins_py2 fixtures/ops.pyi] [case testOperatorDoubleUnionDivisionPython3] from typing import Union From 7d92e70d862c827114c9b4a0141081c1d1b2dc4e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 5 Sep 2018 01:37:12 +0100 Subject: [PATCH 8/9] Add a little TODO item --- mypy/checkexpr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e8f19b44d13b..07abc7d50202 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2067,6 +2067,7 @@ def check_op(self, method: str, base_type: Type, # # We don't do the same for the base expression because it could lead to weird # type inference errors -- e.g. see 'testOperatorDoubleUnionSum'. + # TODO: Can we use `type_overrides_set()` here? right_variants = [(right_type, arg)] if isinstance(right_type, UnionType): right_variants = [(item, TempNode(item)) for item in right_type.relevant_items()] From 73c3e6792604d924fa748e14b91303f453341c5f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 5 Sep 2018 01:59:05 +0100 Subject: [PATCH 9/9] Try to fix a careless merge --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 02279abe90b3..211f183a4733 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1999,7 +1999,7 @@ def lookup_definer(typ: Instance, attr_name: str) -> Optional[str]: # TODO: Remove this extra case return result - self.msg.add_errors(errors[0]) + msg.add_errors(errors[0]) if len(results) == 1: return results[0] else: