@@ -43,6 +43,13 @@ def call_static_with_arg(obj):
4343 return obj .compute (10 )
4444
4545
46+ @cinder_support .failUnlessJITCompiled
47+ def call_fromkeys (obj ):
48+ # `fromkeys` resolves to a classmethod_descriptor (a C-level classmethod)
49+ # when accessed via an instance.
50+ return obj .fromkeys ([1 , 2 ])
51+
52+
4653class LoadMethodCacheTests (unittest .TestCase ):
4754 def test_type_modified (self ):
4855 class Oracle :
@@ -404,6 +411,161 @@ class Oracle:
404411 self .assertEqual (get_meaning_of_life (obj ), 0 )
405412
406413
414+ class LoadMethodClassMethodTests (unittest .TestCase ):
415+ """LoadMethodCache should cache class methods found on the type. Both a
416+ Python-level classmethod and a C-level classmethod_descriptor are unwrapped
417+ to their underlying callable, cached, and bound to the receiver's type (not
418+ the receiver itself) when called.
419+ """
420+
421+ def test_class_method_on_type (self ):
422+ class Oracle :
423+ @classmethod
424+ def meaning_of_life (cls ):
425+ return 42
426+
427+ obj = Oracle ()
428+ # Uncached, then cached.
429+ self .assertEqual (get_meaning_of_life (obj ), 42 )
430+ self .assertEqual (get_meaning_of_life (obj ), 42 )
431+ for _ in range (100 ):
432+ self .assertEqual (get_meaning_of_life (obj ), 42 )
433+
434+ def test_class_method_binds_to_type (self ):
435+ class Oracle :
436+ @classmethod
437+ def meaning_of_life (cls ):
438+ return cls .__name__
439+
440+ obj = Oracle ()
441+ # cls is bound to the receiver's type, not the receiver.
442+ self .assertEqual (get_meaning_of_life (obj ), "Oracle" )
443+ self .assertEqual (get_meaning_of_life (obj ), "Oracle" )
444+
445+ def test_class_method_binds_to_most_derived_type (self ):
446+ class Base :
447+ @classmethod
448+ def meaning_of_life (cls ):
449+ return cls .__name__
450+
451+ class Derived (Base ):
452+ pass
453+
454+ # An inherited classmethod binds to the most-derived type of the
455+ # receiver. The two receiver types get independent cache entries.
456+ self .assertEqual (get_meaning_of_life (Base ()), "Base" )
457+ self .assertEqual (get_meaning_of_life (Derived ()), "Derived" )
458+ self .assertEqual (get_meaning_of_life (Base ()), "Base" )
459+ self .assertEqual (get_meaning_of_life (Derived ()), "Derived" )
460+
461+ def test_class_method_passes_args_after_cls (self ):
462+ class Oracle :
463+ @classmethod
464+ def compute (cls , x ):
465+ return x * 2
466+
467+ obj = Oracle ()
468+ # The explicit argument follows the implicit cls.
469+ self .assertEqual (call_static_with_arg (obj ), 20 )
470+ self .assertEqual (call_static_with_arg (obj ), 20 )
471+
472+ def test_class_method_type_modified (self ):
473+ class Oracle :
474+ @classmethod
475+ def meaning_of_life (cls ):
476+ return 42
477+
478+ obj = Oracle ()
479+ self .assertEqual (get_meaning_of_life (obj ), 42 )
480+ self .assertEqual (get_meaning_of_life (obj ), 42 )
481+
482+ # Replace with a different classmethod; the cache must be invalidated.
483+ # pyrefly: ignore [bad-assignment]
484+ Oracle .meaning_of_life = classmethod (lambda cls : 0 )
485+ self .assertEqual (get_meaning_of_life (obj ), 0 )
486+
487+ def test_class_method_base_modified (self ):
488+ class Base :
489+ @classmethod
490+ def meaning_of_life (cls ):
491+ return 42
492+
493+ class Derived (Base ):
494+ pass
495+
496+ obj = Derived ()
497+ self .assertEqual (get_meaning_of_life (obj ), 42 )
498+ self .assertEqual (get_meaning_of_life (obj ), 42 )
499+
500+ # Mutating the base should propagate to Derived and invalidate the cache.
501+ # pyrefly: ignore [bad-assignment]
502+ Base .meaning_of_life = classmethod (lambda cls : 0 )
503+ self .assertEqual (get_meaning_of_life (obj ), 0 )
504+
505+ def test_class_method_shadowed_by_instance (self ):
506+ class Oracle :
507+ @classmethod
508+ def meaning_of_life (cls ):
509+ return 42
510+
511+ obj = Oracle ()
512+ # Cache the classmethod first.
513+ self .assertEqual (get_meaning_of_life (obj ), 42 )
514+ self .assertEqual (get_meaning_of_life (obj ), 42 )
515+
516+ # A classmethod is a non-data descriptor, so an instance attribute
517+ # shadows it.
518+ # pyrefly: ignore [missing-attribute]
519+ obj .meaning_of_life = nothing
520+ self .assertEqual (get_meaning_of_life (obj ), 0 )
521+
522+ def test_class_method_with_getattr_defined (self ):
523+ """A classmethod present on the type is returned directly, not routed
524+ through __getattr__, even when the type defines __getattr__."""
525+
526+ class Oracle :
527+ @classmethod
528+ def meaning_of_life (cls ):
529+ return 42
530+
531+ def __getattr__ (self , name ):
532+ raise AttributeError (name )
533+
534+ obj = Oracle ()
535+ self .assertEqual (get_meaning_of_life (obj ), 42 )
536+ self .assertEqual (get_meaning_of_life (obj ), 42 )
537+ for _ in range (100 ):
538+ self .assertEqual (get_meaning_of_life (obj ), 42 )
539+
540+ def test_class_method_wrapping_non_function (self ):
541+ """A classmethod wrapping a non-function callable is not cached, but is
542+ still dispatched correctly through the descriptor."""
543+
544+ class Callable :
545+ def __call__ (self , cls ):
546+ return cls .__name__
547+
548+ class Oracle :
549+ meaning_of_life = classmethod (Callable ())
550+
551+ obj = Oracle ()
552+ self .assertEqual (get_meaning_of_life (obj ), "Oracle" )
553+ self .assertEqual (get_meaning_of_life (obj ), "Oracle" )
554+ for _ in range (100 ):
555+ self .assertEqual (get_meaning_of_life (obj ), "Oracle" )
556+
557+ def test_classmethod_descriptor_on_builtin (self ):
558+ """A C-level classmethod_descriptor (e.g. dict.fromkeys) accessed via an
559+ instance is cached and bound to the type."""
560+
561+ obj = {}
562+ expected = {1 : None , 2 : None }
563+ self .assertEqual (call_fromkeys (obj ), expected )
564+ self .assertEqual (call_fromkeys (obj ), expected )
565+ for _ in range (100 ):
566+ self .assertEqual (call_fromkeys (obj ), expected )
567+
568+
407569class LoadMethodGetAttrTests (unittest .TestCase ):
408570 """LoadMethodCache should support types that define __getattr__.
409571
0 commit comments