Skip to content

Commit a1882e8

Browse files
Add with_signature decorator.
Adds a new decorator for overriding the signature that introspection tools see for a wrapped callable, without mutating the wrapped function itself. Accepts a prototype callable, a prebuilt inspect.Signature, or a factory callable invoked with the wrapped function at decoration time. Derives __annotations__, __defaults__, __kwdefaults__, and the argument related attributes of __code__ from the same source so that tools which read those attributes directly stay consistent with inspect.signature. Replaces the need for the adapter argument of wrapt.decorator, which is planned for deprecation. Unlike adapter, this works as a standalone decorator that composes cleanly with other wrapt decorators.
1 parent 43a4d77 commit a1882e8

7 files changed

Lines changed: 984 additions & 74 deletions

File tree

docs/bundled.rst

Lines changed: 232 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,111 @@ is a usage reference for those bundled decorators. For worked examples of how
77
decorators of this kind can be constructed from scratch using **wrapt**, see
88
:doc:`examples`.
99

10+
LRU Cache
11+
---------
12+
13+
The ``functools.lru_cache`` decorator from the standard library works well
14+
for plain functions, but has several limitations when applied to instance
15+
methods:
16+
17+
* **Cache pollution** — because ``self`` is included as a cache key, all
18+
instances share the same ``maxsize`` budget. A cache with ``maxsize=128``
19+
shared across 100 instances gives roughly one entry per instance.
20+
21+
* **Garbage collection** — the cache holds strong references to ``self``
22+
through the cache keys, preventing instances from being garbage collected
23+
as long as they remain in the cache.
24+
25+
* **Hashability** — ``self`` must be hashable for the cache lookup to work.
26+
If a class defines ``__eq__`` without ``__hash__``, applying
27+
``functools.lru_cache`` to its methods will raise a ``TypeError``.
28+
29+
``wrapt.lru_cache`` addresses all three issues by maintaining a separate
30+
per-instance cache stored as an attribute on the instance itself. Each
31+
instance gets its own full ``maxsize`` budget, instances do not need to be
32+
hashable, and caches are automatically cleaned up when the instance is
33+
garbage collected. For plain functions, class methods, and static methods,
34+
a single shared cache is used, the same as ``functools.lru_cache``.
35+
36+
The decorator can be used with or without arguments, just like
37+
``functools.lru_cache``. All keyword arguments are passed through to the
38+
underlying ``functools.lru_cache``.
39+
40+
::
41+
42+
import wrapt
43+
44+
@wrapt.lru_cache
45+
def fibonacci(n):
46+
if n < 2:
47+
return n
48+
return fibonacci(n - 1) + fibonacci(n - 2)
49+
50+
@wrapt.lru_cache(maxsize=32)
51+
def factorial(n):
52+
return n * factorial(n - 1) if n else 1
53+
54+
The decorator works with instance methods, class methods, and static methods.
55+
56+
::
57+
58+
class MyClass:
59+
60+
@wrapt.lru_cache
61+
def compute(self, x):
62+
return x * 2
63+
64+
@wrapt.lru_cache(maxsize=32)
65+
@classmethod
66+
def class_compute(cls, x):
67+
return x * 3
68+
69+
@wrapt.lru_cache
70+
@staticmethod
71+
def static_compute(x):
72+
return x * 4
73+
74+
For instance methods, each instance maintains its own independent cache.
75+
76+
::
77+
78+
>>> obj1 = MyClass()
79+
>>> obj2 = MyClass()
80+
>>> obj1.compute(5)
81+
10
82+
>>> obj2.compute(5)
83+
10
84+
85+
Each instance has its own ``maxsize`` budget, so caching on one instance
86+
does not affect another.
87+
88+
The ``cache_info()``, ``cache_clear()``, and ``cache_parameters()`` methods
89+
are available directly on the decorated function. For instance methods,
90+
these operate on the per-instance cache for the bound instance.
91+
92+
::
93+
94+
>>> obj = MyClass()
95+
>>> obj.compute(5)
96+
10
97+
>>> obj.compute(5)
98+
10
99+
>>> obj.compute.cache_info()
100+
CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
101+
>>> obj.compute.cache_clear()
102+
103+
For plain functions, class methods, and static methods these operate on the
104+
single shared cache.
105+
106+
::
107+
108+
>>> fibonacci(10)
109+
55
110+
>>> fibonacci.cache_info()
111+
CacheInfo(hits=8, misses=11, maxsize=128, currsize=11)
112+
>>> fibonacci.cache_parameters()
113+
{'maxsize': 128, 'typed': False}
114+
10115
Thread Synchronization
11116
----------------------
12117

@@ -438,107 +543,161 @@ resulting calling convention to ``wrapt.synchronized``:
438543
def work(...):
439544
...
440545

441-
LRU Cache
442-
---------
546+
Signature Override
547+
------------------
443548

444-
The ``functools.lru_cache`` decorator from the standard library works well
445-
for plain functions, but has several limitations when applied to instance
446-
methods:
549+
``wrapt.with_signature`` overrides the signature that introspection tools
550+
see for a wrapped callable, without mutating the wrapped function itself.
551+
The wrapper still calls through to the wrapped function normally; only the
552+
signature reported by ``inspect.signature()``, ``inspect.getfullargspec()``,
553+
``help()``, and equivalent tools is substituted. Annotations, defaults,
554+
keyword defaults, and the argument-related attributes of ``__code__`` are
555+
all derived from the supplied signature so that tools which read these
556+
attributes directly stay consistent with ``inspect.signature()``.
447557

448-
* **Cache pollution** — because ``self`` is included as a cache key, all
449-
instances share the same ``maxsize`` budget. A cache with ``maxsize=128``
450-
shared across 100 instances gives roughly one entry per instance.
558+
This is the modern replacement for the ``adapter`` argument of
559+
``wrapt.decorator`` (see :doc:`decorators`). The older ``adapter``
560+
mechanism remains available but is planned for deprecation.
451561

452-
* **Garbage collection** — the cache holds strong references to ``self``
453-
through the cache keys, preventing instances from being garbage collected
454-
as long as they remain in the cache.
455-
456-
* **Hashability** — ``self`` must be hashable for the cache lookup to work.
457-
If a class defines ``__eq__`` without ``__hash__``, applying
458-
``functools.lru_cache`` to its methods will raise a ``TypeError``.
562+
Exactly one of the keyword arguments ``prototype=``, ``signature=``, or
563+
``factory=`` must be supplied. Supplying none, or more than one, raises
564+
``TypeError``.
459565

460-
``wrapt.lru_cache`` addresses all three issues by maintaining a separate
461-
per-instance cache stored as an attribute on the instance itself. Each
462-
instance gets its own full ``maxsize`` budget, instances do not need to be
463-
hashable, and caches are automatically cleaned up when the instance is
464-
garbage collected. For plain functions, class methods, and static methods,
465-
a single shared cache is used, the same as ``functools.lru_cache``.
566+
Providing a prototype function
567+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
466568

467-
The decorator can be used with or without arguments, just like
468-
``functools.lru_cache``. All keyword arguments are passed through to the
469-
underlying ``functools.lru_cache``.
569+
The most common form is to pass a prototype function whose signature is
570+
to be presented. The prototype's body is not executed; only its signature
571+
(including annotations) is used.
470572

471573
::
472574

473575
import wrapt
474576

475-
@wrapt.lru_cache
476-
def fibonacci(n):
477-
if n < 2:
478-
return n
479-
return fibonacci(n - 1) + fibonacci(n - 2)
577+
def _prototype(user: str, count: int = 1) -> bool: ...
480578

481-
@wrapt.lru_cache(maxsize=32)
482-
def factorial(n):
483-
return n * factorial(n - 1) if n else 1
579+
@wrapt.with_signature(prototype=_prototype)
580+
def function(*args, **kwargs):
581+
# The real implementation accepts (*args, **kwargs), but
582+
# introspection sees (user: str, count: int = 1) -> bool.
583+
...
484584

485-
The decorator works with instance methods, class methods, and static methods.
585+
The wrapped function is not modified. ``inspect.signature(function)``
586+
returns the prototype's signature, while
587+
``inspect.signature(function.__wrapped__)`` still returns the wrapped
588+
function's own ``(*args, **kwargs)``.
589+
590+
Providing a Signature object
591+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
592+
593+
If the signature is built programmatically, an ``inspect.Signature`` object
594+
can be supplied directly via ``signature=``.
486595

487596
::
488597

489-
class MyClass:
598+
import inspect
599+
import wrapt
490600

491-
@wrapt.lru_cache
492-
def compute(self, x):
493-
return x * 2
601+
sig = inspect.Signature(
602+
[
603+
inspect.Parameter(
604+
"user",
605+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
606+
annotation=str,
607+
),
608+
],
609+
return_annotation=bool,
610+
)
611+
612+
@wrapt.with_signature(signature=sig)
613+
def function(*args, **kwargs):
614+
...
494615

495-
@wrapt.lru_cache(maxsize=32)
496-
@classmethod
497-
def class_compute(cls, x):
498-
return x * 3
616+
Deriving the signature from the wrapped function
617+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
499618

500-
@wrapt.lru_cache
501-
@staticmethod
502-
def static_compute(x):
503-
return x * 4
619+
A factory callable can be supplied via ``factory=``. It is called at
620+
decoration time with the function being wrapped, and must return either
621+
an ``inspect.Signature`` or a prototype callable from which a signature
622+
will be derived. This form is the equivalent of ``wrapt.adapter_factory``
623+
in the legacy mechanism.
504624

505-
For instance methods, each instance maintains its own independent cache.
625+
::
626+
627+
def prepend_request_id(wrapped):
628+
s = inspect.signature(wrapped)
629+
return s.replace(
630+
parameters=[
631+
inspect.Parameter(
632+
"request_id",
633+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
634+
),
635+
*s.parameters.values(),
636+
]
637+
)
638+
639+
@wrapt.with_signature(factory=prepend_request_id)
640+
def function(a, b):
641+
...
642+
643+
Methods
644+
~~~~~~~
645+
646+
``with_signature`` handles instance methods, class methods, and static
647+
methods. For an instance method the prototype should include ``self``; the
648+
bound view has it stripped automatically, matching Python's built-in
649+
behaviour for ``inspect.signature(instance.method)``.
506650

507651
::
508652

509-
>>> obj1 = MyClass()
510-
>>> obj2 = MyClass()
511-
>>> obj1.compute(5)
512-
10
513-
>>> obj2.compute(5)
514-
10
653+
def _method_proto(self, value: int) -> int: ...
515654

516-
Each instance has its own ``maxsize`` budget, so caching on one instance
517-
does not affect another.
655+
class C:
518656

519-
The ``cache_info()``, ``cache_clear()``, and ``cache_parameters()`` methods
520-
are available directly on the decorated function. For instance methods,
521-
these operate on the per-instance cache for the bound instance.
657+
@wrapt.with_signature(prototype=_method_proto)
658+
def scale(self, *args, **kwargs):
659+
return args[0] * 10
660+
661+
# inspect.signature(C.scale) reports (self, value: int) -> int
662+
# inspect.signature(c.scale) reports (value: int) -> int
663+
664+
For class methods and static methods, ``with_signature`` can be stacked
665+
either above or below ``@classmethod`` / ``@staticmethod``; both orders
666+
produce correct introspection results. The conventional ordering is to
667+
place ``@with_signature`` on top.
522668

523669
::
524670

525-
>>> obj = MyClass()
526-
>>> obj.compute(5)
527-
10
528-
>>> obj.compute(5)
529-
10
530-
>>> obj.compute.cache_info()
531-
CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
532-
>>> obj.compute.cache_clear()
671+
class C:
533672

534-
For plain functions, class methods, and static methods these operate on the
535-
single shared cache.
673+
@wrapt.with_signature(prototype=_cm_proto)
674+
@classmethod
675+
def build(cls, *args, **kwargs):
676+
...
677+
678+
@wrapt.with_signature(prototype=_sm_proto)
679+
@staticmethod
680+
def twice(*args, **kwargs):
681+
...
682+
683+
Stacking under other decorators
684+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
685+
686+
When another **wrapt** decorator is placed on top of ``@with_signature``,
687+
the overridden signature is still reported by introspection on the outer
688+
wrapper. Annotations, defaults, keyword defaults, and the argument
689+
attributes of ``__code__`` all propagate upward through the wrapper
690+
chain.
536691

537692
::
538693

539-
>>> fibonacci(10)
540-
55
541-
>>> fibonacci.cache_info()
542-
CacheInfo(hits=8, misses=11, maxsize=128, currsize=11)
543-
>>> fibonacci.cache_parameters()
544-
{'maxsize': 128, 'typed': False}
694+
@wrapt.decorator
695+
def pass_through(wrapped, instance, args, kwargs):
696+
return wrapped(*args, **kwargs)
697+
698+
@pass_through
699+
@wrapt.with_signature(prototype=_prototype)
700+
def function(*args, **kwargs):
701+
...
702+
703+
# inspect.signature(function) still reports the prototype's signature.

docs/changes.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ their help is much appreciated.
8787
"Calling Convention Markers and Adapters" section of :doc:`bundled` for
8888
details.
8989

90+
* Added ``with_signature``, a decorator for overriding the signature that
91+
introspection tools see for a wrapped callable without mutating the wrapped
92+
function itself. The signature can be supplied as a prototype callable, a
93+
prebuilt ``inspect.Signature`` object, or a factory callable that derives
94+
the signature from the wrapped function at decoration time. Annotations,
95+
defaults, keyword defaults, and argument-related attributes of ``__code__``
96+
are all derived from the supplied signature so that tools which read those
97+
attributes directly stay consistent with ``inspect.signature()``. The
98+
override propagates correctly through outer wrapt decorators stacked on
99+
top, and is handled correctly for instance methods, class methods, and
100+
static methods. ``with_signature`` replaces the need for the ``adapter``
101+
argument of ``wrapt.decorator``, which is planned for deprecation in a
102+
future release. See the "Signature Override" section of :doc:`bundled`
103+
for details.
104+
90105
**Features Changed**
91106

92107
* Improved attribute access on ``BoundFunctionWrapper`` to delegate lookups to

docs/decorators.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,14 @@ original source code for the wrapped function.
436436
Signature Changing Decorators
437437
-----------------------------
438438

439+
.. note::
440+
441+
The ``adapter`` argument described in this section is planned for
442+
deprecation. New code should instead use the ``wrapt.with_signature``
443+
decorator, which provides the same signature override capability as a
444+
standalone decorator that composes cleanly with ``wrapt.decorator`` and
445+
other wrapt decorators. See :doc:`bundled` for details.
446+
439447
When using ``inspect.getargspec()`` the argument specification for the
440448
original wrapped function is returned. If however the decorator is a
441449
signature changing decorator, this is not going to be what is desired.

src/wrapt/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
wrap_object_attribute,
4444
)
4545
from .proxies import AutoObjectProxy, LazyObjectProxy, ObjectProxy, lazy_import
46+
from .signature import with_signature
4647
from .weakrefs import WeakFunctionProxy
4748

4849
__all__ = (
@@ -66,6 +67,7 @@
6667
"mark_as_sync",
6768
"sync_to_async",
6869
"synchronized",
70+
"with_signature",
6971
"discover_post_import_hooks",
7072
"notify_module_loaded",
7173
"register_post_import_hook",

0 commit comments

Comments
 (0)