-
Notifications
You must be signed in to change notification settings - Fork 257
Protocols in typing extensions #464
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 in typing extensions #464
Conversation
@ilevkivskyi -- idk how much I'll be able to contribute here, but I can definitely take a look sometime during the next day or two! |
Great! |
if gvars is not None: | ||
raise TypeError( | ||
"Cannot inherit from Generic[...] or" | ||
" Protocol[...] multiple types.") |
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 mean "multiple times"?
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 just use the same wording as in typing
for consistency, but I actually agree that "multiple times" is more straightforward.
'_abcoll', 'abc')) | ||
|
||
|
||
class ProtocolMeta(GenericMeta): |
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 this be private?
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.
Probably yes, this makes sense not to expose it (I think we can't give guarantees about ProtocolMeta
anyway). I will update now.
if gvars is not None: | ||
raise TypeError( | ||
"Cannot inherit from Generic[...] or" | ||
" Protocol[...] multiple types.") |
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.
"multiple times" (I think)
Would it really be too hard to support typing.py 3.5.1? |
Unfortunately yes, as you remember, generics were completely redesigned in #195 so that basically it would be easier to just have a completely separate implementation of class Proto(Protocol, Generic[T]):
... but this shorthand will error in 3.5.1 class Proto(Protocol[T]):
... This will be very easy to implement. |
As parts of Dropbox are still stuck on Python 3.5.1, I think the compromise version would be great to have. |
@gvanrossum OK, I will do this (but most probably not today or tomorrow). |
No hurry. When you do, will you also add a Travis-CI check to run the tests under 3.5.1? |
The checks for |
(Actually all tests, not only |
Ah, great. (Though maybe we should add 3.6.2 now that it's out.)
|
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.
Looks good to me. Thanks @ilevkivskyi!
The stub for typing_extensions
is likely to be required in the Typeshed for checking protocols statically.
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.
|
@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}' |
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 did a quick review pass and some manual experimentation.
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.
Choose a reason for 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?
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 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.
# 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.
Choose a reason for 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?
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 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.
for attr in self._get_protocol_attrs()) | ||
return False | ||
|
||
def __subclasscheck__(self, cls): |
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 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).
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.
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).
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.
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".
if '__subclasshook__' not in cls.__dict__: | ||
cls.__subclasshook__ = classmethod(_proto_hook) | ||
|
||
def __instancecheck__(self, instance): |
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 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?
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 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.
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 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.)
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 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?
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.
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.
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 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).
class Protocol(object): | ||
"""Base class for protocol classes. Protocol classes are defined as:: | ||
|
||
class Proto(Protocol[T]): |
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 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
.
|
||
class Proto(Protocol[T]): | ||
def meth(self): | ||
# type: () -> 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 use T
in the class body, since this is a generic protocol.
try: | ||
from typing import _type_vars, _next_in_mro, _type_check | ||
except ImportError: | ||
NO_PROTOCOL = True |
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 discussing when this happens and what it means.
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.
Choose a reason for 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.
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.
Choose a reason for 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.
@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. |
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. |
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 README.rst for the typing_extensions package could use an edit to mention Protocols.
try: | ||
from typing import _check_generic | ||
except ImportError: | ||
def _check_generic(cls, parameters): |
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.
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).
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 may be just an artefact from initial attempts to allow this on 3.5.1 with minimal changes, I will check now.
(Whoops, hit return too soon. There's more to come.) |
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 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.
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.
Choose a reason for 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.
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 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.
|
||
|
||
if NO_PROTOCOL: | ||
del _ProtocolMeta |
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.
Perhaps _ProtocolMeta
should also be inside the if not NO_PROTOCOL:
block?
OLD_GENERICS else "Protocol[T]") | ||
|
||
|
||
def runtime(cls): |
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 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.
return cls | ||
|
||
|
||
if NO_PROTOCOL: |
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 seems weird to define it and then delete it. (Also maybe the flag could be reverted and be named HAVE_PROTOCOLS
?)
@gvanrossum Thanks for review, I think I have now addressed all your comments. |
@gvanrossum Ah, sorry, I forgot one your comment, about the |
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>
.
|
OK, but then we will need to also update mypy and typeshed. |
...and the PEP. |
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>
.
|
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.
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.)
@@ -1188,7 +1188,7 @@ class E: | |||
self.assertIsInstance(E(), D) | |||
|
|||
|
|||
if NO_PROTOCOL: | |||
if not HAVE_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.
Arguably the entire class ProtocolTests
should be inside if HAVE_PROTOCOLS:
instead of this little block.
TBH, I am quite exhausted after last two weeks, I would prefer to postpone everything which is not urgent. I you are OK releasing |
Understood. I am okay with releasing now, without more changes. We can debate the |
OK, I just uploaded |
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 |
Yes, still valid. See python/cpython#89138 |
This PR is essentially an adaptation of #417, here are some comments:
Iterable
etc. will be kept nominal for some time.typing
, but it looks like 3.5.0 and 3.5.1 are too hard.Protocol
really needs to be special in many aspects (likeGeneric
).@Michael0x2a @vlasovskikh I will be grateful for a code review and/or playing with this and commenting whether the behaviour matches your expectations.
cc @JukkaL @ambv @gvanrossum