From 5800819506ac2aec765d88126f5a82d6c6709d75 Mon Sep 17 00:00:00 2001 From: elazar Date: Sun, 23 Apr 2017 18:59:52 +0300 Subject: [PATCH 01/13] reverse the subtyping test, more explicit error message, unify cases --- mypy/checkmember.py | 29 +++++++++++------------------ mypy/messages.py | 8 +++----- test-data/unit/check-classes.test | 2 +- test-data/unit/check-functions.test | 6 +++--- test-data/unit/check-selftype.test | 7 +++++-- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index e5a1bdb837b8..545f33ad7503 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -338,33 +338,26 @@ def lookup_member_var_or_accessor(info: TypeInfo, name: str, return None -def check_method_type(functype: FunctionLike, itype: Instance, is_classmethod: bool, +def check_method_type(functype: FunctionLike, original_type: Instance, is_classmethod: bool, context: Context, msg: MessageBuilder) -> None: for item in functype.items(): if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): # No positional first (self) argument (*args is okay). - msg.invalid_method_type(item, context) - elif not is_classmethod: - # Check that self argument has type 'Any' or valid instance type. + msg.fail('Attribute function of type %s does not accept self argument' % msg.format(item), context) + else: + # Check that self argument has type 'Any' or valid instance/class type. selfarg = item.arg_types[0] + # (This is sufficient for the current treatment of @classmethod, + # but probably needs to be revisited when we implement Type[C] + # or advanced variants of it like Type[, C].) + if is_classmethod and isinstance(selfarg, CallableType) and selfarg.is_type_obj(): + selfarg = selfarg.ret_type # If this is a method of a tuple class, correct for the fact that # we passed to typ.fallback in analyze_member_access. See #1432. if isinstance(selfarg, TupleType): selfarg = selfarg.fallback - if not subtypes.is_subtype(selfarg, itype): - msg.invalid_method_type(item, context) - else: - # Check that cls argument has type 'Any' or valid class type. - # (This is sufficient for the current treatment of @classmethod, - # but probably needs to be revisited when we implement Type[C] - # or advanced variants of it like Type[, C].) - clsarg = item.arg_types[0] - if isinstance(clsarg, CallableType) and clsarg.is_type_obj(): - if not subtypes.is_equivalent(clsarg.ret_type, itype): - msg.invalid_class_method_type(item, context) - else: - if not subtypes.is_equivalent(clsarg, AnyType()): - msg.invalid_class_method_type(item, context) + if not subtypes.is_subtype(original_type, erase_to_bound(selfarg)): + msg.invalid_method_type(original_type, item, is_classmethod, context) def analyze_class_attribute_access(itype: Instance, diff --git a/mypy/messages.py b/mypy/messages.py index 78e17686d057..de612f08bbc7 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -768,11 +768,9 @@ def cannot_determine_type(self, name: str, context: Context) -> None: def cannot_determine_type_in_base(self, name: str, base: str, context: Context) -> None: self.fail("Cannot determine type of '%s' in base class '%s'" % (name, base), context) - def invalid_method_type(self, sig: CallableType, context: Context) -> None: - self.fail('Invalid method type', context) - - def invalid_class_method_type(self, sig: CallableType, context: Context) -> None: - self.fail('Invalid class method type', context) + def invalid_method_type(self, arg: Type, sig: CallableType, is_classmethod: bool, context: Context) -> None: + kind = 'class attribute function' if is_classmethod else 'attribute function' + self.fail('Invalid self argument %s to %s %s' % (self.format(arg), kind, self.format(sig)), context) def incompatible_conditional_function_def(self, defn: FuncDef) -> None: self.fail('All conditional function variants must have identical ' diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 6cb42d70eb86..94a9ab620e22 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1844,7 +1844,7 @@ class B: a = A bad = lambda: 42 -B().bad() # E: Invalid method type +B().bad() # E: Attribute function of type Callable[[], int] does not accept self argument reveal_type(B.a) # E: Revealed type is 'def () -> __main__.A' reveal_type(B().a) # E: Revealed type is 'def () -> __main__.A' reveal_type(B().a()) # E: Revealed type is '__main__.A' diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 33fc51d3b046..3e1fb9214cce 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -469,8 +469,8 @@ class A: f = x # type: Callable[[], None] g = x # type: Callable[[B], None] a = None # type: A -a.f() # E: Invalid method type -a.g() # E: Invalid method type +a.f() # E: Attribute function of type Callable[[], None] does not accept self argument +a.g() # E: Invalid self argument "A" to attribute function Callable[[B], None] [case testMethodWithDynamicallyTypedMethodAsDataAttribute] from typing import Any, Callable @@ -530,7 +530,7 @@ class A(Generic[t]): ab = None # type: A[B] ac = None # type: A[C] ab.f() -ac.f() # E: Invalid method type +ac.f() # E: Invalid self argument A[C] to attribute function Callable[[A[B]], None] [case testPartiallyTypedSelfInMethodDataAttribute] from typing import Any, TypeVar, Generic, Callable diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index c82e619698c9..96adb9bdd019 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -342,9 +342,12 @@ class E: [case testSelfTypeProperty] from typing import TypeVar -T = TypeVar('T', bound='A') +T = TypeVar('T', bound='X') -class A: +class X: + pass + +class A(X): @property def member(self: T) -> T: pass From a67b78c1f7a3f4e866f55c4db9a6309747cbb8bb Mon Sep 17 00:00:00 2001 From: elazar Date: Sun, 23 Apr 2017 19:33:41 +0300 Subject: [PATCH 02/13] reverse the direction of type fallback, avoid tuple fallback --- mypy/checkmember.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 545f33ad7503..2356d2f0202c 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -286,7 +286,7 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont # methods: the former to the instance, the latter to the # class. functype = t - check_method_type(functype, itype, var.is_classmethod, node, msg) + check_method_type(functype, original_type, var.is_classmethod, node, msg) signature = bind_self(functype, original_type) if var.is_property: # A property cannot have an overloaded type => the cast @@ -350,12 +350,8 @@ def check_method_type(functype: FunctionLike, original_type: Instance, is_classm # (This is sufficient for the current treatment of @classmethod, # but probably needs to be revisited when we implement Type[C] # or advanced variants of it like Type[, C].) - if is_classmethod and isinstance(selfarg, CallableType) and selfarg.is_type_obj(): - selfarg = selfarg.ret_type - # If this is a method of a tuple class, correct for the fact that - # we passed to typ.fallback in analyze_member_access. See #1432. - if isinstance(selfarg, TupleType): - selfarg = selfarg.fallback + if is_classmethod: + original_type = TypeType(original_type) if not subtypes.is_subtype(original_type, erase_to_bound(selfarg)): msg.invalid_method_type(original_type, item, is_classmethod, context) From a523b1fa09e8af9e8b6e870a0e4243d723f52bd3 Mon Sep 17 00:00:00 2001 From: elazar Date: Sun, 23 Apr 2017 19:55:39 +0300 Subject: [PATCH 03/13] lint --- mypy/checkmember.py | 5 +++-- mypy/messages.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2356d2f0202c..eb86822cba76 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -338,12 +338,13 @@ def lookup_member_var_or_accessor(info: TypeInfo, name: str, return None -def check_method_type(functype: FunctionLike, original_type: Instance, is_classmethod: bool, +def check_method_type(functype: FunctionLike, original_type: Type, is_classmethod: bool, context: Context, msg: MessageBuilder) -> None: for item in functype.items(): if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): # No positional first (self) argument (*args is okay). - msg.fail('Attribute function of type %s does not accept self argument' % msg.format(item), context) + msg.fail('Attribute function of type %s does not accept self argument' + % msg.format(item), context) else: # Check that self argument has type 'Any' or valid instance/class type. selfarg = item.arg_types[0] diff --git a/mypy/messages.py b/mypy/messages.py index de612f08bbc7..f4e776696816 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -768,9 +768,11 @@ def cannot_determine_type(self, name: str, context: Context) -> None: def cannot_determine_type_in_base(self, name: str, base: str, context: Context) -> None: self.fail("Cannot determine type of '%s' in base class '%s'" % (name, base), context) - def invalid_method_type(self, arg: Type, sig: CallableType, is_classmethod: bool, context: Context) -> None: + def invalid_method_type(self, arg: Type, sig: CallableType, is_classmethod: bool, + context: Context) -> None: kind = 'class attribute function' if is_classmethod else 'attribute function' - self.fail('Invalid self argument %s to %s %s' % (self.format(arg), kind, self.format(sig)), context) + self.fail('Invalid self argument %s to %s %s' + % (self.format(arg), kind, self.format(sig)), context) def incompatible_conditional_function_def(self, defn: FuncDef) -> None: self.fail('All conditional function variants must have identical ' From 78d08875884234f100664817799e0a259aa20817 Mon Sep 17 00:00:00 2001 From: elazar Date: Mon, 24 Apr 2017 01:19:54 +0300 Subject: [PATCH 04/13] check access to reversed methods only per-item --- mypy/checker.py | 18 ++++++++++-------- mypy/types.py | 9 ++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 488f3185e897..656c121fa3d8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -782,8 +782,9 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, # TODO check self argument kind # Check for the issue described above. - arg_type = typ.arg_types[1] other_method = nodes.normal_from_reverse_op[method] + arg_type = typ.arg_types[1] + arg_types = [arg_type] if isinstance(arg_type, Instance): if not arg_type.type.has_readable_member(other_method): return @@ -792,15 +793,16 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, elif isinstance(arg_type, UnionType): if not arg_type.has_readable_member(other_method): return + arg_types = list(arg_type.iter_deep()) else: return - - typ2 = self.expr_checker.analyze_external_member_access( - other_method, arg_type, defn) - self.check_overlapping_op_methods( - typ, method, defn.info, - typ2, other_method, cast(Instance, arg_type), - defn) + for arg_type in arg_types: + typ2 = self.expr_checker.analyze_external_member_access( + other_method, arg_type, defn) + self.check_overlapping_op_methods( + typ, method, defn.info, + typ2, other_method, cast(Instance, arg_type), + defn) def check_overlapping_op_methods(self, reverse_type: CallableType, diff --git a/mypy/types.py b/mypy/types.py index 9f9c6c1eb393..39c67e127fe3 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -5,7 +5,7 @@ from collections import OrderedDict from typing import ( Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Sequence, Optional, Union, Iterable, - NamedTuple, Callable, + NamedTuple, Callable, Iterator, ) import mypy.nodes @@ -1062,6 +1062,13 @@ def has_readable_member(self, name: str) -> bool: (isinstance(x, Instance) and x.type.has_readable_member(name)) for x in self.items) + def iter_deep(self) -> Iterator[Type]: + for x in self.items: + if isinstance(x, UnionType): + yield from x.iter_deep() + else: + yield x + def serialize(self) -> JsonDict: return {'.class': 'UnionType', 'items': [t.serialize() for t in self.items], From 3c8e85f2a7a947eed452a7489cf9fb3d3f62ea4d Mon Sep 17 00:00:00 2001 From: elazar Date: Mon, 24 Apr 2017 01:42:37 +0300 Subject: [PATCH 05/13] comment --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index 656c121fa3d8..070f5501a987 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -796,6 +796,7 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, arg_types = list(arg_type.iter_deep()) else: return + # We check that each method is fine when dispatched on a proper self argument for arg_type in arg_types: typ2 = self.expr_checker.analyze_external_member_access( other_method, arg_type, defn) From 670381603d600385260c9ed790deb2880ebd870c Mon Sep 17 00:00:00 2001 From: elazar Date: Tue, 25 Jul 2017 04:44:09 +0300 Subject: [PATCH 06/13] rephrase error message, use union_items --- mypy/checker.py | 2 +- mypy/checkmember.py | 2 +- mypy/types.py | 7 ------- test-data/unit/check-classes.test | 2 +- test-data/unit/check-functions.test | 2 +- 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f50d593d21b3..f29037ff80ad 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -828,7 +828,7 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, elif isinstance(arg_type, UnionType): if not arg_type.has_readable_member(other_method): return - arg_types = list(arg_type.iter_deep()) + arg_types = list(union_items(arg_type)) else: return # We check that each method is fine when dispatched on a proper self argument diff --git a/mypy/checkmember.py b/mypy/checkmember.py index cecac4525c81..2f2c26fe0030 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -373,7 +373,7 @@ def check_method_type(functype: FunctionLike, original_type: Type, is_classmetho for item in functype.items(): if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): # No positional first (self) argument (*args is okay). - msg.fail('Attribute function of type %s does not accept self argument' + msg.fail('Attribute function with type %s does not accept self argument' % msg.format(item), context) else: # Check that self argument has type 'Any' or valid instance/class type. diff --git a/mypy/types.py b/mypy/types.py index 1fcacb6a54b2..d720b49ca7f5 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1130,13 +1130,6 @@ def relevant_items(self) -> List[Type]: else: return [i for i in self.items if not isinstance(i, NoneTyp)] - def iter_deep(self) -> Iterator[Type]: - for x in self.items: - if isinstance(x, UnionType): - yield from x.iter_deep() - else: - yield x - def serialize(self) -> JsonDict: return {'.class': 'UnionType', 'items': [t.serialize() for t in self.items], diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 34f9aaebf180..6735cf361475 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1975,7 +1975,7 @@ class B: a = A bad = lambda: 42 -B().bad() # E: Attribute function of type Callable[[], int] does not accept self argument +B().bad() # E: Attribute function with type Callable[[], int] does not accept self argument reveal_type(B.a) # E: Revealed type is 'def () -> __main__.A' reveal_type(B().a) # E: Revealed type is 'def () -> __main__.A' reveal_type(B().a()) # E: Revealed type is '__main__.A' diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 298d477ab542..dae9d2665075 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -470,7 +470,7 @@ class A: f = x # type: Callable[[], None] g = x # type: Callable[[B], None] a = None # type: A -a.f() # E: Attribute function of type Callable[[], None] does not accept self argument +a.f() # E: Attribute function with type Callable[[], None] does not accept self argument a.g() # E: Invalid self argument "A" to attribute function Callable[[B], None] [case testMethodWithDynamicallyTypedMethodAsDataAttribute] From d7a41f913aaae93d83530497e6cece8fc57aa992 Mon Sep 17 00:00:00 2001 From: elazar Date: Tue, 25 Jul 2017 14:34:53 +0300 Subject: [PATCH 07/13] add test with an unbound typevar --- test-data/unit/check-selftype.test | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index e8787a6adc2c..9bbbcb629e32 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -354,19 +354,21 @@ class E: [case testSelfTypeProperty] from typing import TypeVar +Q = TypeVar('Q') T = TypeVar('T', bound='X') class X: - pass + @property + def __members__(self: Q) -> Q: return self class A(X): @property - def member(self: T) -> T: - pass + def member(self: T) -> T: return self class B(A): pass +reveal_type(X().__members__) # E: Revealed type is '__main__.X*' reveal_type(A().member) # E: Revealed type is '__main__.A*' reveal_type(B().member) # E: Revealed type is '__main__.B*' From 1c58d49a59726a72ce0a072f72dc1b4f043b37c5 Mon Sep 17 00:00:00 2001 From: elazar Date: Tue, 25 Jul 2017 14:39:36 +0300 Subject: [PATCH 08/13] Use TypeType.make_normalized --- mypy/checkmember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 2f2c26fe0030..0edb917c95d0 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -382,7 +382,7 @@ def check_method_type(functype: FunctionLike, original_type: Type, is_classmetho # but probably needs to be revisited when we implement Type[C] # or advanced variants of it like Type[, C].) if is_classmethod: - original_type = TypeType(original_type) + original_type = TypeType.make_normalized(original_type) if not subtypes.is_subtype(original_type, erase_to_bound(selfarg)): msg.invalid_method_type(original_type, item, is_classmethod, context) From 71faee971a564d32e9328c3c930c763fc2115658 Mon Sep 17 00:00:00 2001 From: elazar Date: Sun, 24 Sep 2017 21:04:49 +0300 Subject: [PATCH 09/13] add docstrings and comments --- mypy/checker.py | 4 +++- mypy/checkmember.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f6647d435228..2fe793ef4892 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -847,7 +847,9 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, arg_types = list(union_items(arg_type)) else: return - # We check that each method is fine when dispatched on a proper self argument + # `info.get_method` in `analyze_member_access` does not handle decorated + # functions properly, so we perform it for each item of the union separately. + # see #3227 for arg_type in arg_types: typ2 = self.expr_checker.analyze_external_member_access( other_method, arg_type, defn) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 06d4684fa131..01e92443e0ed 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -314,7 +314,7 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont # methods: the former to the instance, the latter to the # class. functype = t - check_method_type(functype, original_type, var.is_classmethod, node, msg) + check_self_arg(functype, original_type, var.is_classmethod, node, msg) signature = bind_self(functype, original_type, var.is_classmethod) if var.is_property: # A property cannot have an overloaded type => the cast @@ -370,19 +370,18 @@ def lookup_member_var_or_accessor(info: TypeInfo, name: str, return None -def check_method_type(functype: FunctionLike, original_type: Type, is_classmethod: bool, - context: Context, msg: MessageBuilder) -> None: +def check_self_arg(functype: FunctionLike, original_type: Type, is_classmethod: bool, + context: Context, msg: MessageBuilder) -> None: + """Check that the the most precise type of the self argument is compatible + with the declared type of each of the overloads. + """ for item in functype.items(): if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): # No positional first (self) argument (*args is okay). msg.fail('Attribute function with type %s does not accept self argument' % msg.format(item), context) else: - # Check that self argument has type 'Any' or valid instance/class type. selfarg = item.arg_types[0] - # (This is sufficient for the current treatment of @classmethod, - # but probably needs to be revisited when we implement Type[C] - # or advanced variants of it like Type[, C].) if is_classmethod: original_type = TypeType.make_normalized(original_type) if not subtypes.is_subtype(original_type, erase_to_bound(selfarg)): @@ -468,7 +467,7 @@ class B(A): pass info = itype.type # type: TypeInfo if isinstance(t, CallableType): # TODO: Should we propagate type variable values? - tvars = [TypeVarDef(n, i + 1, [], builtin_type('builtins.object'), tv.variance) + tvars = [TypeVarDef(n, i + 1, [], tv.upper_bound, tv.variance) for (i, n), tv in zip(enumerate(info.type_vars), info.defn.type_vars)] if is_classmethod: t = bind_self(t, original_type, is_classmethod=True) From 2108a0350b485a441859a28c1cf82ebb51c9553c Mon Sep 17 00:00:00 2001 From: elazar Date: Sun, 24 Sep 2017 21:51:50 +0300 Subject: [PATCH 10/13] revert uncalled-for fix --- mypy/checkmember.py | 2 +- mypy/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 01e92443e0ed..be0dae884b2d 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -467,7 +467,7 @@ class B(A): pass info = itype.type # type: TypeInfo if isinstance(t, CallableType): # TODO: Should we propagate type variable values? - tvars = [TypeVarDef(n, i + 1, [], tv.upper_bound, tv.variance) + tvars = [TypeVarDef(n, i + 1, [], builtin_type('builtins.object'), tv.variance) for (i, n), tv in zip(enumerate(info.type_vars), info.defn.type_vars)] if is_classmethod: t = bind_self(t, original_type, is_classmethod=True) diff --git a/mypy/types.py b/mypy/types.py index d3a1de678413..a53085a66a36 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -5,7 +5,7 @@ from collections import OrderedDict from typing import ( Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Optional, Union, Iterable, NamedTuple, - Callable, + Callable ) import mypy.nodes From a8a0386798a69ba203632c5b43541799e910558a Mon Sep 17 00:00:00 2001 From: elazar Date: Tue, 26 Sep 2017 20:30:39 +0300 Subject: [PATCH 11/13] use meet to simulate dispatch; cleanup --- mypy/checker.py | 29 ++++++++--------------------- mypy/checkmember.py | 5 ++++- mypy/types.py | 3 +++ test-data/unit/check-selftype.test | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2fe793ef4892..dc1bfe86ea0d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -835,28 +835,15 @@ def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, # Check for the issue described above. other_method = nodes.normal_from_reverse_op[method] arg_type = typ.arg_types[1] - arg_types = [arg_type] - if isinstance(arg_type, Instance): - if not arg_type.type.has_readable_member(other_method): - return - elif isinstance(arg_type, AnyType): + if not (isinstance(arg_type, (Instance, UnionType)) + and arg_type.has_readable_member(other_method)): return - elif isinstance(arg_type, UnionType): - if not arg_type.has_readable_member(other_method): - return - arg_types = list(union_items(arg_type)) - else: - return - # `info.get_method` in `analyze_member_access` does not handle decorated - # functions properly, so we perform it for each item of the union separately. - # see #3227 - for arg_type in arg_types: - typ2 = self.expr_checker.analyze_external_member_access( - other_method, arg_type, defn) - self.check_overlapping_op_methods( - typ, method, defn.info, - typ2, other_method, cast(Instance, arg_type), - defn) + typ2 = self.expr_checker.analyze_external_member_access( + other_method, arg_type, defn) + self.check_overlapping_op_methods( + typ, method, defn.info, + typ2, other_method, cast(Instance, arg_type), + defn) def check_overlapping_op_methods(self, reverse_type: CallableType, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index be0dae884b2d..3a1ee0ddd48e 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -20,6 +20,7 @@ from mypy.plugin import Plugin, AttributeContext from mypy import messages from mypy import subtypes +from mypy import meet MYPY = False if MYPY: # import for forward declaration only import mypy.checker @@ -314,7 +315,9 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont # methods: the former to the instance, the latter to the # class. functype = t - check_self_arg(functype, original_type, var.is_classmethod, node, msg) + # Use meet to simulate dispatch - e.g. reduce Union[A, B] to A on dispatch to A + dispatched_type = meet.meet_types(original_type, itype) + check_self_arg(functype, dispatched_type, var.is_classmethod, node, msg) signature = bind_self(functype, original_type, var.is_classmethod) if var.is_property: # A property cannot have an overloaded type => the cast diff --git a/mypy/types.py b/mypy/types.py index a53085a66a36..c102b3d30583 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -515,6 +515,9 @@ def deserialize(cls, data: Union[JsonDict, str]) -> 'Instance': def copy_modified(self, *, args: List[Type]) -> 'Instance': return Instance(self.type, args, self.line, self.column, self.erased) + def has_readable_member(self, name: str) -> bool: + return self.type.has_readable_member(name) + class TypeVarType(Type): """A type variable type. diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 9bbbcb629e32..d32b74261371 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -381,3 +381,21 @@ class A: # def g(self: None) -> None: ... see in check-python2.test [out] main:3: error: Self argument missing for a non-static method (or an invalid type for self) + +[case testUnionPropertyField] +from typing import Union + +class A: + x: int + +class B: + @property + def x(self) -> int: return 1 + +class C: + @property + def x(self) -> int: return 1 + +ab: Union[A, B, C] +reveal_type(ab.x) # E: Revealed type is 'builtins.int' +[builtins fixtures/property.pyi] From bd1f0f3015b3e2cb88c2e29139e754e712b42d84 Mon Sep 17 00:00:00 2001 From: elazar Date: Tue, 26 Sep 2017 21:25:14 +0300 Subject: [PATCH 12/13] better phrasing --- mypy/checker.py | 44 +++++++++++------------------ mypy/checkmember.py | 6 ++-- mypy/messages.py | 6 ++-- test-data/unit/check-functions.test | 4 +-- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index dc1bfe86ea0d..291b9e2ef994 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -810,40 +810,28 @@ def is_trivial_body(self, block: Block) -> bool: def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, method: str) -> None: """Check a reverse operator method such as __radd__.""" - - # This used to check for some very obscure scenario. It now - # just decides whether it's worth calling - # check_overlapping_op_methods(). + # Decides whether it's worth calling check_overlapping_op_methods(). if method in ('__eq__', '__ne__'): # These are defined for all objects => can't cause trouble. return + if len(typ.arg_types) > 2: + # Plausibly the method could have too few arguments, which would result + # in an error elsewhere. + return - # With 'Any' or 'object' return type we are happy, since any possible - # return value is valid. - ret_type = typ.ret_type - if isinstance(ret_type, AnyType): + other_method = nodes.normal_from_reverse_op[method] + arg_type = typ.arg_types[1] + if not (isinstance(arg_type, (Instance, UnionType)) + and arg_type.has_readable_member(other_method)): return - if isinstance(ret_type, Instance): - if ret_type.type.fullname() == 'builtins.object': - return - # Plausibly the method could have too few arguments, which would result - # in an error elsewhere. - if len(typ.arg_types) <= 2: - # TODO check self argument kind - - # Check for the issue described above. - other_method = nodes.normal_from_reverse_op[method] - arg_type = typ.arg_types[1] - if not (isinstance(arg_type, (Instance, UnionType)) - and arg_type.has_readable_member(other_method)): - return - typ2 = self.expr_checker.analyze_external_member_access( - other_method, arg_type, defn) - self.check_overlapping_op_methods( - typ, method, defn.info, - typ2, other_method, cast(Instance, arg_type), - defn) + + typ2 = self.expr_checker.analyze_external_member_access( + other_method, arg_type, defn) + self.check_overlapping_op_methods( + typ, method, defn.info, + typ2, other_method, cast(Instance, arg_type), + defn) def check_overlapping_op_methods(self, reverse_type: CallableType, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3a1ee0ddd48e..798a08995c45 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -317,7 +317,7 @@ def analyze_var(name: str, var: Var, itype: Instance, info: TypeInfo, node: Cont functype = t # Use meet to simulate dispatch - e.g. reduce Union[A, B] to A on dispatch to A dispatched_type = meet.meet_types(original_type, itype) - check_self_arg(functype, dispatched_type, var.is_classmethod, node, msg) + check_self_arg(functype, dispatched_type, var.is_classmethod, node, name, msg) signature = bind_self(functype, original_type, var.is_classmethod) if var.is_property: # A property cannot have an overloaded type => the cast @@ -374,7 +374,7 @@ def lookup_member_var_or_accessor(info: TypeInfo, name: str, def check_self_arg(functype: FunctionLike, original_type: Type, is_classmethod: bool, - context: Context, msg: MessageBuilder) -> None: + context: Context, name: str, msg: MessageBuilder) -> None: """Check that the the most precise type of the self argument is compatible with the declared type of each of the overloads. """ @@ -388,7 +388,7 @@ def check_self_arg(functype: FunctionLike, original_type: Type, is_classmethod: if is_classmethod: original_type = TypeType.make_normalized(original_type) if not subtypes.is_subtype(original_type, erase_to_bound(selfarg)): - msg.invalid_method_type(original_type, item, is_classmethod, context) + msg.invalid_method_type(name, original_type, item, is_classmethod, context) def analyze_class_attribute_access(itype: Instance, diff --git a/mypy/messages.py b/mypy/messages.py index 45dcc9a11c07..1f25f1990d2a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -856,11 +856,11 @@ def cannot_determine_type(self, name: str, context: Context) -> None: def cannot_determine_type_in_base(self, name: str, base: str, context: Context) -> None: self.fail("Cannot determine type of '%s' in base class '%s'" % (name, base), context) - def invalid_method_type(self, arg: Type, sig: CallableType, is_classmethod: bool, + def invalid_method_type(self, name: str, arg: Type, sig: CallableType, is_classmethod: bool, context: Context) -> None: kind = 'class attribute function' if is_classmethod else 'attribute function' - self.fail('Invalid self argument %s to %s %s' - % (self.format(arg), kind, self.format(sig)), context) + self.fail('Invalid self argument %s to %s "%s" with type %s' + % (self.format(arg), kind, name, self.format(sig)), context) def incompatible_conditional_function_def(self, defn: FuncDef) -> None: self.fail('All conditional function variants must have identical ' diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 373f100fcfdb..f6f3d7a971b6 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -508,7 +508,7 @@ class A: g = x # type: Callable[[B], None] a = None # type: A a.f() # E: Attribute function with type "Callable[[], None]" does not accept self argument -a.g() # E: Invalid self argument "A" to attribute function "Callable[[B], None]" +a.g() # E: Invalid self argument "A" to attribute function "g" with type "Callable[[B], None]" [case testMethodWithDynamicallyTypedMethodAsDataAttribute] from typing import Any, Callable @@ -568,7 +568,7 @@ class A(Generic[t]): ab = None # type: A[B] ac = None # type: A[C] ab.f() -ac.f() # E: Invalid self argument "A[C]" to attribute function "Callable[[A[B]], None]" +ac.f() # E: Invalid self argument "A[C]" to attribute function "f" with type "Callable[[A[B]], None]" [case testPartiallyTypedSelfInMethodDataAttribute] from typing import Any, TypeVar, Generic, Callable From 733fd8e726bda8be851d6fd67fcaf4cc0ce12d00 Mon Sep 17 00:00:00 2001 From: elazar Date: Tue, 26 Sep 2017 21:54:17 +0300 Subject: [PATCH 13/13] assert instead of cast; iterate over union --- mypy/checker.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 291b9e2ef994..8495a3a7722c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -807,31 +807,30 @@ def is_trivial_body(self, block: Block) -> bool: (isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr))) - def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, - method: str) -> None: + def check_reverse_op_method(self, defn: FuncItem, typ: CallableType, method: str) -> None: """Check a reverse operator method such as __radd__.""" # Decides whether it's worth calling check_overlapping_op_methods(). if method in ('__eq__', '__ne__'): # These are defined for all objects => can't cause trouble. return - if len(typ.arg_types) > 2: + if len(typ.arg_types) != 2: # Plausibly the method could have too few arguments, which would result # in an error elsewhere. return other_method = nodes.normal_from_reverse_op[method] arg_type = typ.arg_types[1] + # TODO: arg_type = arg_type.fallback, e.g. for TupleType if not (isinstance(arg_type, (Instance, UnionType)) and arg_type.has_readable_member(other_method)): return - typ2 = self.expr_checker.analyze_external_member_access( - other_method, arg_type, defn) - self.check_overlapping_op_methods( - typ, method, defn.info, - typ2, other_method, cast(Instance, arg_type), - defn) + typ2 = self.expr_checker.analyze_external_member_access(other_method, arg_type, defn) + for t in union_items(arg_type): + assert isinstance(t, Instance) + self.check_overlapping_op_methods(typ, method, defn.info, + typ2, other_method, t, defn) def check_overlapping_op_methods(self, reverse_type: CallableType,