Skip to content

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

Merged
merged 35 commits into from
Sep 14, 2017

Conversation

ilevkivskyi
Copy link
Member

This PR is essentially an adaptation of #417, here are some comments:

  • There is only infrastructure for user-defined protocols, all existing ABCs like Iterable etc. will be kept nominal for some time.
  • I tried to support all versions of typing, but it looks like 3.5.0 and 3.5.1 are too hard.
  • There is some code duplication and recourse to private API, I tried to minimize this, but Protocol really needs to be special in many aspects (like Generic).
  • The implementation is on conservative/safe side with respect to structural subtyping, i.e. attributes should be actually defined, not just declared in a subclass, see examples in the description of Protocols #417

@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

@Michael0x2a
Copy link
Contributor

@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!

@ilevkivskyi
Copy link
Member Author

@Michael0x2a

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.")
Copy link
Member

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"?

Copy link
Member Author

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be private?

Copy link
Member Author

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.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"multiple times" (I think)

@gvanrossum
Copy link
Member

Would it really be too hard to support typing.py 3.5.1?

@ilevkivskyi
Copy link
Member Author

@gvanrossum

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 _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.

@gvanrossum
Copy link
Member

As parts of Dropbox are still stuck on Python 3.5.1, I think the compromise version would be great to have.

@ilevkivskyi
Copy link
Member Author

@gvanrossum OK, I will do this (but most probably not today or tomorrow).

@gvanrossum
Copy link
Member

No hurry. When you do, will you also add a Travis-CI check to run the tests under 3.5.1?

@ilevkivskyi
Copy link
Member Author

When you do, will you also add a Travis-CI check to run the tests under 3.5.1?

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.

@ilevkivskyi
Copy link
Member Author

ilevkivskyi commented Aug 21, 2017

(Actually all tests, not only typing_extensions, generally run on all micro versions, I just checked in .travis.yml.)

@gvanrossum
Copy link
Member

gvanrossum commented Aug 21, 2017 via email

Copy link
Member

@vlasovskikh vlasovskikh left a 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.

@Michael0x2a
Copy link
Contributor

Michael0x2a commented Aug 22, 2017

@ilevkivskyi:

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):

  1. Attempting to define the following class results in an AttributeError: type object 'Generic' has no attribute '_gorg' error message at runtime.

    class P1(Protocol, Generic[T]):
        def bar(self, x: T) -> str: ...

    Doing class Foo(List[str], Protocol): ... also resulted in the same kind of error.

    A little more broadly, I think it would be useful to add tests that intermix Protocol, Generic, and generic types in interesting ways.

  2. Doing isinstance(None, Empty) (where "Empty" is the empty protocol as it's defined in your tests) behaves inconsistently. At runtime, that expression always evaluates to true, but when running mypy, that expression evaluates to false when strict-optional mode is enabled.

    I tested this with the following program:

    @runtime
    class Empty(Protocol): pass
    
    x: str = "foo"
    if isinstance(None, Empty):
        # Raises type error only if strict-optional is disabled 
        # (binder skips this branch when strict optional is enabled
        x = 3    
    
        # Always prints at runtime
        print("bar")

    That said, @runtime isn't supposed to be fully sound anyways so maybe this is ok?

  3. The isinstance checks seem a little too flexible. For example:

    @runtime
    class P2(Protocol):
        def foo(self, x: int) -> int: ...
    
    class Concrete2:
        def foo(self, x: int) -> int: ...
    
    print(issubclass(Concrete2, P2))  # prints 'True', as expected
    print(isinstance(Concrete2, P2))  # also prints 'True'??

    I found the behavior of the last line to be surprising: I would have expected that to report 'false'.

  4. Using the same definitions as above -- doing issubclass(Concrete(), P2) results in the following error: AttributeError: 'Concrete' object has no attribute '__mro__'.

  5. This is probably overkill, but I think it might be nice if you checked that adding @runtime to a function and to a class that inherits a protocol (but isn't a protocol itself) both fail.

  6. I was able to define the following class without any runtime errors: class Bar(List[Protocol]): .... I was also able to define functions that used Protocol for the parameter or return types. Idk if we can do anything about the latter, but it might be worth adding the same checks ClassVar uses in the former case for consistency (and adding related tests).

    I guess we would need to wait until Protocol is formally added to the typing module first in order before we can fix this, though?

    (A related, but off-topic thought: the other approach could be to remove the checks for misuse of ClassVar: since static type checkers can easily catch those sorts of things, maybe it's ok to let people get away with some silliness at runtime?)

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.

  1. The @runtime decorator -- I think calling it @runtime is potentially too misleading. This is partly because I think it could fool people into thinking that the decorator enables the typing module to do full-fledged runtime type analysis. However, since mypy and the runtime error message they'll get makes it pretty clear this isn't the case, I guess this is only a minor UX qualm.

    The other reason is because I think it can mislead people into thinking that @runtime is more powerful then it is even when working with specifically protocols. For example, consider:

    @runtime
    class P3(Protocol):
        def foo(self, x: int) -> int: ...
    
    class Concrete3:
        def foo(self, x: str) -> str: return x
    
    print(isinstance(Concrete3(), P3))

    I know that this behavior is described in the documentation (and it made sense in retrospect), but even after reading it, I forgot and thought that the last line would print false instead of true.

    As a result, I think it might be worth renaming @runtime to a scarier, longer name to discourage these sorts of casual accidents? Maybe something like @enable_protocol_runtime_structural_checks or @enable_unsafe_protocol_runtime? Both names are probably too long, though.

    Or alternatively, it might be worth modifying the documentation to directly warn people that @runtime is unsound and that they should think carefully before using it? Basically, taking the blue box that's already there and making it scarier?

  2. It wasn't fully clear to me how the NoReturn type is supposed to interact with isinstance checks against the Empty protocol in general. Currently, doing any kind of variation of issubclass(NoReturn, Empty) (e.g. swapping issubclass with isinstance, swapping the NoReturn type with a call to a function that returns NoReturn) appears to evaluate at runtime to 'true'.

  3. Also maybe out-of-scope for this pull request, but it seems like how classes and protocols currently interact can accidentally encourage unsound behavior? For example, the following program type-checks with mypy, even with strict-optional enabled, but crashes at runtime:

    class P4(Protocol):
        def foo(self) -> int: ...
    
    class Bar(P4): pass
    
    b = Bar()
    x: int = b.foo()
    print(x + 3)  # Typechecks but crashes at runtime!

    The documentation says (or at least implies) that the way we created Bar is more or less another way to make an ABC. In that case, it would be nice if Bar behaved as if it were defined using the abc module (e.g. as if the methods were annotated with `abc.abstractmethod``) to prevent this sort of thing and guarantee protocols are sound by default.

  4. I think it would be handy to have more documentation regarding generic protocols in general. I wasn't aware that you could do things like class Foo(Protocol[T]) until I read through your unit tests, for example.

  5. Along the documentation vein: how are classvars and protocols supposed to interact? When I add a classvar to a protocol definition, is that classvar a part of the protocol? Also, I tried doing:

    class P5(Protocol):
        x: ClassVar[int] = ...
        y: int = ...

    Mypy reported an error for the definition of x ("Incompatible types in assignment (expression has type "ellipsis", variable has type "int")") but not for 'y' -- I'm not sure if that's a bug or intentional.

  6. Just to confirm, the ability to actually implement functions within a protocol class is intentional, right? In case the user wants to mix classical inheritance + nominal subtyping along with structural subtyping?

@ilevkivskyi
Copy link
Member Author

@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.

  1. Attempting to define the following class results in an AttributeError: type object 'Generic' has no attribute '_gorg' error message at runtime.

Thanks, good catch, will fix this and will add tests as you suggested.

  1. Doing isinstance(None, Empty) (where "Empty" is the empty protocol as it's defined in your tests) behaves inconsistently.

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.

  1. The isinstance checks seem a little too flexible. For example:

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.

  1. Using the same definitions as above -- doing issubclass(Concrete(), P2) results in the following error: AttributeError: 'Concrete' object has no attribute '__mro__'.

I think this should raise a TypeError as for example issubclass(1, int). Will fix this.

  1. This is probably overkill, but I think it might be nice if you checked that adding @runtime to a function and to a class that inherits a protocol (but isn't a protocol itself) both fail.

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
  1. I was able to define the following class without any runtime errors: class Bar(List[Protocol]): ... ... I guess we would need to wait until Protocol is formally added to the typing module first in order before we can fix this, though?

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.

  1. The @runtime decorator -- I think calling it @runtime is potentially too misleading.

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.

  1. It wasn't fully clear to me how the NoReturn type is supposed to interact with isinstance checks against the Empty protocol in general.

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.

  1. Also maybe out-of-scope for this pull request, but it seems like how classes and protocols currently interact can accidentally encourage unsound behavior?

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.

  1. I think it would be handy to have more documentation regarding generic protocols in general.

I think mypy already has some decent docs, when this will be added to typing we could discuss this more.

  1. Along the documentation vein: how are classvars and protocols supposed to interact?

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.

  1. Just to confirm, the ability to actually implement functions within a protocol class is intentional, right?

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}'

Copy link
Contributor

@JukkaL JukkaL left a 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',
Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Contributor

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?

Copy link
Member Author

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):
Copy link
Contributor

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).

Copy link
Member

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).

Copy link
Member Author

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):
Copy link
Contributor

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?

Copy link
Member Author

@ilevkivskyi ilevkivskyi Sep 1, 2017

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.

Copy link
Contributor

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.)

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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]):
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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',
Copy link
Contributor

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]):
Copy link
Contributor

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.

@ilevkivskyi
Copy link
Member Author

@JukkaL Thanks for more review comments! I think they are now implemented. Summary:

  • Prohibit issubclass() for protocols that declare non-method members (isinstance() is still OK).
  • Apply the None rule only to callable protocol members (methods).
  • Add more tests.

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.

@gvanrossum gvanrossum mentioned this pull request Sep 13, 2017
@gvanrossum
Copy link
Member

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.

Copy link
Member

@gvanrossum gvanrossum left a 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):
Copy link
Member

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).

Copy link
Member Author

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.

@gvanrossum
Copy link
Member

(Whoops, hit return too soon. There's more to come.)

Copy link
Member

@gvanrossum gvanrossum left a 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:
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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):
Copy link
Member

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:
Copy link
Member

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?)

@ilevkivskyi
Copy link
Member Author

@gvanrossum Thanks for review, I think I have now addressed all your comments.

@ilevkivskyi
Copy link
Member Author

@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?

@gvanrossum
Copy link
Member

gvanrossum commented Sep 14, 2017 via email

@ilevkivskyi
Copy link
Member Author

I strongly prefer the longer name.

OK, but then we will need to also update mypy and typeshed.

@ilevkivskyi
Copy link
Member Author

...and the PEP.

@gvanrossum
Copy link
Member

gvanrossum commented Sep 14, 2017 via email

Copy link
Member

@gvanrossum gvanrossum left a 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:
Copy link
Member

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.

@gvanrossum gvanrossum merged commit 5841d9b into python:master Sep 14, 2017
@ilevkivskyi ilevkivskyi deleted the protocols-typing-extensions branch September 15, 2017 11:40
@ilevkivskyi
Copy link
Member Author

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.

@gvanrossum
Copy link
Member

Understood. I am okay with releasing now, without more changes. We can debate the @runtime decorator name more when we have some experience.

@ilevkivskyi
Copy link
Member Author

OK, I just uploaded typing_extensions on PyPI.

gvanrossum pushed a commit to python/mypy that referenced this pull request Sep 29, 2017
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
@randolf-scholz
Copy link

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).

@hauntsaninja
Copy link
Collaborator

Yes, still valid. See python/cpython#89138
In general, please avoid commenting on long closed pull requests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants