File: typing.rst

package info (click to toggle)
python-wrapt 2.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 1,592 kB
  • sloc: python: 8,452; ansic: 2,978; makefile: 168; sh: 46
file content (247 lines) | stat: -rw-r--r-- 9,844 bytes parent folder | download
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(): ...