Skip to content

Commit

Permalink
Improve decorators documentation (add decorator factories) (#8336)
Browse files Browse the repository at this point in the history
* Previously only bare decorators were explained, but decorator
  factories are also fairly common in the real world.
* Added decorator examples to cheat sheet page.
* Explained difference in behavior for class decorators.
* Shortened (IMO) excessive example for bare decorators.
  • Loading branch information
intgr authored Jan 31, 2020
1 parent d640155 commit 150d492
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 12 deletions.
20 changes: 19 additions & 1 deletion docs/source/cheat_sheet.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ Functions
# type: (...) -> bool
<code>
When you're puzzled or when things are complicated
**************************************************

Expand Down Expand Up @@ -256,3 +255,22 @@ Miscellaneous
return sys.stdin
else:
return sys.stdout
Decorators
**********

Decorator functions can be expressed via generics. See
:ref:`declaring-decorators` for the more details.

.. code-block:: python
from typing import Any, Callable, TypeVar
F = TypeVar('F', bound=Callable[..., Any])
def bare_decorator(func): # type: (F) -> F
...
def decorator_args(url): # type: (str) -> Callable[[F], F]
...
20 changes: 19 additions & 1 deletion docs/source/cheat_sheet_py3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ Python 3 supports an annotation syntax for function declarations.
quux(3) # Fine
quux(__x=3) # Error
When you're puzzled or when things are complicated
**************************************************

Expand Down Expand Up @@ -311,3 +310,22 @@ Miscellaneous
# class of that name later on in the file
def f(foo: 'A') -> int: # Ok
...
Decorators
**********

Decorator functions can be expressed via generics. See
:ref:`declaring-decorators` for the more details.

.. code-block:: python
from typing import Any, Callable, TypeVar
F = TypeVar('F', bound=Callable[..., Any])
def bare_decorator(func: F) -> F:
...
def decorator_args(url: str) -> Callable[[F], F]:
...
71 changes: 61 additions & 10 deletions docs/source/generics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -522,14 +522,19 @@ Declaring decorators

One common application of type variable upper bounds is in declaring a
decorator that preserves the signature of the function it decorates,
regardless of that signature. Here's a complete example:
regardless of that signature.

Note that class decorators are handled differently than function decorators in
mypy: decorating a class does not erase its type, even if the decorator has
incomplete type annotations.

Here's a complete example of a function decorator:

.. code-block:: python
from typing import Any, Callable, TypeVar, Tuple, cast
FuncType = Callable[..., Any]
F = TypeVar('F', bound=FuncType)
F = TypeVar('F', bound=Callable[..., Any])
# A decorator that preserves the signature.
def my_decorator(func: F) -> F:
Expand All @@ -543,15 +548,8 @@ regardless of that signature. Here's a complete example:
def foo(a: int) -> str:
return str(a)
# Another.
@my_decorator
def bar(x: float, y: float) -> Tuple[float, float, bool]:
return (x, y, x > y)
a = foo(12)
reveal_type(a) # str
b = bar(3.14, 0)
reveal_type(b) # Tuple[float, float, bool]
foo('x') # Type check error: incompatible type "str"; expected "int"
From the final block we see that the signatures of the decorated
Expand All @@ -566,6 +564,59 @@ functions are typically small enough that this is not a big
problem. This is also the reason for the :py:func:`~typing.cast` call in the
``return`` statement in ``my_decorator()``. See :ref:`casts`.

.. _decorator-factories:

Decorator factories
-------------------

Functions that take arguments and return a decorator (also called second-order decorators), are
similarly supported via generics:

.. code-block:: python
from typing import Any, Callable, TypeVar
F = TypeVar('F', bound=Callable[..., Any])
def route(url: str) -> Callable[[F], F]:
...
@route(url='/')
def index(request: Any) -> str:
return 'Hello world'
Sometimes the same decorator supports both bare calls and calls with arguments. This can be
achieved by combining with :py:func:`@overload <typing.overload>`:

.. code-block:: python
from typing import Any, Callable, TypeVar, overload
F = TypeVar('F', bound=Callable[..., Any])
# Bare decorator usage
@overload
def atomic(__func: F) -> F: ...
# Decorator with arguments
@overload
def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ...
# Implementation
def atomic(__func: Callable[..., Any] = None, *, savepoint: bool = True):
def decorator(func: Callable[..., Any]):
... # Code goes here
if __func is not None:
return decorator(__func)
else:
return decorator
# Usage
@atomic
def func1() -> None: ...
@atomic(savepoint=False)
def func2() -> None: ...
Generic protocols
*****************

Expand Down

0 comments on commit 150d492

Please sign in to comment.