diff --git a/zipp/_functools.py b/zipp/_functools.py index 91a26c7..d125003 100644 --- a/zipp/_functools.py +++ b/zipp/_functools.py @@ -19,7 +19,7 @@ def wrapper(self, *args, **kwargs): return wrapper -# from jaraco.functools 3.5.2 +# from jaraco.functools 3.8.1 CallableT = TypeVar("CallableT", bound=Callable[..., object]) @@ -29,6 +29,73 @@ def method_cache( [CallableT], CallableT ] = functools.lru_cache(), # type: ignore[assignment] ) -> CallableT: + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + def wrapper(self: object, *args: object, **kwargs: object) -> object: # it's the first call, replace the method with a cached, bound method bound_method: CallableT = types.MethodType( # type: ignore[assignment] @@ -41,8 +108,9 @@ def wrapper(self: object, *args: object, **kwargs: object) -> object: # Support cache clear even before cache has been created. wrapper.cache_clear = lambda: None # type: ignore[attr-defined] - return ( # type: ignore[return-value] - _special_method_cache(method, cache_wrapper) or wrapper + return ( + _special_method_cache(method, cache_wrapper) # type: ignore[return-value] + or wrapper )