Skip to content

Commit b3ca169

Browse files
ilevkivskyigvanrossum
authored andcommitted
Minor updates to protocol semantics (#3996)
This updates protocol semantic according to the discussion in python/typing#464: None should be considered a subtype of an empty protocol method = None rule should apply only to callable protocol members issublcass() is prohibited for protocols with non-method members. Fixes #3906 Fixes #3938 Fixes #3939
1 parent e9d1ed7 commit b3ca169

File tree

5 files changed

+116
-12
lines changed

5 files changed

+116
-12
lines changed

mypy/checkexpr.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from mypy import join
3737
from mypy.meet import narrow_declared_type
3838
from mypy.maptype import map_instance_to_supertype
39-
from mypy.subtypes import is_subtype, is_equivalent, find_member
39+
from mypy.subtypes import is_subtype, is_equivalent, find_member, non_method_protocol_members
4040
from mypy import applytype
4141
from mypy import erasetype
4242
from mypy.checkmember import analyze_member_access, type_object_type, bind_self
@@ -264,22 +264,37 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
264264
callee_type = self.apply_method_signature_hook(
265265
e, callee_type, object_type, signature_hook)
266266
ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname, object_type)
267-
if (isinstance(e.callee, RefExpr) and len(e.args) == 2 and
268-
e.callee.fullname in ('builtins.isinstance', 'builtins.issubclass')):
269-
for expr in mypy.checker.flatten(e.args[1]):
270-
tp = self.chk.type_map[expr]
271-
if (isinstance(tp, CallableType) and tp.is_type_obj() and
272-
tp.type_object().is_protocol and
273-
not tp.type_object().runtime_protocol):
274-
self.chk.fail('Only @runtime protocols can be used with'
275-
' instance and class checks', e)
267+
if isinstance(e.callee, RefExpr) and len(e.args) == 2:
268+
if e.callee.fullname in ('builtins.isinstance', 'builtins.issubclass'):
269+
self.check_runtime_protocol_test(e)
270+
if e.callee.fullname == 'builtins.issubclass':
271+
self.check_protocol_issubclass(e)
276272
if isinstance(ret_type, UninhabitedType):
277273
self.chk.binder.unreachable()
278274
if not allow_none_return and isinstance(ret_type, NoneTyp):
279275
self.chk.msg.does_not_return_value(callee_type, e)
280276
return AnyType(TypeOfAny.from_error)
281277
return ret_type
282278

279+
def check_runtime_protocol_test(self, e: CallExpr) -> None:
280+
for expr in mypy.checker.flatten(e.args[1]):
281+
tp = self.chk.type_map[expr]
282+
if (isinstance(tp, CallableType) and tp.is_type_obj() and
283+
tp.type_object().is_protocol and
284+
not tp.type_object().runtime_protocol):
285+
self.chk.fail('Only @runtime protocols can be used with'
286+
' instance and class checks', e)
287+
288+
def check_protocol_issubclass(self, e: CallExpr) -> None:
289+
for expr in mypy.checker.flatten(e.args[1]):
290+
tp = self.chk.type_map[expr]
291+
if (isinstance(tp, CallableType) and tp.is_type_obj() and
292+
tp.type_object().is_protocol):
293+
attr_members = non_method_protocol_members(tp.type_object())
294+
if attr_members:
295+
self.chk.msg.report_non_method_protocol(tp.type_object(),
296+
attr_members, e)
297+
283298
def check_typeddict_call(self, callee: TypedDictType,
284299
arg_kinds: List[int],
285300
arg_names: Sequence[Optional[str]],

mypy/meet.py

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
4444
return narrowed
4545
elif isinstance(declared, (Instance, TupleType)):
4646
return meet_types(declared, narrowed)
47+
elif isinstance(declared, TypeType) and isinstance(narrowed, TypeType):
48+
return TypeType.make_normalized(narrow_declared_type(declared.item, narrowed.item))
4749
return narrowed
4850

4951

mypy/messages.py

+9
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,15 @@ def concrete_only_call(self, typ: Type, context: Context) -> None:
10561056
self.fail("Only concrete class can be given where {} is expected"
10571057
.format(self.format(typ)), context)
10581058

1059+
def report_non_method_protocol(self, tp: TypeInfo, members: List[str],
1060+
context: Context) -> None:
1061+
self.fail("Only protocols that don't have non-method members can be"
1062+
" used with issubclass()", context)
1063+
if len(members) < 3:
1064+
attrs = ', '.join(members)
1065+
self.note('Protocol "{}" has non-method member(s): {}'
1066+
.format(tp.name(), attrs), context)
1067+
10591068
def note_call(self, subtype: Type, call: Type, context: Context) -> None:
10601069
self.note('"{}.__call__" has type {}'.format(self.format_bare(subtype),
10611070
self.format(call, verbosity=1)), context)

mypy/subtypes.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ def visit_any(self, left: AnyType) -> bool:
123123
def visit_none_type(self, left: NoneTyp) -> bool:
124124
if experiments.STRICT_OPTIONAL:
125125
return (isinstance(self.right, NoneTyp) or
126-
is_named_instance(self.right, 'builtins.object'))
126+
is_named_instance(self.right, 'builtins.object') or
127+
isinstance(self.right, Instance) and self.right.type.is_protocol and
128+
not self.right.type.protocol_members)
127129
else:
128130
return True
129131

@@ -386,7 +388,7 @@ def f(self) -> A: ...
386388
is_compat = is_proper_subtype(subtype, supertype)
387389
if not is_compat:
388390
return False
389-
if isinstance(subtype, NoneTyp) and member.startswith('__') and member.endswith('__'):
391+
if isinstance(subtype, NoneTyp) and isinstance(supertype, CallableType):
390392
# We want __hash__ = None idiom to work even without --strict-optional
391393
return False
392394
subflags = get_member_flags(member, left.type)
@@ -516,6 +518,21 @@ def find_node_type(node: Union[Var, FuncBase], itype: Instance, subtype: Type) -
516518
return typ
517519

518520

521+
def non_method_protocol_members(tp: TypeInfo) -> List[str]:
522+
"""Find all non-callable members of a protocol."""
523+
524+
assert tp.is_protocol
525+
result = [] # type: List[str]
526+
anytype = AnyType(TypeOfAny.special_form)
527+
instance = Instance(tp, [anytype] * len(tp.defn.type_vars))
528+
529+
for member in tp.protocol_members:
530+
typ = find_member(member, instance, instance)
531+
if not isinstance(typ, CallableType):
532+
result.append(member)
533+
return result
534+
535+
519536
def is_callable_subtype(left: CallableType, right: CallableType,
520537
ignore_return: bool = False,
521538
ignore_pos_arg_names: bool = False,

test-data/unit/check-protocols.test

+61
Original file line numberDiff line numberDiff line change
@@ -2118,3 +2118,64 @@ main:10: note: def other(self, *args: Any, hint: Optional[str] = ..., **
21182118
main:10: note: Got:
21192119
main:10: note: def other(self) -> int
21202120

2121+
[case testObjectAllowedInProtocolBases]
2122+
from typing import Protocol
2123+
class P(Protocol, object):
2124+
pass
2125+
[out]
2126+
2127+
[case testNoneSubtypeOfEmptyProtocol]
2128+
from typing import Protocol
2129+
class P(Protocol):
2130+
pass
2131+
2132+
x: P = None
2133+
[out]
2134+
2135+
[case testNoneSubtypeOfAllProtocolsWithoutStrictOptional]
2136+
from typing import Protocol
2137+
class P(Protocol):
2138+
attr: int
2139+
def meth(self, arg: str) -> str:
2140+
pass
2141+
2142+
x: P = None
2143+
[out]
2144+
2145+
[case testNoneSubtypeOfEmptyProtocolStrict]
2146+
# flags: --strict-optional
2147+
from typing import Protocol
2148+
class P(Protocol):
2149+
pass
2150+
x: P = None
2151+
2152+
class PBad(Protocol):
2153+
x: int
2154+
y: PBad = None # E: Incompatible types in assignment (expression has type "None", variable has type "PBad")
2155+
[out]
2156+
2157+
[case testOnlyMethodProtocolUsableWithIsSubclass]
2158+
from typing import Protocol, runtime, Union, Type
2159+
@runtime
2160+
class P(Protocol):
2161+
def meth(self) -> int:
2162+
pass
2163+
@runtime
2164+
class PBad(Protocol):
2165+
x: str
2166+
2167+
class C:
2168+
x: str
2169+
def meth(self) -> int:
2170+
pass
2171+
class E: pass
2172+
2173+
cls: Type[Union[C, E]]
2174+
issubclass(cls, PBad) # E: Only protocols that don't have non-method members can be used with issubclass() \
2175+
# N: Protocol "PBad" has non-method member(s): x
2176+
if issubclass(cls, P):
2177+
reveal_type(cls) # E: Revealed type is 'Type[__main__.C]'
2178+
else:
2179+
reveal_type(cls) # E: Revealed type is 'Type[__main__.E]'
2180+
[builtins fixtures/isinstance.pyi]
2181+
[out]

0 commit comments

Comments
 (0)