-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Protocols #3132
Protocols #3132
Conversation
@JukkaL Thanks! I think I have implemented all comments so far. The only question left is how precise we should be with variances in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another batch of test case reviews.
def last(seq: Linked[T]) -> T: | ||
pass | ||
|
||
reveal_type(last(L())) # E: Revealed type is 'builtins.int*' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add test where a class subtly does not implement a recursive protocol. For example, the return type of next()
is not compatible but otherwise things are fine.
|
||
t: P1 | ||
t = A() # OK | ||
t = B() # E: Incompatible types in assignment (expression has type "B", variable has type "P1") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, consider testing more incompatible cases, such as attr2
existing but with an incompatible list item type.
test-data/unit/check-protocols.test
Outdated
def f(self, x: int) -> int: pass | ||
@overload | ||
def f(self, x: str) -> str: pass | ||
def f(self, x): pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overloaded method implementation should probably be optional in a protocol. If there is no implementation, the method would be treated as abstract.
def f(self, x: int) -> None: | ||
pass | ||
|
||
x: P = C() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test that a class with an overloaded method with an identical signature is treated as compatible. Test that a class with an invalid overloaded method is treated as incompatible.
test-data/unit/check-protocols.test
Outdated
|
||
l = [x, y] | ||
|
||
reveal_type(l) # E: Revealed type is 'builtins.list[__main__.P*]' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also test the trivial case of [x, x]
or similar.
def bar(a: Sized) -> int: | ||
return a.__len__() | ||
|
||
bar(Foo()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also call with a list argument?
test-data/unit/check-protocols.test
Outdated
|
||
[builtins fixtures/isinstancelist.pyi] | ||
[out] | ||
main:11: error: Argument 1 to "foo" has incompatible type "str"; expected "SupportsInt" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use # E: ...
comment?
@property | ||
def attr2(self) -> int: pass | ||
|
||
x: P = C() # E: Incompatible types in assignment (expression has type "C", variable has type "P") \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you test anywhere what happens if a class has method and the protocol has an attribute/property, and vice versa?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like no, I added two tests.
test-data/unit/check-protocols.test
Outdated
fun_p(C()) # OK | ||
[builtins fixtures/list.pyi] | ||
|
||
[case testIpmlicitTypesInProtocols] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: Ipmlicit
class P(Protocol[T]): # E: Invariant type variable 'T' used in protocol where covariant one is expected | ||
attr: int | ||
|
||
[case testGenericProtocolsInference1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember if I saw a test case that would have a related case where the protocol is non-generic and a non-generic class has a generic method. Say, protocol has signature int -> int
and class has signature T -> T
(but T
has method scope).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have one, but only with self types, I added two tests with just "usual" generic methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another batch of comments. Hopefully the next batch will be the last one.
down_args = [UninhabitedType() if i == j else AnyType(TypeOfAny.special_form) | ||
for j, _ in enumerate(tvars)] | ||
up, down = Instance(info, up_args), Instance(info, down_args) | ||
# TODO: add advanced variance checks for recursive protocols |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create issue about variance checks for recursive protocols? It seems that a set of mutually recursive protocols should be checked as a unit, since the variance in one protocol can affect variance in another protocol.
@@ -2697,6 +2720,8 @@ def overload_arg_similarity(actual: Type, formal: Type) -> int: | |||
# subtyping algorithm if type promotions are possible (e.g., int vs. float). | |||
if formal.type in actual.type.mro: | |||
return 2 | |||
elif formal.type.is_protocol and is_subtype(actual, erasetype.erase_type(formal)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we erase the actual type as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure, but in view of #3656 I am reluctant to increase similarity of anything to protocols.
@@ -336,6 +338,38 @@ def visit_instance(self, template: Instance) -> List[Constraint]: | |||
res.extend(infer_constraints( | |||
template.args[j], mapped.args[j], neg_op(self.direction))) | |||
return res | |||
if (template.type.is_protocol and self.direction == SUPERTYPE_OF and | |||
# We avoid infinite recursion for structural subtypes by checking | |||
# whether this type already appeared in the inference chain. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add something about the correctness of this. Maybe mention that this is a standard way to perform structural subtype checks.
mypy/constraints.py
Outdated
for member in template.type.protocol_members: | ||
inst = mypy.subtypes.find_member(member, instance, original_actual) | ||
temp = mypy.subtypes.find_member(member, template, original_actual) | ||
assert inst is not None and temp is not None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment about why this is safe.
mypy/constraints.py
Outdated
res.extend(infer_constraints(temp, inst, neg_op(self.direction))) | ||
template.type.inferring.pop() | ||
return res | ||
elif (instance.type.is_protocol and self.direction == SUBTYPE_OF and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the previous section of code look very similar. Can you merge them to avoid code duplication?
mypy/nodes.py
Outdated
abstract_attributes = None # type: List[str] | ||
protocol_members = None # type: List[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment. Some edge cases to discuss: What about inherited protocol members? What about members defined in object
?
mypy/nodes.py
Outdated
abstract_attributes = None # type: List[str] | ||
protocol_members = None # type: List[str] | ||
|
||
# These represent structural subtype matrices. Note that these are shared |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first sentence is not descriptive enough. This is a pretty tricky concept so please explain it in more detail and maybe give an example.
mypy/nodes.py
Outdated
# during the type checking phase. | ||
assuming = None # type: List[Tuple[mypy.types.Instance, mypy.types.Instance]] | ||
assuming_proper = None # type: List[Tuple[mypy.types.Instance, mypy.types.Instance]] | ||
# Ditto for temporary stack of recursive constraint inference. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explain what goes into the stack and why we need it.
mypy/nodes.py
Outdated
inferring = None # type: List[mypy.types.Instance] | ||
cache = None # type: Set[Tuple[mypy.types.Type, mypy.types.Type]] | ||
cache_proper = None # type: Set[Tuple[mypy.types.Type, mypy.types.Type]] | ||
# 'inferring' and 'assumig' can't be also made sets, since we need to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: 'assumig'.
mypy/nodes.py
Outdated
# there is a dependency infer_constraint -> is_subtype -> is_callable_subtype -> | ||
# -> infer_constraints. | ||
inferring = None # type: List[mypy.types.Instance] | ||
cache = None # type: Set[Tuple[mypy.types.Type, mypy.types.Type]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document cache
and cache_proper
. Explain why we have them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This concludes my review. As I mentioned earlier, fixing most issues now is optional -- there wasn't anything super critical. They can be fixed after this has been merged. If you leave things out, create issues which mention comments that haven't been followed through yet to more easily track what is not done yet.
mypy/subtypes.py
Outdated
# NOTO: left.type.mro may be None in quick mode if there | ||
if (left, right) in right.type.cache: | ||
return True | ||
# NOTE: left.type.mro may be None in quick mode if there | ||
# was an error somewhere. | ||
if left.type.mro is not None: | ||
for base in left.type.mro: | ||
if base._promote and is_subtype( | ||
base._promote, self.right, self.check_type_parameter, | ||
ignore_pos_arg_names=self.ignore_pos_arg_names): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to propagate ignore_declared_variance
in all recursive calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, yes, but there are too many places, this is related to #3828, I will add a TODO item here.
mypy/subtypes.py
Outdated
# was an error somewhere. | ||
if left.type.mro is not None: | ||
for base in left.type.mro: | ||
if base._promote and is_subtype( | ||
base._promote, self.right, self.check_type_parameter, | ||
ignore_pos_arg_names=self.ignore_pos_arg_names): | ||
right.type.cache.add((left, right)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better style to use a TypeInfo
method such as record_subtype_cache_entry(left, right)
instead of directly accessing cache
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, should we only add to cache if no subtype flags such as ignore_pos_arg_names
are being used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not super sure, but I think it is OK to always add them, since these are only for Instance
s and they (at least currently) are always used with ignore_pos_arg_names
.
mypy/subtypes.py
Outdated
@@ -130,24 +143,33 @@ def visit_instance(self, left: Instance) -> bool: | |||
if isinstance(right, TupleType) and right.fallback.type.is_enum: | |||
return is_subtype(left, right.fallback) | |||
if isinstance(right, Instance): | |||
# NOTO: left.type.mro may be None in quick mode if there | |||
if (left, right) in right.type.cache: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better style to use a method such as if right.type.is_cached_subtype_check(left, right):
.
mypy/subtypes.py
Outdated
# nominal subtyping currently ignores '__init__' and '__new__' signatures | ||
if member in ('__init__', '__new__'): | ||
continue | ||
# The third argiment below indicates to what self type is bound. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: argiment
mypy/subtypes.py
Outdated
stack.pop() | ||
|
||
|
||
def is_protocol_implementation(left: Instance, right: Instance, allow_any: bool = True) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about changing allow_any
to proper_subtype
(and negate the value) or similar for consistency?
mypy/subtypes.py
Outdated
return missing | ||
|
||
|
||
def get_conflict_types(left: Instance, right: Instance) -> List[Tuple[str, Type, Type]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, it looks like this should be in messages.py
(can still be a top-level function) and should include protocol in the name for clarity.
mypy/subtypes.py
Outdated
return conflicts | ||
|
||
|
||
def get_bad_flags(left: Instance, right: Instance) -> List[Tuple[str, Set[int], Set[int]]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like above, move to messages.py
and include 'protocol' in the function name.
mypy/subtypes.py
Outdated
@@ -549,7 +809,8 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: | |||
# Since runtime type checks will ignore type arguments, erase the types. | |||
erased_s = erase_type(s) | |||
new_items = [item for item in t.relevant_items() | |||
if (not is_proper_subtype(erase_type(item), erased_s) | |||
if (not (is_proper_subtype(erase_type(item), erased_s) or | |||
is_proper_subtype(item, erased_s)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not only related to protocols, but it affected several tests. This is actually a step in the direction of #3827. The logic is following, if I erase both item
and s
, then there will be "too many" Any
s there, and is_proper_subtype
will return False
, while at runtime isinstance()
will actually return True
for protocols. Maybe this is not 100% ideal, but it fixed several test failures without causing new failures in existing tests.
More generally, I think we could reconsider use of is_proper_subtype(erased_t, erased_s)
for isinstance()
with nominal types, maybe there could be a better concept, something like is_runtime_subtype
? I don't think we need a separate issue for this, this is basically just an extended discussion for #3827
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this seems okay. Maybe add a reference to #3827 here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I will add a comment.
mypy/subtypes.py
Outdated
@@ -601,26 +862,38 @@ def visit_deleted_type(self, left: DeletedType) -> bool: | |||
def visit_instance(self, left: Instance) -> bool: | |||
right = self.right | |||
if isinstance(right, Instance): | |||
if (left, right) in right.type.cache_proper: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you add helper methods for accessing the cache, use them here.
@@ -188,6 +188,15 @@ def __init__(self, | |||
def accept(self, visitor: 'TypeVisitor[T]') -> T: | |||
return visitor.visit_unbound_type(self) | |||
|
|||
def __hash__(self) -> int: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed this file but I can do it later (even after the PR has been landed) since this looks pretty straightforward.
Before protocols are usable, |
Yes, I will do this soon. |
@JukkaL It looks like all the comments are implemented now (or corresponding issues filed). Thanks for review once more! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good now! Thanks for persevering with the very long review process!
Here are some important follow-up tasks:
After the above steps user-defined protocols should work. Changing |
I will hopefully make PRs for |
By the way, @matthiaskramm what do you (i.e. pytype team) think about protocols and making things like |
Woot! Congrats and thanks everyone. |
Thanks! @ilevkivskyi +1 for putting |
This is an implementation of protocols proposed in PEP 544. This PR adds support for:
Type[]
andisinstance()
Callable
andTuple
For example, now one can do this:
In general, the PR is practically feature complete, the main missing thing now is structural join/meet for partially overlapping protocols. For example, sometimes an explicit annotation is needed:
The reason for this is that I decided to re-use
Instance
s for protocols, and real structural join/meet would require creating a "fake"TypeInfo
to describe the resulting protocol. However, I have encountered problems with serialization/de-serialization of "fake"TypeInfo
s and postponed this feature (as proposed by Jukka). On the other hand, reusingInstance
s for protocols allowed to implement this in a very simple way. The PR is very small and 2/3 of it are extensive tests.There is something I encountered while working on this PR. It looks like structural subtyping is significantly slower than nominal. This is why I also implement "subtype cache" for
Instance
s, so that there is a net win in speed of few percent (even when I locally updated typeshed replacing all ABC intyping
with protocols).In terms of work-flow, I would be happy if people will try this PR and give also feedback on behaviour, not just a code review. I will soon submit a PR to typeshed with updates for
typing
stubs, and a PR with runtime implementation. As well, a PR to PEPs repo with an updated draft that takes into account comments that appeared so far.@JukkaL @ambv @gvanrossum