Skip to content

Improve decorators documentation (add decorator factories) #8336

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 1 commit into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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