From ec7e239c2d702dd408718a531450f9b35f259935 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 24 Sep 2017 21:26:55 +0200 Subject: [PATCH 1/5] None should be subtype of empty protocol --- mypy/subtypes.py | 4 +++- test-data/unit/check-protocols.test | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 049da8a07228..c79678420a82 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -123,7 +123,9 @@ def visit_any(self, left: AnyType) -> bool: def visit_none_type(self, left: NoneTyp) -> bool: if experiments.STRICT_OPTIONAL: return (isinstance(self.right, NoneTyp) or - is_named_instance(self.right, 'builtins.object')) + is_named_instance(self.right, 'builtins.object') or + isinstance(self.right, Instance) and self.right.type.is_protocol and + not self.right.type.protocol_members) else: return True diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index fd43bbf3e0f3..87acb188b676 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2118,3 +2118,23 @@ main:10: note: def other(self, *args: Any, hint: Optional[str] = ..., ** main:10: note: Got: main:10: note: def other(self) -> int +[case testNoneSubtypeOfEmptyProtocol] +from typing import Protocol +class P(Protocol): + pass + +x: P = None +[out] + +[case testNoneSubtypeOfEmptyProtocolStrict] +# flags: --strict-optional +from typing import Protocol +class P(Protocol): + pass +x: P = None + +class PBad(Protocol): + x: int +y: PBad = None # E: Incompatible types in assignment (expression has type "None", variable has type "PBad") +[out] + From 2876a95ed3a39b8690a5b4a19882cc028e4106b1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 24 Sep 2017 21:34:55 +0200 Subject: [PATCH 2/5] Apply None rule only to Callables, add unrelated previously forgotten test --- mypy/subtypes.py | 2 +- test-data/unit/check-protocols.test | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index c79678420a82..4ddb4886c424 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -388,7 +388,7 @@ def f(self) -> A: ... is_compat = is_proper_subtype(subtype, supertype) if not is_compat: return False - if isinstance(subtype, NoneTyp) and member.startswith('__') and member.endswith('__'): + if isinstance(subtype, NoneTyp) and isinstance(supertype, CallableType): # We want __hash__ = None idiom to work even without --strict-optional return False subflags = get_member_flags(member, left.type) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 87acb188b676..92f7860436e6 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2118,6 +2118,12 @@ main:10: note: def other(self, *args: Any, hint: Optional[str] = ..., ** main:10: note: Got: main:10: note: def other(self) -> int +[case testObjectAllowedInProtocolBases] +from typing import Protocol +class P(Protocol, object): + pass +[out] + [case testNoneSubtypeOfEmptyProtocol] from typing import Protocol class P(Protocol): From fcdb2762d56e6e7daa84858b133a12da11453583 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 24 Sep 2017 23:02:46 +0200 Subject: [PATCH 3/5] Prohibit issubclass() for non-method protocols --- mypy/checkexpr.py | 12 +++++++++++- mypy/meet.py | 2 ++ mypy/messages.py | 9 +++++++++ mypy/subtypes.py | 15 +++++++++++++++ mypy/typeanal.py | 2 +- test-data/unit/check-isinstance.test | 14 +++++++------- test-data/unit/check-protocols.test | 25 +++++++++++++++++++++++++ 7 files changed, 70 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 36c89b3ceda4..c308496afa84 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -36,7 +36,7 @@ from mypy import join from mypy.meet import narrow_declared_type from mypy.maptype import map_instance_to_supertype -from mypy.subtypes import is_subtype, is_equivalent, find_member +from mypy.subtypes import is_subtype, is_equivalent, find_member, non_method_protocol_members from mypy import applytype from mypy import erasetype from mypy.checkmember import analyze_member_access, type_object_type, bind_self @@ -273,6 +273,16 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: not tp.type_object().runtime_protocol): self.chk.fail('Only @runtime protocols can be used with' ' instance and class checks', e) + if (isinstance(e.callee, RefExpr) and len(e.args) == 2 and + e.callee.fullname == 'builtins.issubclass'): + for expr in mypy.checker.flatten(e.args[1]): + tp = self.chk.type_map[expr] + if (isinstance(tp, CallableType) and tp.is_type_obj() and + tp.type_object().is_protocol): + attr_members = non_method_protocol_members(tp.type_object()) + if attr_members: + self.chk.msg.report_non_method_protocol(tp.type_object(), + attr_members, e) if isinstance(ret_type, UninhabitedType): self.chk.binder.unreachable() if not allow_none_return and isinstance(ret_type, NoneTyp): diff --git a/mypy/meet.py b/mypy/meet.py index 33d7f6e3df4a..3e883b53fd4d 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -44,6 +44,8 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: return narrowed elif isinstance(declared, (Instance, TupleType)): return meet_types(declared, narrowed) + elif isinstance(declared, TypeType) and isinstance(narrowed, TypeType): + return TypeType.make_normalized(narrow_declared_type(declared.item, narrowed.item)) return narrowed diff --git a/mypy/messages.py b/mypy/messages.py index 2f3473b3e59d..281a92c48411 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1052,6 +1052,15 @@ def concrete_only_call(self, typ: Type, context: Context) -> None: self.fail("Only concrete class can be given where {} is expected" .format(self.format(typ)), context) + def report_non_method_protocol(self, tp: TypeInfo, members: List[str], + context: Context) -> None: + self.fail("Only protocols that don't have non-method members can be" + " used with issubclass()", context) + if len(members) < 3: + attrs = ', '.join(members) + self.note('Protocol "{}" has non-method member(s): {}' + .format(tp.name(), attrs), context) + def note_call(self, subtype: Type, call: Type, context: Context) -> None: self.note('"{}.__call__" has type {}'.format(self.format_bare(subtype), self.format(call, verbosity=1)), context) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4ddb4886c424..e5034cd50bc1 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -518,6 +518,21 @@ def find_node_type(node: Union[Var, FuncBase], itype: Instance, subtype: Type) - return typ +def non_method_protocol_members(tp: TypeInfo) -> List[str]: + """Find all non-callable members of a protocol.""" + + assert tp.is_protocol + result = [] # type: List[str] + anytype = AnyType(TypeOfAny.special_form) + instance = Instance(tp, [anytype] * len(tp.defn.type_vars)) + + for member in tp.protocol_members: + typ = find_member(member, instance, instance) + if not isinstance(typ, CallableType): + result.append(member) + return result + + def is_callable_subtype(left: CallableType, right: CallableType, ignore_return: bool = False, ignore_pos_arg_names: bool = False, diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 1a7f66d0ae22..07f17872b0c4 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -718,7 +718,7 @@ def visit_partial_type(self, t: PartialType) -> None: pass def visit_type_type(self, t: TypeType) -> None: - pass + t.item.accept(self) TypeVarList = List[Tuple[str, TypeVarExpr]] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 60eef4eb4555..8279e1aeafd4 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1446,10 +1446,10 @@ def f(x: Union[Type[int], Type[str], Type[List]]) -> None: reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]' x()[1] # E: Value of type "Union[int, str]" is not indexable else: - reveal_type(x) # E: Revealed type is 'Type[builtins.list]' + reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]' reveal_type(x()) # E: Revealed type is 'builtins.list[]' x()[1] - reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]' + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' @@ -1468,10 +1468,10 @@ def f(x: Type[Union[int, str, List]]) -> None: reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]' x()[1] # E: Value of type "Union[int, str]" is not indexable else: - reveal_type(x) # E: Revealed type is 'Type[builtins.list]' + reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]' reveal_type(x()) # E: Revealed type is 'builtins.list[]' x()[1] - reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]' + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' @@ -1485,17 +1485,17 @@ def f(x: Type[Union[int, str, List]]) -> None: from typing import Union, List, Tuple, Dict, Type def f(x: Type[Union[int, str, List]]) -> None: - reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]' + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (int,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]' x()[1] # E: Value of type "Union[int, str]" is not indexable else: - reveal_type(x) # E: Revealed type is 'Type[builtins.list]' + reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]' reveal_type(x()) # E: Revealed type is 'builtins.list[]' x()[1] - reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]' + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 92f7860436e6..146cc924f0ad 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2144,3 +2144,28 @@ class PBad(Protocol): y: PBad = None # E: Incompatible types in assignment (expression has type "None", variable has type "PBad") [out] +[case testOnlyMethodProtocolUsableWithIsSubclass] +from typing import Protocol, runtime, Union, Type +@runtime +class P(Protocol): + def meth(self) -> int: + pass +@runtime +class PBad(Protocol): + x: str + +class C: + x: str + def meth(self) -> int: + pass +class E: pass + +cls: Type[Union[C, E]] +issubclass(cls, PBad) # E: Only protocols that don't have non-method members can be used with issubclass() \ + # N: Protocol "PBad" has non-method member(s): x +if issubclass(cls, P): + reveal_type(cls) # E: Revealed type is 'Type[__main__.C]' +else: + reveal_type(cls) # E: Revealed type is 'Type[__main__.E]' +[builtins fixtures/isinstance.pyi] +[out] From c31c99fbaa0be2b37eae84e43263020aa3736a6f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 28 Sep 2017 21:34:12 +0200 Subject: [PATCH 4/5] Address CR --- mypy/checkexpr.py | 43 ++++++++++++++++------------- test-data/unit/check-protocols.test | 10 +++++++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c308496afa84..e35ab9044609 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -264,25 +264,11 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: callee_type = self.apply_method_signature_hook( e, callee_type, object_type, signature_hook) ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname, object_type) - if (isinstance(e.callee, RefExpr) and len(e.args) == 2 and - e.callee.fullname in ('builtins.isinstance', 'builtins.issubclass')): - for expr in mypy.checker.flatten(e.args[1]): - tp = self.chk.type_map[expr] - if (isinstance(tp, CallableType) and tp.is_type_obj() and - tp.type_object().is_protocol and - not tp.type_object().runtime_protocol): - self.chk.fail('Only @runtime protocols can be used with' - ' instance and class checks', e) - if (isinstance(e.callee, RefExpr) and len(e.args) == 2 and - e.callee.fullname == 'builtins.issubclass'): - for expr in mypy.checker.flatten(e.args[1]): - tp = self.chk.type_map[expr] - if (isinstance(tp, CallableType) and tp.is_type_obj() and - tp.type_object().is_protocol): - attr_members = non_method_protocol_members(tp.type_object()) - if attr_members: - self.chk.msg.report_non_method_protocol(tp.type_object(), - attr_members, e) + if isinstance(e.callee, RefExpr) and len(e.args) == 2: + if e.callee.fullname in ('builtins.isinstance', 'builtins.issubclass'): + self.check_runtime_protocol_test(e) + if e.callee.fullname == 'builtins.issubclass': + self.check_protocol_issubclass(e) if isinstance(ret_type, UninhabitedType): self.chk.binder.unreachable() if not allow_none_return and isinstance(ret_type, NoneTyp): @@ -290,6 +276,25 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: return AnyType(TypeOfAny.from_error) return ret_type + def check_runtime_protocol_test(self, e: CallExpr) -> None: + for expr in mypy.checker.flatten(e.args[1]): + tp = self.chk.type_map[expr] + if (isinstance(tp, CallableType) and tp.is_type_obj() and + tp.type_object().is_protocol and + not tp.type_object().runtime_protocol): + self.chk.fail('Only @runtime protocols can be used with' + ' instance and class checks', e) + + def check_protocol_issubclass(self, e: CallExpr) -> None: + for expr in mypy.checker.flatten(e.args[1]): + tp = self.chk.type_map[expr] + if (isinstance(tp, CallableType) and tp.is_type_obj() and + tp.type_object().is_protocol): + attr_members = non_method_protocol_members(tp.type_object()) + if attr_members: + self.chk.msg.report_non_method_protocol(tp.type_object(), + attr_members, e) + def check_typeddict_call(self, callee: TypedDictType, arg_kinds: List[int], arg_names: Sequence[Optional[str]], diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 146cc924f0ad..1da2c1f63a52 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2132,6 +2132,16 @@ class P(Protocol): x: P = None [out] +[case testNoneSubtypeOfAllProtocolsWithoutStrictOptional] +from typing import Protocol +class P(Protocol): + attr: int + def meth(self, arg: str) -> str: + pass + +x: P = None +[out] + [case testNoneSubtypeOfEmptyProtocolStrict] # flags: --strict-optional from typing import Protocol From 07de2a3e8e64d4cc2481742ce8e26ee08987fad7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 28 Sep 2017 21:37:14 +0200 Subject: [PATCH 5/5] Restore correct typeshed commit --- typeshed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typeshed b/typeshed index 55dbb967ad00..d389ef3d854c 160000 --- a/typeshed +++ b/typeshed @@ -1 +1 @@ -Subproject commit 55dbb967ad006d5835590c53ab023774dc5a1341 +Subproject commit d389ef3d854cf38d1e52d04ad2d4cfc5f45a0e29