GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Protocols in typing extensions by ilevkivskyi · Pull Request #464 · python/typing Conversation
Member
This PR is essentially an adaptation of #417, here are some comments:
@Michael0x2a @vlasovskikh I will be grateful for a code review and/or playing with this and commenting whether the behaviour matches your expectations.
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
This was referenced
Contributor
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
Author
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean "multiple times"?
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just use the same wording as in
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be private?
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably yes, this makes sense not to expose it (I think we can't give guarantees about
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "multiple times" (I think)
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
Member
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
Author
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
Author
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
Member
Author
All reactions
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page.
|
There was an error while loading. Please reload this page.
|
(Actually all tests, not only |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ah, great. (Though maybe we should add 3.6.2 now that it's out.)
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. Thanks @ilevkivskyi!
The stub for typing_extensions is likely to be required in the Typeshed for checking protocols statically.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Note: I haven't really been paying close attention to Protocol-related discussion, so I apologize in advance if anything I'm mentioning before has already been brought up somewhere else. In any case, I found the following bugs/inconsistencies (I'm running Python 3.6.1):
You also asked if the behavior matched my expectations, so here are some notes related to that. I don't think these are really in-scope for this pull request though, so I can open issues on either the mypy or typing issue trackers as appropriate if you want.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@Michael0x2a Thank you very much for comments and sorry for super-long delay. Here I reply to them, a bit later a will push the commits.
Thanks, good catch, will fix this and will add tests as you suggested.
Yes, but I think in this case this is a problem with mypy. We already have issue python/mypy#3827 to track this. In principle I think there is a good chance to make
Although it is surprising, it is technically true from the point of view of runtime check, since the runtime check does not care about signatures of methods, only their presence. On the contrary, mypy of course distinguishes classes and instances in static structural subtype checks.
I think this should raise a
There is already such check, I will see if there are enough tests for it: >>> class C(Protocol): ... x: int >>> @runtime ... class D(C): ... pass Traceback (most recent call last): File "<stdin>", line 2, in <module> File "/Users/ivan/typehinting/typing_extensions/src_py3/typing_extensions.py", line 927, in runtime ' got %r' % cls) TypeError: @runtime can be only applied to protocol classes, got __main__.D
Yes, it will be much easier to do this in
This question already appeared, but this is probably the best name proposed so far. I think this is OK, since this mirrors how ABCs like class Test: __iter__ = 'Surprise!' isinstance(Test(), collections.abc.Iterable) # True isinstance(Test(), typing.Iterable) # Also True, since it simply delegates to the above. I haven't heard many complains about this.
I think that it should be
This is not specific to protocols. Exactly the same problem exists with normal classes since mypy does not typecheck empty function bodies. I can see that maybe it is more important for protocols. It would be easy to implement in mypy, if you think it is important, then you can open an issue at mypy tracker.
I think mypy already has some decent docs, when this will be added to
Yes, this is not mentioned in the docs, but mypy tries to carefully verify that not only types, but also "flags" (class vs instance variable, writable vs property, static method or not, etc) for attributes are satisfied in a structural subtype. Concerning the error with ellipsis, protocol attributes without default values should be declared like this (no r.h.s.): class P5(Protocol): x: ClassVar[int] y: int The ellipsis is used in tub files, to indicate that there is a default value. Therefore the fact that mypy does not show an error is probably a bug, I think assigning ellipses should be allowed only in stubs.
Yes, the user might want to provide a default implementation (just like with ABCs), for example: class Proto(Protocol): @abstractmethod def meth(self, arg: int) -> str: return str(arg + 1) class Concrete(Proto): def meth(arg: int) -> str: basic = super().meth() return f'Fancy {basic}' |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I did a quick review pass and some manual experimentation.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| def _collection_protocol(cls): | ||
| # Selected set of collections ABCs that are considered protocols. | ||
| name = cls.__name__ | ||
| return (name in ('ABC', 'Callable', 'Awaitable', |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Before the bigger protocol-related typeshed PR lands, mypy won't treat these as protocols. Is this a problem in case the typeshed change gets delayed?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will cause problems, but just for consistency I will remove this, thus prohibiting to create subprotocols of Iterable etc. Anyway, there is a separate typing PR #417 that is oriented for post-typeshed-PR time.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| # We need this method for situations where attributes are assigned in __init__ | ||
| if isinstance(instance, type): | ||
| # This looks like a fundamental limitation of Python 2. | ||
| # It cannot support runtime protocol metaclasses |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
It's not clear what this limitation implies. Can you make the comment more specific?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I will add comment. On Python 2 classes cannot be correctly inspected as instances of protocols, I will double check if it is true only for old-style classes or for both.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| for attr in self._get_protocol_attrs()) | ||
| return False | ||
|
|
||
| def __subclasscheck__(self, cls): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This fails with class A has no attribute '__mro__':
from typing_extensions import Protocol, runtime @runtime class P(Protocol): def f(self): pass class A: pass # Note: no explicit object base class print issubclass(A, P) # Error
An explicit object base class works around the problem.
More generally, I'm not sure if subclass checks are well-defined if a protocol has non-method attributes, since they could well be initialized in __init__ and not visible in the type object. This could be fixed by not supporting issubclass with protocols, or only supporting it with protocols that only define methods, since it's rare that a method is defined in __init__ (though it's not completely unheard of).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
That's an old-style class in Python 2; I'm not sure we should care about support for old-style classes for Protocols in Python 2 (since they are deprecated and a lot of the class machinery works differently for them).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Yes, old-stye classes are problematic, I will check if it is possible to do something reasonable with them, if not, then I will add a more meaningful error message like "Old style classes (not inheriting from object) are not supported with protocols".
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if '__subclasshook__' not in cls.__dict__: | ||
| cls.__subclasshook__ = classmethod(_proto_hook) | ||
|
|
||
| def __instancecheck__(self, instance): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This unexpectedly prints out True (Python 2):
from typing_extensions import Protocol, runtime @runtime class P(Protocol): x = None # type: int class B(object): pass print isinstance(B(), P) # True, but should be False?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This maybe a glitch in the logic of treating None. The None is special, since __iter__ = None explicitly indicates that a class is not iterable at runtime. I will double check.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem quite right. Here is another Python 2 example (Python 3 is similar):
from typing import Optional from typing_extensions import Protocol, runtime @runtime class P(Protocol): x = None # type: Optional[str] class B(object): def __init__(self): # type: () -> None self.x = None # type: Optional[str] b = B() print isinstance(b, P) # False b.x = '' print isinstance(b, P) # True b.x = None print isinstance(b, P) # False
I'd expect the output to be True/True/True. What about only using the special None logic for protocol members that are callables in the protocol? They could still be used to represent a missing method. None is a valid value for a non-method attribute, so special casing those cases seems wrong to me.
(Overriding with None breaks Liskov substitutability but supporting it is reasonable since it's a Python idiom, for better or worse.)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
What about only using the special None logic for protocol members that are callables in the protocol?
What do you think about doing the same that mypy currently does -- apply the None in subclass rule only to dunder attributes? (like __iter__). Maybe even better idea would be to do exactly the same as ABCs in collections.abc do, i.e. only check whether e.g. __iter__ is set to None on class, not on instances. What do you think?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Just checking for None attribute in the class sounds like a good idea, thought it wouldn't be enough by itself. For example, B could be defined like:
class B(object): x = None # type: Optional[str]
Restriction to dunder attributes seems too ad hoc -- the None check is also used for non-dunder methods such as send and throw in collections.abc (https://github.com/python/cpython/blob/master/Lib/_collections_abc.py#L151). Mypy probably shouldn't use it either.
What about doing a callable() check on the attribute value defined in the protocol and requiring that the type object of the instance has a None value for the corresponding attribute? I think that this would be unlikely to cause problems and would cover all important use cases. It's still possible to construct an example which doesn't work right, but that's acceptable since @runtime was never supposed to be 100% correct anyway.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
What about doing a callable() check on the attribute value defined in the protocol and requiring that the type object of the instance has a None value for the corresponding attribute?
OK, I will go with this (applying the None rule only to callable attributes, and check them on the class object). I will also open an issue in mypy, to do the same (instead of the ad-hoc dunder restriction).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| class Protocol(object): | ||
| """Base class for protocol classes. Protocol classes are defined as:: | ||
|
|
||
| class Proto(Protocol[T]): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I think that the first example should be non-generic protocol. They are likely much more common that generic ones. Also, somebody might imply from this that a protocol must be generic and is used like Generic.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| class Proto(Protocol[T]): | ||
| def meth(self): | ||
| # type: () -> int |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Maybe use T in the class body, since this is a generic protocol.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| try: | ||
| from typing import _type_vars, _next_in_mro, _type_check | ||
| except ImportError: | ||
| NO_PROTOCOL = True |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Add comment discussing when this happens and what it means.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| def _collection_protocol(cls): | ||
| # Selected set of collections ABCs that are considered protocols. | ||
| name = cls.__name__ | ||
| return (name in ('ABC', 'Callable', 'Awaitable', |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
See my comment about the similar function in the Python 2 implementation.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| class Protocol(metaclass=_ProtocolMeta): | ||
| """Base class for protocol classes. Protocol classes are defined as:: | ||
|
|
||
| class Proto(Protocol[T]): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
See my comments about the corresponding docstring in the Python 2 implementation. They are also relevant here.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@JukkaL Thanks for more review comments! I think they are now implemented. Summary:
I also opened two issues python/mypy#3938 and python/mypy#3939 to synchronize mypy behaviour. Please check if this PR can be now merged. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Everybody, I am going to try and review this tomorrow. I just read through all the discussion (whew!) to recover as much context as I can, hopefully it's still in my head tomorrow morning. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The README.rst for the typing_extensions package could use an edit to mention Protocols.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| try: | ||
| from typing import _check_generic | ||
| except ImportError: | ||
| def _check_generic(cls, parameters): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Why not just always define this locally? What do you get by importing it from typing (apart from the risk that a future version of typing changes this internal function in a way that breaks us)? Same for _no_slots_copy (though not for the other conditional imports -- I see their value).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This may be just an artefact from initial attempts to allow this on 3.5.1 with minimal changes, I will check now.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
(Whoops, hit return too soon. There's more to come.) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I have a few low-level nits, but basically I think we should just release this and iterate, rather than sitting on it forever in code review.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| self.__tree_hash__ = (hash(self._subs_tree()) if origin else | ||
| super(GenericMeta, self).__hash__()) | ||
| return self | ||
| if OLD_GENERICS: |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Why not just put the def inside if not OLD_GENERICS:? Just so you can copy/paste without reindenting? That feels a little weird to me.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I just wanted to save some horizontal space. But I see that it is easy to miss these two lines when scrolling through the code. I will probably just put all definitions inside (nested) if statements.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
|
|
||
| if NO_PROTOCOL: | ||
| del _ProtocolMeta |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Perhaps _ProtocolMeta should also be inside the if not NO_PROTOCOL: block?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| OLD_GENERICS else "Protocol[T]") | ||
|
|
||
|
|
||
| def runtime(cls): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I still don't like the @runtime name. Maybe @runtime_checkable? The longer this is the better, to discourage people from using it without understanding the consequences.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| return cls | ||
|
|
||
|
|
||
| if NO_PROTOCOL: |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Again it seems weird to define it and then delete it. (Also maybe the flag could be reverted and be named HAVE_PROTOCOLS?)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@gvanrossum Thanks for review, I think I have now addressed all your comments. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@gvanrossum Ah, sorry, I forgot one your comment, about the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I strongly prefer the longer name.
…On Sep 14, 2017 2:00 PM, "Ivan Levkivskyi" ***@***.***> wrote:
@gvanrossum <https://github.com/gvanrossum> Ah, sorry, I forgot one your
comment, about the @runtime name. Your version @runtime_checkable
actually looks OK. But what do you think about releasing typing_extensions
with @runtime and see how it will go?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#464 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ACwrMnwKqTCk30rbmrhw7ECcJwWD-Ai6ks5siZQHgaJpZM4O8UJj>
.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
OK, but then we will need to also update mypy and typeshed. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
...and the PEP. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
So be it, now's the time.
…On Sep 14, 2017 2:26 PM, "Ivan Levkivskyi" ***@***.***> wrote:
...and the PEP.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#464 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/ACwrMuKOtls0Yg8-zFW1Z0vU4go77u0Aks5siZmqgaJpZM4O8UJj>
.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
However I will merge now and we can iterate before the release. (Or after if you think it's too much work to rename @runtime to @runtime_checkable.)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
|
|
||
| if NO_PROTOCOL: | ||
| if not HAVE_PROTOCOLS: |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Arguably the entire class ProtocolTests should be inside if HAVE_PROTOCOLS: instead of this little block.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
TBH, I am quite exhausted after last two weeks, I would prefer to postpone everything which is not urgent. I you are OK releasing |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Understood. I am okay with releasing now, without more changes. We can debate the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
OK, I just uploaded |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
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
|
Is the rationale for disallowing |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Yes, still valid. See python/cpython#89138 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.