1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
|
Type Hinting
============
As of version 2.0.0, **wrapt** includes type hints for its public APIs to
improve interoperability with static type checkers (e.g. ``pyright``, ``mypy``).
The type metadata is available when running on Python 3.10 or later (the minimum
version the annotations target).
The type annotations aim to ensure type inference works correctly in the common
cases, but the dynamic nature of decorators and wrappers means that some
patterns cannot be expressed precisely. The type hints are designed to be
broadly compatible with the major type checkers, but there are some differences
in how they interpret certain constructs, so you may find that some things work
in one type checker but not another. In some situations you may still
need to add explicit annotations yourself, sometimes by introducing small
helper functions to guide the type checker.
Of the type checkers, ``pyright`` used within VS Code Pylance extension gives
the best experience. ``mypy`` and ``ty`` also work well in most situations, but
have some limitations when dealing with static methods of classes decorated with
**wrapt** decorators. The ``pyrefly`` type checker currently does not work
correctly with **wrapt** decorators applied to methods of classes. Problems with
``mypy``, ``ty`` and ``pyrefly`` may be due to limitations in those type
checkers rather than issues with the **wrapt** type hints. Many attempts have
been made to ensure compatibility, but it is not possible to guarantee that all
type checkers will work correctly in all situations due to differences in how
they interpret type hint constructs.
Always ensure you are using the most up to date version of a type checking tool.
If you encounter any issues, please report them on the **wrapt** issue tracker
so they can be investigated and suggested workarounds or fixes can be provided.
Function Decorators
-------------------
The **wrapt** module exposes two helpers for authoring function decorators:
``@decorator`` and ``@function_wrapper``. ``@function_wrapper`` is a lightweight
subset of ``@decorator`` intended for straightforward function wrapping; it
omits the advanced extension points in exchange for a smaller, simpler
surface. We will use ``@decorator`` in this discussion of type hints, but
``@function_wrapper`` is also fully type hinted and can be used in a similar way.
The most basic example of a function decorator is:
::
@wrapt.decorator
def pass_through(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
@pass_through
def add(a, b):
return a + b
result = add(2, 3)
In this example, because no type hints are provided, the type checker will not
be able to infer the types of the function's parameters or return value.
Adding type hints, the example becomes:
::
from typing import Any, Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
@wrapt.decorator
def pass_through(
wrapped: Callable[P, R],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> R:
return wrapped(*args, **kwargs)
@pass_through
def add(a: int, b: int) -> int:
return a + b
result: int = add(2, 3)
In this example, because the decorator simply passes through the call to the
wrapped function and is intended to be generic, it is necessary to use
``ParamSpec`` and ``TypeVar`` to express that the decorator can be applied to
functions with any signature and return type. The ``wrapped`` parameter is
annotated with ``Callable[P, R]`` to indicate that it accepts the same parameters
as the decorated function and returns the same type. The ``instance``, ``args``,
and ``kwargs`` parameters are annotated with appropriate generic types. The
return type of the decorator is annotated as ``R`` to match the return type of
the wrapped function.
With the type hints as shown, the type checker should be able to validate both the
arguments supplied to the decorated function and its return value at the point
it is being called. For example, the type checker will flag incorrect argument
types or an incompatible assigned result as in the following.
::
result: str = add("hello", "world") # Error: incompatible types
When ``@decorator`` or ``@function_wrapper`` are applied to a wrapper function,
the type checker should give an error when the return type of the wrapper
function is incompatible with the returned type given for the wrapped function.
The type checker will in some circumstances not appear to correctly verify that
the number and type of arguments of the wrapper function itself are what is
expected because of the decorators being able to be applied to a class, a class
instance, instance methods, class methods and normal functions. Thus there are
multiple valid signatures for the wrapper function depending on the context in
which the decorator is applied. As such, it may look like it does not flag
incorrect arguments as it matches a different but still valid signature.
No type checking is performed when the custom decorator is applied to a
function. This is because the decorator can be applied to any callable with
any signature, and the type checker cannot determine the correct signature
to use for the decorated function.
Signature Adapters
------------------
Sometimes you want the decorated callable to present a different public
signature from the underlying implementation (for example, to narrow the
parameters, rename them, or enforce keyword-only usage). You can express this
using a signature adapter: a small prototype function whose only purpose is
to declare the outward-facing signature the wrapped function should appear to
have after decoration.
For instance, imagine the original function returns an integer, but you want
the decorated function to return a string. You would define a signature adapter
like this:
::
def adapter_prototype(i: int) -> str: ...
@wrapt.decorator(adapter=wrapt.adapter_factory(adapter_prototype))
def int_to_str(wrapped, instance, args, kwargs):
return str(wrapped(*args, **kwargs))
@int_to_str
def function(x) -> int:
"""A function that takes an integer and returns it."""
return x
result = function(1)
In this example we passed the prototype function itself via the ``adapter``
argument. **wrapt** also supports alternative forms: you can supply the
prototype as a string, or return a pre-formatted argument spec instead of a
callable.
Declaring the adapter explicitly ensures that runtime introspection
(``inspect.signature``, ``help()``, IDE tooling, etc.) reports the adapted
signature rather than the underlying implementation detail. Because the
adaptation is applied dynamically (and the prototype may itself be generated
at runtime), the **wrapt** type hints will not work, and so you must use a
helper function.
::
def adapter_prototype(i: int) -> str: ...
def int_to_str(wrapped: Callable[[int], int]) -> Callable[[int], str]:
@wrapt.decorator(adapter=adapter_prototype)
def wrapper(
wrapped: Callable[[int], int],
instance: Any,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any:
return str(wrapped(*args, **kwargs))
return wrapper(wrapped)
@int_to_str
def function(x: int) -> int:
"""A function that takes an integer and returns it."""
return x
result: str = function(1)
In this version the outer helper function constructs the decorator and
added explicit type hints to its parameters and return type. This allows the
type checker to validate calls to the decorated function and propagate the
correct return type.
Note that inside the decorator body the ``wrapped`` callable is annotated
with signature of the functions to be wrapped. The return type of the wrapper
function does however need to be ``Any``. You could just as well omit
those inner annotations as what matters for most static checking is the
user facing signature exposed by the outer helper function.
Decorating Classes
------------------
Decorators can be applied to classes as well as functions and methods. when
applied to a class, the decorator object effectively replaces the original.
With the way the **wrapt** decorator works, it is still possible to use the
decorated class as a base class in an inheritance hierarchy, however, this
confuses the type checker.
::
@pass_through
class BaseClass:
def __init__(self): ...
# Error: type checker doesn't recognise the class as a base class.
class DerivedClass(BaseClass): # <-- Invalid error warning.
def __init__(self): ...
The type checker can also give invalid error warnings when using functions
such as ``issubclass()`` due to not recognising the decorated class as a
class type.
::
# Error: type checker doesn't recognise the class as a base class.
issubclass(DerivedClass, BaseClass) # <-- Invalid error warning.
Class as Decorator
------------------
Normally decorators are functions, but it is also possible to use a class as a
decorator. In this situation the wrapper function (``__call__()`` method of class)
is not type checked as it would be if the ``@decorator`` were being applied to it
directly. Further, the type checker cannot match the arguments for the
constructor of the class at the point it it is being created.
::
@wrapt.decorator
class ClassDecorator:
def __init__(self, arg: str): ...
# Error: type checker will not check arguments of wrapper function.
def __call__(self, wrapped, instance, args, kwargs): ... # <-- Not checked.
# Error: type checker doesn't recognise arguments correctly.
@ClassDecorator("string") # <-- Invalid error warning.
def function(): ...
|