@@ -267,6 +267,161 @@ def test_access_via_instance(self):
267267 self .assertEqual (info .misses , 1 )
268268
269269
270+ class OverrideBase :
271+ def __init__ (self ):
272+ self .base_calls = 0
273+ self .derived_calls = 0
274+
275+ @wrapt .lru_cache
276+ def compute (self , x ):
277+ self .base_calls += 1
278+ return x * 2
279+
280+
281+ class OverrideDerived (OverrideBase ):
282+ @wrapt .lru_cache
283+ def compute (self , x ):
284+ self .derived_calls += 1
285+ return super ().compute (x ) + 1
286+
287+
288+ class TestOverriddenMethodWithSuper (unittest .TestCase ):
289+ def test_super_call_returns_correct_result (self ):
290+ obj = OverrideDerived ()
291+ self .assertEqual (obj .compute (10 ), 21 )
292+
293+ def test_super_call_does_not_recurse (self ):
294+ # Regression test: a subclass method decorated with lru_cache that
295+ # called the base method via super() used to recurse forever because
296+ # the base and derived methods shared a single per-instance cache
297+ # slot derived from the method name alone.
298+ obj = OverrideDerived ()
299+ try :
300+ result = obj .compute (10 )
301+ except RecursionError :
302+ self .fail ("super() call recursed instead of reaching base method" )
303+ self .assertEqual (result , 21 )
304+
305+ def test_both_bodies_execute_once (self ):
306+ obj = OverrideDerived ()
307+ obj .compute (10 )
308+ self .assertEqual (obj .derived_calls , 1 )
309+ self .assertEqual (obj .base_calls , 1 )
310+
311+ def test_base_and_derived_cached_independently (self ):
312+ obj = OverrideDerived ()
313+ obj .compute (10 )
314+ obj .compute (10 )
315+ # Second call is served from both caches, so neither body re-runs.
316+ self .assertEqual (obj .derived_calls , 1 )
317+ self .assertEqual (obj .base_calls , 1 )
318+ info = obj .compute .cache_info ()
319+ self .assertEqual (info .hits , 1 )
320+ self .assertEqual (info .misses , 1 )
321+
322+ def test_base_class_instance_unaffected (self ):
323+ obj = OverrideBase ()
324+ self .assertEqual (obj .compute (10 ), 20 )
325+ self .assertEqual (obj .base_calls , 1 )
326+ self .assertEqual (obj .derived_calls , 0 )
327+
328+ def test_separate_instances_have_separate_caches (self ):
329+ obj1 = OverrideDerived ()
330+ obj2 = OverrideDerived ()
331+ obj1 .compute (10 )
332+ obj2 .compute (10 )
333+ self .assertEqual (obj1 .derived_calls , 1 )
334+ self .assertEqual (obj2 .derived_calls , 1 )
335+ self .assertEqual (obj1 .base_calls , 1 )
336+ self .assertEqual (obj2 .base_calls , 1 )
337+
338+
339+ class ProxyWrapped :
340+ def __init__ (self , a ):
341+ self .a = a
342+
343+
344+ class ProxyCached (wrapt .ObjectProxy ):
345+ @wrapt .lru_cache
346+ def compute (self , x ):
347+ return self .a + x
348+
349+
350+ class ProxyCachedWithState (wrapt .ObjectProxy ):
351+ def __init__ (self , wrapped , factor ):
352+ super ().__init__ (wrapped )
353+ self ._self_factor = factor
354+
355+ @wrapt .lru_cache
356+ def compute (self , x ):
357+ return self ._self_factor * x
358+
359+
360+ class SlottedWrapped :
361+ __slots__ = ("a" ,)
362+
363+ def __init__ (self , a ):
364+ self .a = a
365+
366+
367+ class TestObjectProxySubclass (unittest .TestCase ):
368+ def test_returns_correct_result (self ):
369+ obj = ProxyCached (ProxyWrapped (1 ))
370+ self .assertEqual (obj .compute (10 ), 11 )
371+
372+ def test_caching (self ):
373+ obj = ProxyCached (ProxyWrapped (1 ))
374+ obj .compute (10 )
375+ obj .compute (10 )
376+ info = obj .compute .cache_info ()
377+ self .assertEqual (info .hits , 1 )
378+ self .assertEqual (info .misses , 1 )
379+
380+ def test_cache_not_stored_on_wrapped_object (self ):
381+ wrapped = ProxyWrapped (1 )
382+ obj = ProxyCached (wrapped )
383+ obj .compute (10 )
384+ cache_attrs = [k for k in vars (wrapped ) if k .startswith ("_lru_cache_" )]
385+ self .assertEqual (cache_attrs , [])
386+
387+ def test_per_proxy_state_not_shared_for_same_wrapped_object (self ):
388+ # Two proxies over the same wrapped object must keep independent
389+ # per-instance caches keyed to their own proxy state, not a single
390+ # cache stored on the shared wrapped object.
391+ wrapped = ProxyWrapped (1 )
392+ obj1 = ProxyCachedWithState (wrapped , 2 )
393+ obj2 = ProxyCachedWithState (wrapped , 10 )
394+ self .assertEqual (obj1 .compute (5 ), 10 )
395+ self .assertEqual (obj2 .compute (5 ), 50 )
396+
397+ def test_proxy_garbage_collected_with_long_lived_wrapped (self ):
398+ # The cache must be stored on the proxy, not the wrapped object, so
399+ # a proxy is not kept alive by a wrapped object that outlives it.
400+ registry = []
401+
402+ def make_and_use ():
403+ backing = ProxyWrapped (7 )
404+ registry .append (backing )
405+ proxy = ProxyCached (backing )
406+ proxy .compute (4 )
407+ return weakref .ref (proxy )
408+
409+ ref = make_and_use ()
410+ gc .collect ()
411+ self .assertIsNone (ref ())
412+
413+ def test_wrapped_object_without_dict (self ):
414+ # A wrapped object that does not accept arbitrary attributes (for
415+ # example one using __slots__) must not cause the cache storage to
416+ # fail, since the cache is stored on the proxy.
417+ obj = ProxyCached (SlottedWrapped (1 ))
418+ self .assertEqual (obj .compute (10 ), 11 )
419+ self .assertEqual (obj .compute (10 ), 11 )
420+ info = obj .compute .cache_info ()
421+ self .assertEqual (info .hits , 1 )
422+ self .assertEqual (info .misses , 1 )
423+
424+
270425class TestIntrospection (unittest .TestCase ):
271426 def test_function_name (self ):
272427 self .assertEqual (cached_function .__name__ , "cached_function" )
0 commit comments