| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
@JukkaL Thanks! I think I have implemented all comments so far. The only question left is how precise we should be with variances in typing-full.pyi fixture? If you think it makes sense to match typeshed, then I will also update this. |
Sorry, something went wrong.
There was a problem hiding this comment.
Another batch of test case reviews.
Sorry, something went wrong.
| def last(seq: Linked[T]) -> T: | ||
| pass | ||
|
|
||
| reveal_type(last(L())) # E: Revealed type is 'builtins.int*' |
There was a problem hiding this comment.
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.
Sorry, something went wrong.
|
|
||
| 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.
Again, consider testing more incompatible cases, such as attr2 existing but with an incompatible list item type.
Sorry, something went wrong.
| 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.
Overloaded method implementation should probably be optional in a protocol. If there is no implementation, the method would be treated as abstract.
Sorry, something went wrong.
| def f(self, x: int) -> None: | ||
| pass | ||
|
|
||
| x: P = C() |
There was a problem hiding this comment.
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.
Sorry, something went wrong.
|
|
||
| l = [x, y] | ||
|
|
||
| reveal_type(l) # E: Revealed type is 'builtins.list[__main__.P*]' |
There was a problem hiding this comment.
Also test the trivial case of [x, x] or similar.
Sorry, something went wrong.
| def bar(a: Sized) -> int: | ||
| return a.__len__() | ||
|
|
||
| bar(Foo()) |
There was a problem hiding this comment.
Also call with a list argument?
Sorry, something went wrong.
|
|
||
| [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.
Use # E: ... comment?
Sorry, something went wrong.
| @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.
Do you test anywhere what happens if a class has method and the protocol has an attribute/property, and vice versa?
Sorry, something went wrong.
There was a problem hiding this comment.
It looks like no, I added two tests.
Sorry, something went wrong.
| fun_p(C()) # OK | ||
| [builtins fixtures/list.pyi] | ||
|
|
||
| [case testIpmlicitTypesInProtocols] |
There was a problem hiding this comment.
Typo: Ipmlicit
Sorry, something went wrong.
| 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.
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).
Sorry, something went wrong.
There was a problem hiding this comment.
I have one, but only with self types, I added two tests with just "usual" generic methods.
Sorry, something went wrong.
There was a problem hiding this comment.
Another batch of comments. Hopefully the next batch will be the last one.
Sorry, something went wrong.
| 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.
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.
Sorry, something went wrong.
| # 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.
Should we erase the actual type as well?
Sorry, something went wrong.
There was a problem hiding this comment.
I am not sure, but in view of #3656 I am reluctant to increase similarity of anything to protocols.
Sorry, something went wrong.
| 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.
Add something about the correctness of this. Maybe mention that this is a standard way to perform structural subtype checks.
Sorry, something went wrong.
| 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.
Add comment about why this is safe.
Sorry, something went wrong.
| 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.
This and the previous section of code look very similar. Can you merge them to avoid code duplication?
Sorry, something went wrong.
| is_protocol = False # Is this a protocol class? | ||
| runtime_protocol = False # Does this protocol support isinstance checks? | ||
| abstract_attributes = None # type: List[str] | ||
| protocol_members = None # type: List[str] |
There was a problem hiding this comment.
Add comment. Some edge cases to discuss: What about inherited protocol members? What about members defined in object?
Sorry, something went wrong.
| 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.
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.
Sorry, something went wrong.
| # 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.
Explain what goes into the stack and why we need it.
Sorry, something went wrong.
| 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.
Typo: 'assumig'.
Sorry, something went wrong.
| # 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.
Document cache and cache_proper. Explain why we have them.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
| 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.
Do we need to propagate ignore_declared_variance in all recursive calls?
Sorry, something went wrong.
There was a problem hiding this comment.
Ideally, yes, but there are too many places, this is related to #3828, I will add a TODO item here.
Sorry, something went wrong.
| 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.
Would be better style to use a TypeInfo method such as record_subtype_cache_entry(left, right) instead of directly accessing cache.
Sorry, something went wrong.
There was a problem hiding this comment.
Also, should we only add to cache if no subtype flags such as ignore_pos_arg_names are being used?
Sorry, something went wrong.
There was a problem hiding this comment.
I am not super sure, but I think it is OK to always add them, since these are only for Instances and they (at least currently) are always used with ignore_pos_arg_names.
Sorry, something went wrong.
| 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.
Would be better style to use a method such as if right.type.is_cached_subtype_check(left, right):.
Sorry, something went wrong.
| # 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.
Typo: argiment
Sorry, something went wrong.
| stack.pop() | ||
|
|
||
|
|
||
| def is_protocol_implementation(left: Instance, right: Instance, allow_any: bool = True) -> bool: |
There was a problem hiding this comment.
What about changing allow_any to proper_subtype (and negate the value) or similar for consistency?
Sorry, something went wrong.
| return missing | ||
|
|
||
|
|
||
| def get_conflict_types(left: Instance, right: Instance) -> List[Tuple[str, Type, Type]]: |
There was a problem hiding this comment.
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.
Sorry, something went wrong.
| return conflicts | ||
|
|
||
|
|
||
| def get_bad_flags(left: Instance, right: Instance) -> List[Tuple[str, Set[int], Set[int]]]: |
There was a problem hiding this comment.
Like above, move to messages.py and include 'protocol' in the function name.
Sorry, something went wrong.
| 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.
What's this change?
Sorry, something went wrong.
There was a problem hiding this comment.
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" Anys 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
Sorry, something went wrong.
There was a problem hiding this comment.
Yeah, this seems okay. Maybe add a reference to #3827 here?
Sorry, something went wrong.
There was a problem hiding this comment.
OK, I will add a comment.
Sorry, something went wrong.
| 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.
If you add helper methods for accessing the cache, use them here.
Sorry, something went wrong.
| def accept(self, visitor: 'TypeVisitor[T]') -> T: | ||
| return visitor.visit_unbound_type(self) | ||
|
|
||
| def __hash__(self) -> int: |
There was a problem hiding this comment.
I haven't reviewed this file but I can do it later (even after the PR has been landed) since this looks pretty straightforward.
Sorry, something went wrong.
|
Before protocols are usable, Protocol needs to be added to typing_extensions (this can happen after this PR has been merged). |
Sorry, something went wrong.
Yes, I will do this soon. |
Sorry, something went wrong.
|
@JukkaL It looks like all the comments are implemented now (or corresponding issues filed). Thanks for review once more! |
Sorry, something went wrong.
There was a problem hiding this comment.
This looks good now! Thanks for persevering with the very long review process!
Sorry, something went wrong.
|
Here are some important follow-up tasks:
After the above steps user-defined protocols should work. Changing typing ABCs to be protocols can happen later -- potentially we'll have a mypy release with only support for user-defined protocols first. It's still not clear how we'll move forward with the bigger typeshed change. |
Sorry, something went wrong.
I will hopefully make PRs for typing_extensions (implementation and stubs) tomorrow. I think it is OK to go straight with typing_extensions since @vlasovskikh (and maybe other) is already interested in this. |
Sorry, something went wrong.
|
By the way, @matthiaskramm what do you (i.e. pytype team) think about protocols and making things like Iterable etc. protocols in typeshed? |
Sorry, something went wrong.
|
Woot! Congrats and thanks everyone. |
Sorry, something went wrong.
|
Thanks! @ilevkivskyi +1 for putting Protocol into typing_extensions. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
This is an implementation of protocols proposed in PEP 544. This PR adds support for:
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 Instances 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" TypeInfos and postponed this feature (as proposed by Jukka). On the other hand, reusing Instances 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 Instances, so that there is a net win in speed of few percent (even when I locally updated typeshed replacing all ABC in typing 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