@@ -7,6 +7,111 @@ is a usage reference for those bundled decorators. For worked examples of how
77decorators 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+
10115Thread 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.
0 commit comments