Skip to content

Convert selected ABCs to Protocols #1220

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 2 commits into from
Nov 19, 2017
Merged

Conversation

ilevkivskyi
Copy link
Member

A basic set of updates to typing that converts selected ABCs to protocols, see PEP 544.

This accompanies python/mypy#3132 and python/typing#417
The tests will fail, so that this PR can be merged after the mypy one.
Then, if everything goes OK, we could convert more types in typeshed to protocols.

Attn: @JukkaL @ambv @gvanrossum

@@ -16,6 +16,7 @@ Type = object()
_promote = object()
no_type_check = object()
ClassVar = object()
Protocol = object()
Copy link
Member

Choose a reason for hiding this comment

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

I know we have a lot of similar definitions already, but I'd prefer to keep the stub closer to reality, maybe with just class Protocol: pass

Copy link
Member Author

@ilevkivskyi ilevkivskyi Apr 26, 2017

Choose a reason for hiding this comment

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

This form is actually necessary (in some sense) since mypy will prohibit any other operations with Protocol in runtime context, like:

Protocol()  # instantiating is a runtime error
Protocol[int]  # type application is a runtime error
def make_one(cls: type) -> Any: ...
make_one(Protocol)  # passing to a function is not a good idea
def fun(x: Protocol) -> int: # Using it in type context is not a good idea either
    ...

This could be fixed by additional special-casing, but a simpler way is just make it an object().

Copy link
Member

Choose a reason for hiding this comment

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

It's special either way: if I do Protocol = object() elsewhere, mypy probably won't let me do class SupportsFloat(Protocol): pass. Using a class is at least a little closer to what Protocol actually does at runtime, even if Protocol obviously isn't a normal class.

Copy link
Member Author

@ilevkivskyi ilevkivskyi Apr 26, 2017

Choose a reason for hiding this comment

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

Yes, but if I write class Protocol: ... then mypy will allow most things I mentioned above. This is why object() is used also for Generic and friends. Otherwise prohibiting all possible invalid scenarios will require a lot of effort.

Copy link
Member

Choose a reason for hiding this comment

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

Oh I see, I thought it would just ignore the implementation in the stub. Ignore this then.

@JelleZijlstra
Copy link
Member

This looks good to merge once the mypy PR is in.

I fixed a merge conflict that happened because I added typing.ContextManager. Not sure though if it should have the @runtime decorator.

@ilevkivskyi
Copy link
Member Author

Not sure though if it should have the @runtime decorator.

Yes, this is a runtime protocol (since contextlib.AbstractContextManager supports runtime structural checks via __subclasshook__). Also AsyncConetxtManager should be marked as @runtime when added.

@JukkaL JukkaL mentioned this pull request Aug 10, 2017
@JukkaL
Copy link
Contributor

JukkaL commented Aug 10, 2017

A few notes:

  • Please don't merge this until you get an okay here from me, even if the corresponding mypy PR is merged.
  • I think that the explicit SupportsFoo base classes in the stubs for builtins (for example, SupportsInt in int) should be removed. The rationale is that they are unnecessary as these are now protocols, and they also confuse mypy type inference as implemented in Protocols mypy#3132 by generating inferred types such as List[SupportsInt] instead List[object] that are unexpected.

@ilevkivskyi
Copy link
Member Author

OK, I have updated builtins (removed the Supports* bases).

@ilevkivskyi
Copy link
Member Author

OK, now all tests pass except for one. We are now in an interesting situation, since pythoneval tests are run with full stubs, this means that I cannot fix this in mypy before this one is merged and vice-versa. This means that either mypy of typeshed would need to merge a failing PR or this will involve a long loop of PRs with temporarily disabling this test in mypy then merging this PR and then re-enabling this one test again. @JelleZijlstra how such situations are typically treated?

I think this can be considered unblocked and ready for review, since python/mypy#3132 is now merged. Also, we could think now what are other classes in stdlib that may be made protocols (maybe PathLike?), and where removing an explicit bases could improve situation (for example, currently mypy does not attempt structural check, if there is an explicit base, and subtype check fails because of type variables, variance, etc.)

@@ -62,46 +63,58 @@ _KT_co = TypeVar('_KT_co', covariant=True) # Key type covariant containers.
_VT_co = TypeVar('_VT_co', covariant=True) # Value type covariant containers.
_T_contra = TypeVar('_T_contra', contravariant=True) # Ditto contravariant.

class SupportsInt(metaclass=ABCMeta):
def runtime(cls: _T) -> _T: ...
Copy link
Member

Choose a reason for hiding this comment

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

Should the argument be a TypeVar bound to Type[object] or even Type[Protocol], so that type checkers will give an error when this decorator is applied to something that's not a class?

Copy link
Member Author

Choose a reason for hiding this comment

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

Type[Protocol] will not work, since it does not appear explicitly in MRO, but Type[object] looks like a good idea.

def __aenter__(self) -> Awaitable[_T_co]: ...
def __aexit__(self, exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType]) -> Awaitable[Optional[bool]]: ...

class Mapping(_Collection[_KT], Generic[_KT, _VT_co]):
@runtime # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need the # type: ignore here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, it looks like is is not necessary anymore. The point is that Mapping is not really covariant in value (see signature of get) and mypy is deliberately very strict about such situations with protocols, however it looks like treatment of unions has changed since this was added. I will remove it just in case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Weird, somehow this works in Python 3 but fails in Python 2, will investigate later, have to run now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, found it, the return types for values() etc. are list[_VT_co], so on Python 2 it is indeed clearly invariant in value and we need #type: ignore.

@JelleZijlstra
Copy link
Member

I guess it makes the most sense to have the same person merge the mypy and typeshed changes at approximately the same time. I believe Jukka has push privileges to typeshed, so it might make the most sense for him to do it. I could also take care of doing the actual merges if Jukka or another mypy core team member approves the corresponding mypy PR.

As for uses of protocols, my recent experience has been that protocols would be mostly useful in IO-related argument types, where now we often have to write IO when really we only need an object with a .read() method. I'm on really bad airport wifi now so I can't look up examples easily.

@ilevkivskyi
Copy link
Member Author

@JelleZijlstra @JukkaL OK, here is the corresponding mypy PR with an updated test output python/mypy#3846

@ilevkivskyi
Copy link
Member Author

@JelleZijlstra @JukkaL
OK, I have removed Sequence and Mapping from this PR as @gvanrossum proposed in python/typing#11 (comment) so this PR can be merged now. Sequence and Mapping can be considered later, since they are not yet protocols at runtime.

The logistic issue here is that this PR and python/mypy#3846 mutually depend on each other. As proposed above the simplest way would be to just merge them simultaneously.

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.

See also my suggestion at python/mypy#3846 (comment) on how to avoid the mutual dependency between the two PRs.

Finally, can you rebase? There are no merge conflicts but it's really old, which surprised me when testing this manually. (OTOH the manual test on internal codebases found no new issues, so green light from that POV.)

class SupportsInt(metaclass=ABCMeta):
def runtime(cls: _TC) -> _TC: ...

@runtime
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a stub, why does it need @runtime?

Copy link
Member Author

Choose a reason for hiding this comment

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

Otherwise mypy will complain about e.g. issubclass(it, collections.abc.Iterable) something like isinstance() can be used only with @runtime protocols.


class SupportsInt(metaclass=ABCMeta):
def runtime(cls: _TC) -> _TC: ...
Copy link
Member

Choose a reason for hiding this comment

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

IIUC @runtime is not defined (yet) in typing.py but (only) in typing_extensions.py?

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, and the same for Protocol (there is only old internal _Protocol in typing). We however need them in typeshed to avoid circular dependencies with typing_extensions. We discussed this with @JukkaL few moths ago, the plan (which I like) is this:

  1. Iterate on PR Make python 2 subprocess consistent with python 3 #3132 until it's approved.
  2. Publish Protocol in typing_extensions. Also include it in the stub for typing_extensions.
  3. Merge PR Make python 2 subprocess consistent with python 3 #3132 but don't merge typeshed changes yet. This lets mypy users (at least those who use GitHub master) experiment with user-defined protocols.
  4. Get an okay from PyCharm and Pytype that they are fine with merging the typeshed PR. They might need to implement some support for the Protocol special form first, but if they don't use the typing typeshed stub this may be a non-issue.
  5. If major issues are found in the mypy implementation of protocols, fix them.
  6. Release a new mypy version with protocols as an experimental feature!
  7. Merge the typeshed PR and use it in mypy.
  8. Seek acceptance of the protocols PEP once we have some practical experience and protocols seem to work fine.
  9. Officially support using protocols in typeshed once Pytype and PyCharm are okay with it.
  10. Release Protocol in Python stdlib typing module.

This PR is step number 7

@ilevkivskyi
Copy link
Member Author

@gvanrossum OK, I rebased (but also squashed, since there were previous merge conflicts I don't want to fix them all again).

@ilevkivskyi
Copy link
Member Author

OK, all tests pass now, this is ready to be merged.

@gvanrossum gvanrossum merged commit ec2cb8e into python:master Nov 19, 2017
@ilevkivskyi ilevkivskyi deleted the protocols branch November 19, 2017 17:51
ilevkivskyi added a commit to python/mypy that referenced this pull request Nov 19, 2017
…ols) (#3846)

This PR contains the tests output after python/typeshed#1220 is merged
and synced (note that pythoneval tests are run with full typeshed stubs).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants