| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
@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! |
Sorry, something went wrong.
Great! |
Sorry, something went wrong.
| if gvars is not None: | ||
| raise TypeError( | ||
| "Cannot inherit from Generic[...] or" | ||
| " Protocol[...] multiple types.") |
There was a problem hiding this comment.
Do you mean "multiple times"?
Sorry, something went wrong.
There was a problem hiding this comment.
I just use the same wording as in typing for consistency, but I actually agree that "multiple times" is more straightforward.
Sorry, something went wrong.
| '_abcoll', 'abc')) | ||
|
|
||
|
|
||
| class ProtocolMeta(GenericMeta): |
There was a problem hiding this comment.
Should this be private?
Sorry, something went wrong.
There was a problem hiding this comment.
Probably yes, this makes sense not to expose it (I think we can't give guarantees about ProtocolMeta anyway). I will update now.
Sorry, something went wrong.
| if gvars is not None: | ||
| raise TypeError( | ||
| "Cannot inherit from Generic[...] or" | ||
| " Protocol[...] multiple types.") |
There was a problem hiding this comment.
"multiple times" (I think)
Sorry, something went wrong.
|
Would it really be too hard to support typing.py 3.5.1? |
Sorry, something went wrong.
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 _ProtocolMeta, Protocol, etc., under a huge if sys.version_info[:3] == (3, 5, 1): .... This is not impossible, but the main problem is that I already forgot how generics worked at the time of 3.5.1, so it will take time. A possible compromise would be to implement a simplified version of Protocol, so that this will be valid: 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. |
Sorry, something went wrong.
|
As parts of Dropbox are still stuck on Python 3.5.1, I think the compromise version would be great to have. |
Sorry, something went wrong.
|
@gvanrossum OK, I will do this (but most probably not today or tomorrow). |
Sorry, something went wrong.
|
No hurry. When you do, will you also add a Travis-CI check to run the tests under 3.5.1? |
Sorry, something went wrong.
The checks for typing_extensions are already run on all versions (including micro), in the current version of the PR I just skip the checks on 3.5.0. and on 3.5.1. |
Sorry, something went wrong.
|
(Actually all tests, not only typing_extensions, generally run on all micro versions, I just checked in .travis.yml.) |
Sorry, something went wrong.
|
Ah, great. (Though maybe we should add 3.6.2 now that it's out.)
|
Sorry, something went wrong.
There was a problem hiding this comment.
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.
|
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.
|
@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 @runtime protocols almost 100% safe.
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 TypeError as for example issubclass(1, int). Will fix this.
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 typing itself, I think I already have all the checks for wrong use of Protocol same as for Generic in the typing PR.
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 Iterable behave, for example: 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 True, even for static structural subtype checks it is safe to say that everything is a subtype of an empty protocol. I will open an issue at mypy tracker.
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 typing we could discuss this more.
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: intThe 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 a problem hiding this comment.
I did a quick review pass and some manual experimentation.
Sorry, something went wrong.
| 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.
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 a problem hiding this comment.
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.
| # 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.
It's not clear what this limitation implies. Can you make the comment more specific?
Sorry, something went wrong.
There was a problem hiding this comment.
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.
| for attr in self._get_protocol_attrs()) | ||
| return False | ||
|
|
||
| def __subclasscheck__(self, cls): |
There was a problem hiding this comment.
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) # ErrorAn 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 a problem hiding this comment.
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 a problem hiding this comment.
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.
| if '__subclasshook__' not in cls.__dict__: | ||
| cls.__subclasshook__ = classmethod(_proto_hook) | ||
|
|
||
| def __instancecheck__(self, instance): |
There was a problem hiding this comment.
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 a problem hiding this comment.
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 a problem hiding this comment.
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) # FalseI'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 a problem hiding this comment.
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 a problem hiding this comment.
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 a problem hiding this comment.
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.
| class Protocol(object): | ||
| """Base class for protocol classes. Protocol classes are defined as:: | ||
|
|
||
| class Proto(Protocol[T]): |
There was a problem hiding this comment.
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.
|
|
||
| class Proto(Protocol[T]): | ||
| def meth(self): | ||
| # type: () -> int |
There was a problem hiding this comment.
Maybe use T in the class body, since this is a generic protocol.
Sorry, something went wrong.
| try: | ||
| from typing import _type_vars, _next_in_mro, _type_check | ||
| except ImportError: | ||
| NO_PROTOCOL = True |
There was a problem hiding this comment.
Add comment discussing when this happens and what it means.
Sorry, something went wrong.
| 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.
See my comment about the similar function in the Python 2 implementation.
Sorry, something went wrong.
| 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.
See my comments about the corresponding docstring in the Python 2 implementation. They are also relevant here.
Sorry, something went wrong.
|
@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.
|
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 a problem hiding this comment.
The README.rst for the typing_extensions package could use an edit to mention Protocols.
Sorry, something went wrong.
| try: | ||
| from typing import _check_generic | ||
| except ImportError: | ||
| def _check_generic(cls, parameters): |
There was a problem hiding this comment.
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 a problem hiding this comment.
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.
|
(Whoops, hit return too soon. There's more to come.) |
Sorry, something went wrong.
There was a problem hiding this comment.
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.
| 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.
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 a problem hiding this comment.
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.
|
|
||
|
|
||
| if NO_PROTOCOL: | ||
| del _ProtocolMeta |
There was a problem hiding this comment.
Perhaps _ProtocolMeta should also be inside the if not NO_PROTOCOL: block?
Sorry, something went wrong.
| OLD_GENERICS else "Protocol[T]") | ||
|
|
||
|
|
||
| def runtime(cls): |
There was a problem hiding this comment.
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.
| return cls | ||
|
|
||
|
|
||
| if NO_PROTOCOL: |
There was a problem hiding this comment.
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.
|
@gvanrossum Thanks for review, I think I have now addressed all your comments. |
Sorry, something went wrong.
|
@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? |
Sorry, something went wrong.
|
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.
OK, but then we will need to also update mypy and typeshed. |
Sorry, something went wrong.
|
...and the PEP. |
Sorry, something went wrong.
|
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 a problem hiding this comment.
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.
|
|
||
|
|
||
| if NO_PROTOCOL: | ||
| if not HAVE_PROTOCOLS: |
There was a problem hiding this comment.
Arguably the entire class ProtocolTests should be inside if HAVE_PROTOCOLS: instead of this little block.
Sorry, something went wrong.
|
TBH, I am quite exhausted after last two weeks, I would prefer to postpone everything which is not urgent. I you are OK releasing typing_extensions as is, then I will be only happy. |
Sorry, something went wrong.
|
Understood. I am okay with releasing now, without more changes. We can debate the @runtime decorator name more when we have some experience. |
Sorry, something went wrong.
|
OK, I just uploaded typing_extensions on PyPI. |
Sorry, something went wrong.
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 issubclass still valid, 6 years later? Honestly, it would be pretty useful to have issubclass check ClassVar-attributes (and ignore non-ClassVar attributes). |
Sorry, something went wrong.
|
Yes, still valid. See python/cpython#89138 |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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.
cc @JukkaL @ambv @gvanrossum