@@ -261,5 +261,162 @@ def to_bool_2(instance):
261261 self ._check_specialization (to_bool_2 , H (), "TO_BOOL" , should_specialize = False )
262262
263263
264+ @support .cpython_only
265+ class PerTypeLookupCacheTests (unittest .TestCase ):
266+ """Tests for the per-type lookup cache."""
267+
268+ type_cache_lookup = staticmethod (_testinternalcapi .type_cache_lookup )
269+ type_cache_invalidate = staticmethod (_testinternalcapi .type_cache_invalidate )
270+
271+ def _make_type (self ):
272+ class C :
273+ x = "x-value"
274+ return C
275+
276+ def test_lookup_miss_on_empty_cache (self ):
277+ # A freshly-created type has not cached any names yet; the cache
278+ # should report a miss for an arbitrary name.
279+ C = self ._make_type ()
280+ hit , value , version = self .type_cache_lookup (C , "x" )
281+ self .assertEqual (hit , 0 )
282+ self .assertIsNone (value )
283+ self .assertEqual (version , 0 )
284+
285+ def test_lookup_hit_after_access (self ):
286+ # Reading an attribute goes through _PyType_Lookup which
287+ # caches the result. Subsequent lookups for the same name
288+ # should hit the cache.
289+ C = self ._make_type ()
290+ hit , value , version = self .type_cache_lookup (C , "x" )
291+ self .assertEqual (hit , 0 )
292+ attr = C .x
293+ hit , value , version = self .type_cache_lookup (C , "x" )
294+ self .assertEqual (hit , 1 )
295+ self .assertIs (value , attr )
296+ self .assertNotEqual (version , 0 )
297+ self .assertEqual (version , type_get_version (C ))
298+
299+ def test_lookup_caches_missing_name (self ):
300+ # _PyType_Lookup caches negative results too: a name that is not in
301+ # the MRO should still produce a cache hit with a None value.
302+ C = self ._make_type ()
303+ with self .assertRaises (AttributeError ):
304+ C .does_not_exist
305+ hit , value , _ = self .type_cache_lookup (C , "does_not_exist" )
306+ self .assertEqual (hit , 1 )
307+ self .assertIsNone (value )
308+
309+ def test_lookup_on_static_type (self ):
310+ # The cache for static types is stored on interpreter for isolation
311+ # between subinterpreters, test that cache works for them as well.
312+ self .type_cache_invalidate (int )
313+ self .assertEqual (self .type_cache_lookup (int , "bit_length" )[0 ], 0 )
314+ attr = int .bit_length
315+ hit , value , _ = self .type_cache_lookup (int , "bit_length" )
316+ self .assertEqual (hit , 1 )
317+ self .assertIs (value , attr )
318+
319+ def test_invalidate_clears_cache (self ):
320+ C = self ._make_type ()
321+ C .x # populate cache
322+ self .assertEqual (self .type_cache_lookup (C , "x" )[0 ], 1 )
323+
324+ self .type_cache_invalidate (C )
325+ hit , value , _ = self .type_cache_lookup (C , "x" )
326+ self .assertEqual (hit , 0 )
327+ self .assertIsNone (value )
328+
329+ def test_setattr_invalidates_cache (self ):
330+ # Mutating a type's attributes must invalidate any cached entries
331+ # for that type.
332+ C = self ._make_type ()
333+ C .x
334+ self .assertEqual (self .type_cache_lookup (C , "x" )[0 ], 1 )
335+
336+ C .x = "new-value"
337+ hit , _ , _ = self .type_cache_lookup (C , "x" )
338+ self .assertEqual (hit , 0 )
339+
340+ # The next access should re-populate the cache with the new value.
341+ self .assertEqual (C .x , "new-value" )
342+ hit , value , _ = self .type_cache_lookup (C , "x" )
343+ self .assertEqual (hit , 1 )
344+ self .assertEqual (value , "new-value" )
345+
346+ def test_setattr_on_subclass_preserves_base (self ):
347+ # Adding an attribute to a subclass changes the lookup result for
348+ # the subclass, so its cache must be invalidated, but the base's
349+ # cache for the same name stays valid.
350+ class Base :
351+ x = "base"
352+ class Sub (Base ):
353+ pass
354+
355+ self .assertEqual (Sub .x , "base" )
356+ self .assertEqual (Base .x , "base" )
357+ self .assertEqual (self .type_cache_lookup (Sub , "x" )[0 ], 1 )
358+ self .assertEqual (self .type_cache_lookup (Base , "x" )[0 ], 1 )
359+
360+ Sub .x = "sub"
361+ # Sub's cache should be invalidated.
362+ self .assertEqual (self .type_cache_lookup (Sub , "x" )[0 ], 0 )
363+ # Base is untouched.
364+ hit , value , _ = self .type_cache_lookup (Base , "x" )
365+ self .assertEqual (hit , 1 )
366+ self .assertEqual (value , "base" )
367+
368+ def test_setattr_on_base_invalidates_subclass (self ):
369+ class Base :
370+ x = "base"
371+ class Sub (Base ):
372+ pass
373+
374+ Sub .x
375+ self .assertEqual (self .type_cache_lookup (Sub , "x" )[0 ], 1 )
376+
377+ Base .x = "new-base"
378+ # Modifying the base must invalidate the subclass cache too.
379+ self .assertEqual (self .type_cache_lookup (Sub , "x" )[0 ], 0 )
380+
381+ def test_lookup_detects_stale_cache_version (self ):
382+ # The cache stores the type's tp_version_tag alongside its entries
383+ # and re-checks it after locating a hit. If the type version moves
384+ # forward without the cache being invalidated (the race window in
385+ # lock-free invalidation), the consistency check must downgrade
386+ # the hit to a miss.
387+ C = self ._make_type ()
388+ C .x # populate cache
389+ orig_version = type_get_version (C )
390+ self .assertNotEqual (orig_version , 0 )
391+ self .assertEqual (self .type_cache_lookup (C , "x" )[0 ], 1 )
392+
393+ # Bump the type version directly without touching the cache slot
394+ # (PyType_Modified would also invalidate, defeating the test).
395+ type_assign_specific_version_unsafe (C , orig_version + 1 )
396+ self .assertEqual (type_get_version (C ), orig_version + 1 )
397+
398+ hit , value , _ = self .type_cache_lookup (C , "x" )
399+ self .assertEqual (hit , 0 )
400+ self .assertIsNone (value )
401+
402+ def test_setattr_on_unrelated_type_preserves_cache (self ):
403+ # Modifying one type must not invalidate a sibling's cache.
404+ class A :
405+ x = "a"
406+ class B :
407+ x = "b"
408+
409+ A .x
410+ B .x
411+ self .assertEqual (self .type_cache_lookup (A , "x" )[0 ], 1 )
412+ self .assertEqual (self .type_cache_lookup (B , "x" )[0 ], 1 )
413+
414+ B .x = "b2"
415+ # A's cache is unaffected.
416+ hit , value , _ = self .type_cache_lookup (A , "x" )
417+ self .assertEqual (hit , 1 )
418+ self .assertEqual (value , "a" )
419+
420+
264421if __name__ == "__main__" :
265422 unittest .main ()
0 commit comments