From 00d547f266e617c16ea8f309ce27a9fabc466f36 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 27 Sep 2022 11:22:17 -0700 Subject: [PATCH] Talk about user defined protocols first (#13737) --- docs/source/protocols.rst | 415 +++++++++++++++++++------------------- 1 file changed, 210 insertions(+), 205 deletions(-) diff --git a/docs/source/protocols.rst b/docs/source/protocols.rst index 48530310c8cb..6c78a43389d9 100644 --- a/docs/source/protocols.rst +++ b/docs/source/protocols.rst @@ -55,11 +55,220 @@ For example, ``IntList`` below is iterable, over ``int`` values: print_numbered(x) # OK print_numbered([4, 5]) # Also OK -The subsections below introduce all built-in protocols defined in +:ref:`predefined_protocols_reference` lists all protocols defined in :py:mod:`typing` and the signatures of the corresponding methods you need to define to implement each protocol (the signatures can be left out, as always, but mypy won't type check unannotated methods). +Simple user-defined protocols +***************************** + +You can define your own protocol class by inheriting the special ``Protocol`` +class: + +.. code-block:: python + + from typing import Iterable + from typing_extensions import Protocol + + class SupportsClose(Protocol): + def close(self) -> None: + ... # Empty method body (explicit '...') + + class Resource: # No SupportsClose base class! + # ... some methods ... + + def close(self) -> None: + self.resource.release() + + def close_all(items: Iterable[SupportsClose]) -> None: + for item in items: + item.close() + + close_all([Resource(), open('some/file')]) # Okay! + +``Resource`` is a subtype of the ``SupportsClose`` protocol since it defines +a compatible ``close`` method. Regular file objects returned by :py:func:`open` are +similarly compatible with the protocol, as they support ``close()``. + +.. note:: + + The ``Protocol`` base class is provided in the ``typing_extensions`` + package for Python 3.4-3.7. Starting with Python 3.8, ``Protocol`` + is included in the ``typing`` module. + +Defining subprotocols and subclassing protocols +*********************************************** + +You can also define subprotocols. Existing protocols can be extended +and merged using multiple inheritance. Example: + +.. code-block:: python + + # ... continuing from the previous example + + class SupportsRead(Protocol): + def read(self, amount: int) -> bytes: ... + + class TaggedReadableResource(SupportsClose, SupportsRead, Protocol): + label: str + + class AdvancedResource(Resource): + def __init__(self, label: str) -> None: + self.label = label + + def read(self, amount: int) -> bytes: + # some implementation + ... + + resource: TaggedReadableResource + resource = AdvancedResource('handle with care') # OK + +Note that inheriting from an existing protocol does not automatically +turn the subclass into a protocol -- it just creates a regular +(non-protocol) class or ABC that implements the given protocol (or +protocols). The ``Protocol`` base class must always be explicitly +present if you are defining a protocol: + +.. code-block:: python + + class NotAProtocol(SupportsClose): # This is NOT a protocol + new_attr: int + + class Concrete: + new_attr: int = 0 + + def close(self) -> None: + ... + + # Error: nominal subtyping used by default + x: NotAProtocol = Concrete() # Error! + +You can also include default implementations of methods in +protocols. If you explicitly subclass these protocols you can inherit +these default implementations. Explicitly including a protocol as a +base class is also a way of documenting that your class implements a +particular protocol, and it forces mypy to verify that your class +implementation is actually compatible with the protocol. + +Recursive protocols +******************* + +Protocols can be recursive (self-referential) and mutually +recursive. This is useful for declaring abstract recursive collections +such as trees and linked lists: + +.. code-block:: python + + from typing import TypeVar, Optional + from typing_extensions import Protocol + + class TreeLike(Protocol): + value: int + + @property + def left(self) -> Optional['TreeLike']: ... + + @property + def right(self) -> Optional['TreeLike']: ... + + class SimpleTree: + def __init__(self, value: int) -> None: + self.value = value + self.left: Optional['SimpleTree'] = None + self.right: Optional['SimpleTree'] = None + + root: TreeLike = SimpleTree(0) # OK + +Using isinstance() with protocols +********************************* + +You can use a protocol class with :py:func:`isinstance` if you decorate it +with the ``@runtime_checkable`` class decorator. The decorator adds +support for basic runtime structural checks: + +.. code-block:: python + + from typing_extensions import Protocol, runtime_checkable + + @runtime_checkable + class Portable(Protocol): + handles: int + + class Mug: + def __init__(self) -> None: + self.handles = 1 + + def use(handles: int) -> None: ... + + mug = Mug() + if isinstance(mug, Portable): + use(mug.handles) # Works statically and at runtime + +:py:func:`isinstance` also works with the :ref:`predefined protocols ` +in :py:mod:`typing` such as :py:class:`~typing.Iterable`. + +.. note:: + :py:func:`isinstance` with protocols is not completely safe at runtime. + For example, signatures of methods are not checked. The runtime + implementation only checks that all protocol members are defined. + +.. _callback_protocols: + +Callback protocols +****************** + +Protocols can be used to define flexible callback types that are hard +(or even impossible) to express using the :py:data:`Callable[...] ` syntax, such as variadic, +overloaded, and complex generic callbacks. They are defined with a special :py:meth:`__call__ ` +member: + +.. code-block:: python + + from typing import Optional, Iterable + from typing_extensions import Protocol + + class Combiner(Protocol): + def __call__(self, *vals: bytes, maxlen: Optional[int] = None) -> list[bytes]: ... + + def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes: + for item in data: + ... + + def good_cb(*vals: bytes, maxlen: Optional[int] = None) -> list[bytes]: + ... + def bad_cb(*vals: bytes, maxitems: Optional[int]) -> list[bytes]: + ... + + batch_proc([], good_cb) # OK + batch_proc([], bad_cb) # Error! Argument 2 has incompatible type because of + # different name and kind in the callback + +Callback protocols and :py:data:`~typing.Callable` types can be used interchangeably. +Argument names in :py:meth:`__call__ ` methods must be identical, unless +a double underscore prefix is used. For example: + +.. code-block:: python + + from typing import Callable, TypeVar + from typing_extensions import Protocol + + T = TypeVar('T') + + class Copy(Protocol): + def __call__(self, __origin: T) -> T: ... + + copy_a: Callable[[T], T] + copy_b: Copy + + copy_a = copy_b # OK + copy_b = copy_a # Also OK + +.. _predefined_protocols_reference: + +Predefined protocol reference +***************************** + Iteration protocols ................... @@ -283,207 +492,3 @@ AsyncContextManager[T] traceback: Optional[TracebackType]) -> Awaitable[Optional[bool]] See also :py:class:`~typing.AsyncContextManager`. - -Simple user-defined protocols -***************************** - -You can define your own protocol class by inheriting the special ``Protocol`` -class: - -.. code-block:: python - - from typing import Iterable - from typing_extensions import Protocol - - class SupportsClose(Protocol): - def close(self) -> None: - ... # Empty method body (explicit '...') - - class Resource: # No SupportsClose base class! - # ... some methods ... - - def close(self) -> None: - self.resource.release() - - def close_all(items: Iterable[SupportsClose]) -> None: - for item in items: - item.close() - - close_all([Resource(), open('some/file')]) # Okay! - -``Resource`` is a subtype of the ``SupportsClose`` protocol since it defines -a compatible ``close`` method. Regular file objects returned by :py:func:`open` are -similarly compatible with the protocol, as they support ``close()``. - -.. note:: - - The ``Protocol`` base class is provided in the ``typing_extensions`` - package for Python 3.4-3.7. Starting with Python 3.8, ``Protocol`` - is included in the ``typing`` module. - -Defining subprotocols and subclassing protocols -*********************************************** - -You can also define subprotocols. Existing protocols can be extended -and merged using multiple inheritance. Example: - -.. code-block:: python - - # ... continuing from the previous example - - class SupportsRead(Protocol): - def read(self, amount: int) -> bytes: ... - - class TaggedReadableResource(SupportsClose, SupportsRead, Protocol): - label: str - - class AdvancedResource(Resource): - def __init__(self, label: str) -> None: - self.label = label - - def read(self, amount: int) -> bytes: - # some implementation - ... - - resource: TaggedReadableResource - resource = AdvancedResource('handle with care') # OK - -Note that inheriting from an existing protocol does not automatically -turn the subclass into a protocol -- it just creates a regular -(non-protocol) class or ABC that implements the given protocol (or -protocols). The ``Protocol`` base class must always be explicitly -present if you are defining a protocol: - -.. code-block:: python - - class NotAProtocol(SupportsClose): # This is NOT a protocol - new_attr: int - - class Concrete: - new_attr: int = 0 - - def close(self) -> None: - ... - - # Error: nominal subtyping used by default - x: NotAProtocol = Concrete() # Error! - -You can also include default implementations of methods in -protocols. If you explicitly subclass these protocols you can inherit -these default implementations. Explicitly including a protocol as a -base class is also a way of documenting that your class implements a -particular protocol, and it forces mypy to verify that your class -implementation is actually compatible with the protocol. - -Recursive protocols -******************* - -Protocols can be recursive (self-referential) and mutually -recursive. This is useful for declaring abstract recursive collections -such as trees and linked lists: - -.. code-block:: python - - from typing import TypeVar, Optional - from typing_extensions import Protocol - - class TreeLike(Protocol): - value: int - - @property - def left(self) -> Optional['TreeLike']: ... - - @property - def right(self) -> Optional['TreeLike']: ... - - class SimpleTree: - def __init__(self, value: int) -> None: - self.value = value - self.left: Optional['SimpleTree'] = None - self.right: Optional['SimpleTree'] = None - - root: TreeLike = SimpleTree(0) # OK - -Using isinstance() with protocols -********************************* - -You can use a protocol class with :py:func:`isinstance` if you decorate it -with the ``@runtime_checkable`` class decorator. The decorator adds -support for basic runtime structural checks: - -.. code-block:: python - - from typing_extensions import Protocol, runtime_checkable - - @runtime_checkable - class Portable(Protocol): - handles: int - - class Mug: - def __init__(self) -> None: - self.handles = 1 - - def use(handles: int) -> None: ... - - mug = Mug() - if isinstance(mug, Portable): - use(mug.handles) # Works statically and at runtime - -:py:func:`isinstance` also works with the :ref:`predefined protocols ` -in :py:mod:`typing` such as :py:class:`~typing.Iterable`. - -.. note:: - :py:func:`isinstance` with protocols is not completely safe at runtime. - For example, signatures of methods are not checked. The runtime - implementation only checks that all protocol members are defined. - -.. _callback_protocols: - -Callback protocols -****************** - -Protocols can be used to define flexible callback types that are hard -(or even impossible) to express using the :py:data:`Callable[...] ` syntax, such as variadic, -overloaded, and complex generic callbacks. They are defined with a special :py:meth:`__call__ ` -member: - -.. code-block:: python - - from typing import Optional, Iterable - from typing_extensions import Protocol - - class Combiner(Protocol): - def __call__(self, *vals: bytes, maxlen: Optional[int] = None) -> list[bytes]: ... - - def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes: - for item in data: - ... - - def good_cb(*vals: bytes, maxlen: Optional[int] = None) -> list[bytes]: - ... - def bad_cb(*vals: bytes, maxitems: Optional[int]) -> list[bytes]: - ... - - batch_proc([], good_cb) # OK - batch_proc([], bad_cb) # Error! Argument 2 has incompatible type because of - # different name and kind in the callback - -Callback protocols and :py:data:`~typing.Callable` types can be used interchangeably. -Argument names in :py:meth:`__call__ ` methods must be identical, unless -a double underscore prefix is used. For example: - -.. code-block:: python - - from typing import Callable, TypeVar - from typing_extensions import Protocol - - T = TypeVar('T') - - class Copy(Protocol): - def __call__(self, __origin: T) -> T: ... - - copy_a: Callable[[T], T] - copy_b: Copy - - copy_a = copy_b # OK - copy_b = copy_a # Also OK