Skip to content

Commit 9ee898a

Browse files
Restructure code and rename internal classes for consistency.
Split decorators.py into topic-focused modules: - caching.py for lru_cache and its supporting classes. - synchronization.py for synchronized, mark_as_sync, mark_as_async, async_to_sync, sync_to_async, and their supporting classes. decorators.py now holds only the decorator factory, adapter machinery, and bind_state_to_wrapper. Renamed non-public classes so they all follow the same convention: Qualifier + FunctionWrapper for unbound wrappers, with Bound prefix for the corresponding bound wrapper. Six previously-unprefixed internal classes gained underscore prefixes (AdapterFunctionWrapper and the Sync/Async FunctionWrapper pairs, plus DelegatedAdapterFactory). The two internal PartialDecorator classes in the synchronized lock path were renamed to SynchronizedLockProxy for clarity. StateBindingWrapper removed from the public API; the snake-case alias bind_state_to_wrapper is retained as the sole public name.
1 parent a1882e8 commit 9ee898a

9 files changed

Lines changed: 623 additions & 601 deletions

File tree

docs/changes.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,14 +1210,14 @@ Version 1.10.0
12101210

12111211
::
12121212

1213-
class DelegatedAdapterFactory(wrapt.AdapterFactory):
1213+
class _DelegatedAdapterFactory(wrapt.AdapterFactory):
12141214
def __init__(self, factory):
1215-
super(DelegatedAdapterFactory, self).__init__()
1215+
super(_DelegatedAdapterFactory, self).__init__()
12161216
self.factory = factory
12171217
def __call__(self, wrapped):
12181218
return self.factory(wrapped)
12191219

1220-
adapter_factory = DelegatedAdapterFactory
1220+
adapter_factory = _DelegatedAdapterFactory
12211221

12221222
**Bugs Fixed**
12231223

docs/decorators.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,14 +562,14 @@ example, ``wrapt.adapter_factory()`` is itself implemented as:
562562

563563
::
564564

565-
class DelegatedAdapterFactory(wrapt.AdapterFactory):
565+
class _DelegatedAdapterFactory(wrapt.AdapterFactory):
566566
def __init__(self, factory):
567-
super(DelegatedAdapterFactory, self).__init__()
567+
super(_DelegatedAdapterFactory, self).__init__()
568568
self.factory = factory
569569
def __call__(self, wrapped):
570570
return self.factory(wrapped)
571571

572-
adapter_factory = DelegatedAdapterFactory
572+
adapter_factory = _DelegatedAdapterFactory
573573

574574
Decorating Functions
575575
--------------------

src/wrapt/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
PartialCallableObjectProxy,
1414
partial,
1515
)
16+
from .caching import lru_cache
1617
from .decorators import (
1718
AdapterFactory,
18-
StateBindingWrapper,
1919
adapter_factory,
20-
async_to_sync,
2120
bind_state_to_wrapper,
2221
decorator,
23-
lru_cache,
22+
)
23+
from .synchronization import (
24+
async_to_sync,
2425
mark_as_async,
2526
mark_as_sync,
2627
sync_to_async,
@@ -57,7 +58,6 @@
5758
"PartialCallableObjectProxy",
5859
"partial",
5960
"AdapterFactory",
60-
"StateBindingWrapper",
6161
"adapter_factory",
6262
"bind_state_to_wrapper",
6363
"async_to_sync",

src/wrapt/__init__.pyi

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -403,45 +403,47 @@ if sys.version_info >= (3, 10):
403403

404404
# bind_state_to_wrapper()
405405

406-
class StateBindingWrapper:
406+
class _StateBindingWrapper:
407407
name: str
408408
wrapper_factory: Descriptor | None
409409
def __init__(self, *, name: str = "state") -> None: ...
410-
def __call__(self, wrapper_factory: Descriptor) -> StateBindingWrapper: ...
410+
def __call__(self, wrapper_factory: Descriptor) -> _StateBindingWrapper: ...
411411
def __get__(
412412
self, instance: Any, owner: type[Any] | None = None
413413
) -> (
414-
StateBindingWrapper
414+
_StateBindingWrapper
415415
| Callable[[Callable[..., Any]], FunctionWrapper[..., Any]]
416416
): ...
417417

418-
bind_state_to_wrapper = StateBindingWrapper
418+
bind_state_to_wrapper = _StateBindingWrapper
419419

420420
# lru_cache()
421421

422-
class _LRUCacheBoundWrapper(BoundFunctionWrapper[P1, R1]):
422+
class _BoundLRUCacheFunctionWrapper(BoundFunctionWrapper[P1, R1]):
423423
def cache_info(self) -> Any | None: ...
424424
def cache_clear(self) -> None: ...
425425
def cache_parameters(self) -> dict[str, Any] | None: ...
426426

427-
class _LRUCacheWrapper(FunctionWrapper[P1, R1]):
428-
__bound_function_wrapper__: type[_LRUCacheBoundWrapper[P1, R1]]
427+
class _LRUCacheFunctionWrapper(FunctionWrapper[P1, R1]):
428+
__bound_function_wrapper__: type[_BoundLRUCacheFunctionWrapper[P1, R1]]
429429
def cache_info(self) -> Any | None: ...
430430
def cache_clear(self) -> None: ...
431431
def cache_parameters(self) -> dict[str, Any] | None: ...
432432

433433
@overload
434-
def lru_cache(func: Callable[P, R], /) -> _LRUCacheWrapper[P, R]: ...
434+
def lru_cache(func: Callable[P, R], /) -> _LRUCacheFunctionWrapper[P, R]: ...
435435
@overload
436436
def lru_cache(
437437
func: None = None, /, **kwargs: Any
438-
) -> Callable[[Callable[P, R]], _LRUCacheWrapper[P, R]]: ...
438+
) -> Callable[[Callable[P, R]], _LRUCacheFunctionWrapper[P, R]]: ...
439439

440440
# with_signature()
441441

442442
def with_signature(
443443
*,
444444
prototype: Callable[..., Any] | None = None,
445445
signature: Signature | None = None,
446-
factory: Callable[[Callable[..., Any]], Signature | Callable[..., Any]] | None = None,
446+
factory: (
447+
Callable[[Callable[..., Any]], Signature | Callable[..., Any]] | None
448+
) = None,
447449
) -> Callable[[Callable[P, R]], FunctionWrapper[P, R]]: ...

src/wrapt/caching.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""Caching decorators. Currently provides ``lru_cache``, a drop-in
2+
replacement for ``functools.lru_cache`` with correct handling of instance
3+
methods: a separate cache is maintained per instance, stored as an
4+
attribute on the instance itself so it is cleaned up with the instance
5+
by the garbage collector. For plain functions, class methods, and static
6+
methods a single shared cache is used, matching ``functools.lru_cache``.
7+
"""
8+
9+
from functools import lru_cache as _functools_lru_cache
10+
from functools import partial
11+
12+
from .__wrapt__ import BoundFunctionWrapper, FunctionWrapper
13+
from .decorators import decorator
14+
from .synchronization import synchronized
15+
16+
# Decorator that applies functools.lru_cache to the wrapped function.
17+
# Unlike using functools.lru_cache directly, this works correctly with
18+
# instance methods and class methods by maintaining a separate cache
19+
# per instance or class. Each per-instance cache is stored as an
20+
# attribute on the instance itself, so it is automatically cleaned up
21+
# when the instance is garbage collected. For plain functions and
22+
# static methods, a single cache is stored on the wrapper itself.
23+
# The underlying caches are created lazily on first call, with
24+
# double-checked locking via synchronized to avoid races in
25+
# multi-threaded code.
26+
#
27+
# Custom FunctionWrapper and BoundFunctionWrapper subclasses are used
28+
# so that cache_info() and cache_clear() are available directly on
29+
# the decorated function. For bound methods, the BoundFunctionWrapper
30+
# uses _self_instance to locate the per-instance cache.
31+
32+
33+
class _BoundLRUCacheFunctionWrapper(BoundFunctionWrapper):
34+
35+
def _is_instance_method(self):
36+
return self._self_binding == "function"
37+
38+
def __call__(self, *args, **kwargs):
39+
parent = self._self_parent
40+
41+
# For class methods and static methods, use a single shared
42+
# cache stored on the parent wrapper. The cache is created
43+
# from the bound __wrapped__ since the parent's __wrapped__
44+
# is the raw classmethod/staticmethod descriptor.
45+
46+
if not self._is_instance_method():
47+
if parent._self_cache is None:
48+
with synchronized(parent):
49+
if parent._self_cache is None:
50+
parent._self_cache = _functools_lru_cache(
51+
**parent._self_lru_kwargs
52+
)(self.__wrapped__)
53+
54+
return parent._self_cache(*args, **kwargs)
55+
56+
# Instance method — per-instance cache stored as an attribute
57+
# on the instance so it is cleaned up with the instance by the
58+
# garbage collector.
59+
60+
instance = self._self_instance
61+
cache_attr = parent._self_cache_attr
62+
63+
cache = getattr(instance, cache_attr, None)
64+
65+
if cache is None:
66+
with synchronized(parent):
67+
cache = getattr(instance, cache_attr, None)
68+
69+
if cache is None:
70+
cache = _functools_lru_cache(**parent._self_lru_kwargs)(
71+
self.__wrapped__
72+
)
73+
74+
setattr(instance, cache_attr, cache)
75+
76+
return cache(*args, **kwargs)
77+
78+
def cache_info(self):
79+
"""Return the cache statistics for this binding's cache, or
80+
``None`` if the cache has not yet been created.
81+
"""
82+
83+
if not self._is_instance_method():
84+
return self._self_parent.cache_info()
85+
86+
cache = getattr(self._self_instance, self._self_parent._self_cache_attr, None)
87+
88+
if cache is not None:
89+
return cache.cache_info()
90+
91+
return None
92+
93+
def cache_clear(self):
94+
"""Clear this binding's cache."""
95+
96+
if not self._is_instance_method():
97+
self._self_parent.cache_clear()
98+
return
99+
100+
cache = getattr(self._self_instance, self._self_parent._self_cache_attr, None)
101+
102+
if cache is not None:
103+
cache.cache_clear()
104+
105+
def cache_parameters(self):
106+
"""Return the parameters used to create the cache."""
107+
108+
if not self._is_instance_method():
109+
return self._self_parent.cache_parameters()
110+
111+
cache = getattr(self._self_instance, self._self_parent._self_cache_attr, None)
112+
113+
if cache is not None:
114+
return cache.cache_parameters()
115+
116+
return None
117+
118+
119+
class _LRUCacheFunctionWrapper(FunctionWrapper):
120+
121+
__bound_function_wrapper__ = _BoundLRUCacheFunctionWrapper
122+
123+
def __init__(self, wrapped, wrapper, **kwargs):
124+
super().__init__(wrapped, wrapper, **kwargs)
125+
126+
# Extract the LRU cache configuration that was attached to the
127+
# wrapper function before it was passed to the decorator.
128+
129+
self._self_lru_kwargs = wrapper._self_lru_kwargs
130+
self._self_cache = None
131+
132+
# Use __func__ to get the name for classmethod/staticmethod
133+
# descriptors which lack __name__ on Python < 3.10.
134+
135+
name = getattr(wrapped, "__name__", None)
136+
137+
if name is None:
138+
name = wrapped.__func__.__name__
139+
140+
self._self_cache_attr = "_lru_cache_" + name
141+
142+
def __call__(self, *args, **kwargs):
143+
# Plain function or static method — single cache stored
144+
# on the wrapper itself.
145+
146+
if self._self_cache is None:
147+
with synchronized(self):
148+
if self._self_cache is None:
149+
self._self_cache = _functools_lru_cache(**self._self_lru_kwargs)(
150+
self.__wrapped__
151+
)
152+
153+
return self._self_cache(*args, **kwargs)
154+
155+
def cache_info(self):
156+
"""Return the cache statistics, or ``None`` if the cache has
157+
not yet been created.
158+
"""
159+
160+
if self._self_cache is not None:
161+
return self._self_cache.cache_info()
162+
163+
return None
164+
165+
def cache_clear(self):
166+
"""Clear the cache and reset the statistics."""
167+
168+
if self._self_cache is not None:
169+
self._self_cache.cache_clear()
170+
171+
def cache_parameters(self):
172+
"""Return the parameters used to create the cache, or ``None``
173+
if the cache has not yet been created.
174+
"""
175+
176+
if self._self_cache is not None:
177+
return self._self_cache.cache_parameters()
178+
179+
return None
180+
181+
182+
def lru_cache(func=None, /, **kwargs):
183+
"""A decorator that applies ``functools.lru_cache`` to the wrapped
184+
function, with correct handling for instance methods, class methods,
185+
and static methods.
186+
187+
For instance methods, a separate ``functools.lru_cache`` is
188+
maintained per instance. The cache is stored as an attribute on the
189+
instance itself, so it is automatically cleaned up when the instance
190+
is garbage collected. This means instances do not need to be
191+
hashable, each instance gets its own full ``maxsize`` budget, and
192+
no external mapping prevents garbage collection.
193+
194+
For plain functions, class methods, and static methods, a single
195+
shared cache is used.
196+
197+
All keyword arguments are passed through to ``functools.lru_cache``.
198+
199+
Cache management methods ``cache_info()`` and ``cache_clear()`` are
200+
available directly on the decorated function. For bound methods,
201+
these operate on the per-instance cache for the bound instance.
202+
"""
203+
204+
if func is None:
205+
return partial(lru_cache, **kwargs)
206+
207+
def _wrapper(wrapped, instance, args, _kwargs):
208+
return wrapped(*args, **_kwargs)
209+
210+
_wrapper._self_lru_kwargs = kwargs
211+
212+
return decorator(_wrapper, proxy=_LRUCacheFunctionWrapper)(func)

0 commit comments

Comments
 (0)