Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor updates to protocol semantics #3996

Merged
merged 6 commits into from
Sep 29, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the body to a separate method, as this function is already quite long. Maybe also move the body of the above if statement (from line 269) to a separate method. You could also move the if statement to be nested under the above if statement (line 267). You'd only need the check if e.callee.fullname == 'builtins.subclass' which would simplify things slightly.

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):
Expand Down
2 changes: 2 additions & 0 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
9 changes: 9 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 19 additions & 2 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -386,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)
Expand Down Expand Up @@ -516,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,
Expand Down
2 changes: 1 addition & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
14 changes: 7 additions & 7 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -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[<nothing>]'
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[<nothing>]]'
if issubclass(x, (str, (list,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
Expand All @@ -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[<nothing>]'
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[<nothing>]]'
if issubclass(x, (str, (list,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
Expand All @@ -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[<nothing>]]'
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[<nothing>]'
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[<nothing>]]'
if issubclass(x, (str, (list,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
Expand Down
51 changes: 51 additions & 0 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -2118,3 +2118,54 @@ 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):
pass

x: P = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also test that None is compatible with a non-empty protocol when not using strict optional checking.

[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]

[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]