@@ -2313,5 +2313,171 @@ def f(*args):
23132313 with self .assertRaisesRegex (TypeError , msg ):
23142314 f ()
23152315
2316+
2317+ class CachedCostItem :
2318+ _cost = 1
2319+
2320+ def __init__ (self ):
2321+ self .lock = py_functools .RLock ()
2322+
2323+ @py_functools .cached_property
2324+ def cost (self ):
2325+ """The cost of the item."""
2326+ with self .lock :
2327+ self ._cost += 1
2328+ return self ._cost
2329+
2330+
2331+ class OptionallyCachedCostItem :
2332+ _cost = 1
2333+
2334+ def get_cost (self ):
2335+ """The cost of the item."""
2336+ self ._cost += 1
2337+ return self ._cost
2338+
2339+ cached_cost = py_functools .cached_property (get_cost )
2340+
2341+
2342+ class CachedCostItemWait :
2343+
2344+ def __init__ (self , event ):
2345+ self ._cost = 1
2346+ self .lock = py_functools .RLock ()
2347+ self .event = event
2348+
2349+ @py_functools .cached_property
2350+ def cost (self ):
2351+ self .event .wait (1 )
2352+ with self .lock :
2353+ self ._cost += 1
2354+ return self ._cost
2355+
2356+
2357+ class CachedCostItemWithSlots :
2358+ __slots__ = ('_cost' )
2359+
2360+ def __init__ (self ):
2361+ self ._cost = 1
2362+
2363+ @py_functools .cached_property
2364+ def cost (self ):
2365+ raise RuntimeError ('never called, slots not supported' )
2366+
2367+
2368+ class TestCachedProperty (unittest .TestCase ):
2369+ def test_cached (self ):
2370+ item = CachedCostItem ()
2371+ self .assertEqual (item .cost , 2 )
2372+ self .assertEqual (item .cost , 2 ) # not 3
2373+
2374+ def test_cached_attribute_name_differs_from_func_name (self ):
2375+ item = OptionallyCachedCostItem ()
2376+ self .assertEqual (item .get_cost (), 2 )
2377+ self .assertEqual (item .cached_cost , 3 )
2378+ self .assertEqual (item .get_cost (), 4 )
2379+ self .assertEqual (item .cached_cost , 3 )
2380+
2381+ def test_threaded (self ):
2382+ go = threading .Event ()
2383+ item = CachedCostItemWait (go )
2384+
2385+ num_threads = 3
2386+
2387+ orig_si = sys .getswitchinterval ()
2388+ sys .setswitchinterval (1e-6 )
2389+ try :
2390+ threads = [
2391+ threading .Thread (target = lambda : item .cost )
2392+ for k in range (num_threads )
2393+ ]
2394+ with support .start_threads (threads ):
2395+ go .set ()
2396+ finally :
2397+ sys .setswitchinterval (orig_si )
2398+
2399+ self .assertEqual (item .cost , 2 )
2400+
2401+ def test_object_with_slots (self ):
2402+ item = CachedCostItemWithSlots ()
2403+ with self .assertRaisesRegex (
2404+ TypeError ,
2405+ "No '__dict__' attribute on 'CachedCostItemWithSlots' instance to cache 'cost' property." ,
2406+ ):
2407+ item .cost
2408+
2409+ def test_immutable_dict (self ):
2410+ class MyMeta (type ):
2411+ @py_functools .cached_property
2412+ def prop (self ):
2413+ return True
2414+
2415+ class MyClass (metaclass = MyMeta ):
2416+ pass
2417+
2418+ with self .assertRaisesRegex (
2419+ TypeError ,
2420+ "The '__dict__' attribute on 'MyMeta' instance does not support item assignment for caching 'prop' property." ,
2421+ ):
2422+ MyClass .prop
2423+
2424+ def test_reuse_different_names (self ):
2425+ """Disallow this case because decorated function a would not be cached."""
2426+ with self .assertRaises (RuntimeError ) as ctx :
2427+ class ReusedCachedProperty :
2428+ @py_functools .cached_property
2429+ def a (self ):
2430+ pass
2431+
2432+ b = a
2433+
2434+ self .assertEqual (
2435+ str (ctx .exception .__context__ ),
2436+ str (TypeError ("Cannot assign the same cached_property to two different names ('a' and 'b')." ))
2437+ )
2438+
2439+ def test_reuse_same_name (self ):
2440+ """Reusing a cached_property on different classes under the same name is OK."""
2441+ counter = 0
2442+
2443+ @py_functools .cached_property
2444+ def _cp (_self ):
2445+ nonlocal counter
2446+ counter += 1
2447+ return counter
2448+
2449+ class A :
2450+ cp = _cp
2451+
2452+ class B :
2453+ cp = _cp
2454+
2455+ a = A ()
2456+ b = B ()
2457+
2458+ self .assertEqual (a .cp , 1 )
2459+ self .assertEqual (b .cp , 2 )
2460+ self .assertEqual (a .cp , 1 )
2461+
2462+ def test_set_name_not_called (self ):
2463+ cp = py_functools .cached_property (lambda s : None )
2464+ class Foo :
2465+ pass
2466+
2467+ Foo .cp = cp
2468+
2469+ with self .assertRaisesRegex (
2470+ TypeError ,
2471+ "Cannot use cached_property instance without calling __set_name__ on it." ,
2472+ ):
2473+ Foo ().cp
2474+
2475+ def test_access_from_class (self ):
2476+ self .assertIsInstance (CachedCostItem .cost , py_functools .cached_property )
2477+
2478+ def test_doc (self ):
2479+ self .assertEqual (CachedCostItem .cost .__doc__ , "The cost of the item." )
2480+
2481+
23162482if __name__ == '__main__' :
23172483 unittest .main ()
0 commit comments