From f46f4fd570a7a4bbf2fbeedc06df1f3a219a1bdb Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 00:55:05 +0300 Subject: [PATCH 1/8] gh-92810: Add atomic FT wrappers for uint64_t --- .../internal/pycore_pyatomic_ft_wrappers.h | 9 +++++++ Modules/_abc.c | 24 ++++--------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index 3e41e2fd1569ca..09540b613d2dbf 100644 --- a/Include/internal/pycore_pyatomic_ft_wrappers.h +++ b/Include/internal/pycore_pyatomic_ft_wrappers.h @@ -45,6 +45,8 @@ extern "C" { _Py_atomic_load_uint16_relaxed(&value) #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) \ _Py_atomic_load_uint32_relaxed(&value) +#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \ + _Py_atomic_load_uint64_relaxed(&value) #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \ _Py_atomic_load_ulong_relaxed(&value) #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \ @@ -61,6 +63,8 @@ extern "C" { _Py_atomic_store_uint16_relaxed(&value, new_value) #define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) \ _Py_atomic_store_uint32_relaxed(&value, new_value) +#define FT_ATOMIC_STORE_UINT64_RELAXED(value, new_value) \ + _Py_atomic_store_uint64_relaxed(&value, new_value) #define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) \ _Py_atomic_store_char_relaxed(&value, new_value) #define FT_ATOMIC_LOAD_CHAR_RELAXED(value) \ @@ -111,6 +115,8 @@ extern "C" { _Py_atomic_load_ullong_relaxed(&value) #define FT_ATOMIC_ADD_SSIZE(value, new_value) \ (void)_Py_atomic_add_ssize(&value, new_value) +#define FT_ATOMIC_ADD_UINT64(value, new_value) \ + (void)_Py_atomic_add_uint64(&value, new_value) #else #define FT_ATOMIC_LOAD_PTR(value) value @@ -126,6 +132,7 @@ extern "C" { #define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value +#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value @@ -134,6 +141,7 @@ extern "C" { #define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_UINT16_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) value = new_value +#define FT_ATOMIC_STORE_UINT64_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_LOAD_CHAR_RELAXED(value) value #define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_LOAD_UCHAR_RELAXED(value) value @@ -159,6 +167,7 @@ extern "C" { #define FT_ATOMIC_LOAD_ULLONG_RELAXED(value) value #define FT_ATOMIC_STORE_ULLONG_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_ADD_SSIZE(value, new_value) (void)(value += new_value) +#define FT_ATOMIC_ADD_UINT64(value, new_value) (void)(value += new_value) #endif diff --git a/Modules/_abc.c b/Modules/_abc.c index d6a953b336025d..883925aa71cdff 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -35,21 +35,13 @@ get_abc_state(PyObject *module) static inline uint64_t get_invalidation_counter(_abcmodule_state *state) { -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_uint64(&state->abc_invalidation_counter); -#else - return state->abc_invalidation_counter; -#endif + return FT_ATOMIC_LOAD_UINT64_RELAXED(state->abc_invalidation_counter); } static inline void increment_invalidation_counter(_abcmodule_state *state) { -#ifdef Py_GIL_DISABLED - _Py_atomic_add_uint64(&state->abc_invalidation_counter, 1); -#else - state->abc_invalidation_counter++; -#endif + FT_ATOMIC_ADD_UINT64(state->abc_invalidation_counter, 1); } /* This object stores internal state for ABCs. @@ -72,21 +64,13 @@ typedef struct { static inline uint64_t get_cache_version(_abc_data *impl) { -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_uint64(&impl->_abc_negative_cache_version); -#else - return impl->_abc_negative_cache_version; -#endif + return FT_ATOMIC_LOAD_UINT64_RELAXED(impl->_abc_negative_cache_version); } static inline void set_cache_version(_abc_data *impl, uint64_t version) { -#ifdef Py_GIL_DISABLED - _Py_atomic_store_uint64(&impl->_abc_negative_cache_version, version); -#else - impl->_abc_negative_cache_version = version; -#endif + FT_ATOMIC_STORE_UINT64_RELAXED(impl->_abc_negative_cache_version, version); } static int From 1329ef7a7599c12ed96ed579c75e63c043503d7f Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 00:55:52 +0300 Subject: [PATCH 2/8] gh-92810: Add more tests for ABC inheritance & registration --- Lib/test/test_abc.py | 234 +++++++++++++++++++++++++++++++----- Lib/test/test_isinstance.py | 22 ++++ 2 files changed, 225 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 80ee9e0ba56e75..04b5528a8f8c34 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -70,6 +70,25 @@ def foo(): return 4 class TestABC(unittest.TestCase): + def check_isinstance(self, obj, target_class): + self.assertIsInstance(obj, target_class) + self.assertIsInstance(obj, (target_class,)) + self.assertIsInstance(obj, target_class | target_class) + + def check_not_isinstance(self, obj, target_class): + self.assertNotIsInstance(obj, target_class) + self.assertNotIsInstance(obj, (target_class,)) + self.assertNotIsInstance(obj, target_class | target_class) + + def check_issubclass(self, klass, target_class): + self.assertIsSubclass(klass, target_class) + self.assertIsSubclass(klass, (target_class,)) + self.assertIsSubclass(klass, target_class | target_class) + + def check_not_issubclass(self, klass, target_class): + self.assertNotIsSubclass(klass, target_class) + self.assertNotIsSubclass(klass, (target_class,)) + self.assertNotIsSubclass(klass, target_class | target_class) def test_ABC_helper(self): # create an ABC using the helper class and perform basic checks @@ -270,29 +289,75 @@ def x(self): class C(metaclass=meta): pass + def test_isinstance_direct_inheritance(self): + class A(metaclass=abc_ABCMeta): + pass + class B(A): + pass + class C(A): + pass + + a = A() + b = B() + c = C() + # trigger caching + for _ in range(2): + self.check_isinstance(a, A) + self.check_not_isinstance(a, B) + self.check_not_isinstance(a, C) + + self.check_isinstance(b, B) + self.check_isinstance(b, A) + self.check_not_isinstance(b, C) + + self.check_isinstance(c, C) + self.check_isinstance(c, A) + self.check_not_isinstance(c, B) + + self.check_issubclass(B, A) + self.check_issubclass(C, A) + self.check_not_issubclass(B, C) + self.check_not_issubclass(C, B) + self.check_not_issubclass(A, B) + self.check_not_issubclass(A, C) + def test_registration_basics(self): class A(metaclass=abc_ABCMeta): pass class B(object): pass + + a = A() b = B() - self.assertNotIsSubclass(B, A) - self.assertNotIsSubclass(B, (A,)) - self.assertNotIsInstance(b, A) - self.assertNotIsInstance(b, (A,)) + # trigger caching + for _ in range(2): + self.check_not_issubclass(B, A) + self.check_not_isinstance(b, A) + + self.check_not_issubclass(A, B) + self.check_not_isinstance(a, B) + B1 = A.register(B) - self.assertIsSubclass(B, A) - self.assertIsSubclass(B, (A,)) - self.assertIsInstance(b, A) - self.assertIsInstance(b, (A,)) - self.assertIs(B1, B) + # trigger caching + for _ in range(2): + self.check_issubclass(B, A) + self.check_isinstance(b, A) + self.assertIs(B1, B) + + self.check_not_issubclass(A, B) + self.check_not_isinstance(a, B) + class C(B): pass + c = C() - self.assertIsSubclass(C, A) - self.assertIsSubclass(C, (A,)) - self.assertIsInstance(c, A) - self.assertIsInstance(c, (A,)) + # trigger caching + for _ in range(2): + self.check_issubclass(C, A) + self.check_isinstance(c, A) + + self.check_not_issubclass(A, C) + self.check_not_isinstance(a, C) def test_register_as_class_deco(self): class A(metaclass=abc_ABCMeta): @@ -377,39 +442,95 @@ class A(metaclass=abc_ABCMeta): pass self.assertIsSubclass(A, A) self.assertIsSubclass(A, (A,)) + class B(metaclass=abc_ABCMeta): pass self.assertNotIsSubclass(A, B) self.assertNotIsSubclass(A, (B,)) self.assertNotIsSubclass(B, A) self.assertNotIsSubclass(B, (A,)) + class C(metaclass=abc_ABCMeta): pass A.register(B) class B1(B): pass - self.assertIsSubclass(B1, A) - self.assertIsSubclass(B1, (A,)) + # trigger caching + for _ in range(2): + self.assertIsSubclass(B1, A) + self.assertIsSubclass(B1, (A,)) + class C1(C): pass B1.register(C1) - self.assertNotIsSubclass(C, B) - self.assertNotIsSubclass(C, (B,)) - self.assertNotIsSubclass(C, B1) - self.assertNotIsSubclass(C, (B1,)) - self.assertIsSubclass(C1, A) - self.assertIsSubclass(C1, (A,)) - self.assertIsSubclass(C1, B) - self.assertIsSubclass(C1, (B,)) - self.assertIsSubclass(C1, B1) - self.assertIsSubclass(C1, (B1,)) + # trigger caching + for _ in range(2): + self.assertNotIsSubclass(C, B) + self.assertNotIsSubclass(C, (B,)) + self.assertNotIsSubclass(C, B1) + self.assertNotIsSubclass(C, (B1,)) + self.assertIsSubclass(C1, A) + self.assertIsSubclass(C1, (A,)) + self.assertIsSubclass(C1, B) + self.assertIsSubclass(C1, (B,)) + self.assertIsSubclass(C1, B1) + self.assertIsSubclass(C1, (B1,)) + C1.register(int) class MyInt(int): pass - self.assertIsSubclass(MyInt, A) - self.assertIsSubclass(MyInt, (A,)) - self.assertIsInstance(42, A) - self.assertIsInstance(42, (A,)) + # trigger caching + for _ in range(2): + self.assertIsSubclass(MyInt, A) + self.assertIsSubclass(MyInt, (A,)) + self.assertIsInstance(42, A) + self.assertIsInstance(42, (A,)) + + def test_custom_subclasses(self): + class A: pass + class B(A): pass + + class C: pass + class D(C): pass + + class Root(metaclass=abc_ABCMeta): pass + + class Parent1(Root): + @classmethod + def __subclasses__(cls): + return [A] + + class Parent2(Root): + __subclasses__ = lambda: [A] + + # trigger caching + for _ in range(2): + self.check_isinstance(A(), Parent1) + self.check_isinstance(B(), Parent1) + self.check_issubclass(A, Parent1) + self.check_issubclass(B, Parent1) + self.check_not_isinstance(C(), Parent1) + self.check_not_isinstance(D(), Parent1) + self.check_not_issubclass(C, Parent1) + self.check_not_issubclass(D, Parent1) + + self.check_isinstance(A(), Parent2) + self.check_isinstance(B(), Parent2) + self.check_issubclass(A, Parent2) + self.check_issubclass(B, Parent2) + self.check_not_isinstance(C(), Parent2) + self.check_not_isinstance(D(), Parent2) + self.check_not_issubclass(C, Parent2) + self.check_not_issubclass(D, Parent2) + + self.check_isinstance(A(), Root) + self.check_isinstance(B(), Root) + self.check_isinstance(C(), Root) + self.check_isinstance(D(), Root) + self.check_issubclass(A, Root) + self.check_issubclass(B, Root) + self.check_issubclass(C, Root) + self.check_issubclass(D, Root) def test_issubclass_bad_arguments(self): class A(metaclass=abc_ABCMeta): @@ -460,8 +581,40 @@ class S(metaclass=abc_ABCMeta): with self.assertRaisesRegex(CustomError, exc_msg): issubclass(int, S) - def test_subclasshook(self): + def test_issubclass_bad_class(self): class A(metaclass=abc.ABCMeta): + pass + + A._abc_impl = 1 + error_msg = "_abc_impl is set to a wrong type" + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(A, A) + + class B(metaclass=_py_abc.ABCMeta): + pass + + B._abc_registry = 1 + error_msg = "argument of type 'int' is not a container or iterable" + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(B, B) + + class C(metaclass=_py_abc.ABCMeta): + pass + + C._abc_cache = 1 + error_msg = "argument of type 'int' is not a container or iterable" + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(C, C) + + class D(metaclass=_py_abc.ABCMeta): + pass + + D._abc_negative_cache = 1 + with self.assertRaisesRegex(TypeError, error_msg): + issubclass(D, D) + + def test_subclasshook(self): + class A(metaclass=abc_ABCMeta): @classmethod def __subclasshook__(cls, C): if cls is A: @@ -478,6 +631,26 @@ class C: self.assertNotIsSubclass(C, A) self.assertNotIsSubclass(C, (A,)) + def test_subclasshook_exception(self): + # Check that issubclass() propagates exceptions raised by + # __subclasshook__. + class CustomError(Exception): ... + exc_msg = "exception from __subclasshook__" + class A(metaclass=abc_ABCMeta): + @classmethod + def __subclasshook__(cls, C): + raise CustomError(exc_msg) + with self.assertRaisesRegex(CustomError, exc_msg): + issubclass(A, A) + class B(A): + pass + with self.assertRaisesRegex(CustomError, exc_msg): + issubclass(B, A) + class C: + pass + with self.assertRaisesRegex(CustomError, exc_msg): + issubclass(C, A) + def test_all_new_methods_are_called(self): class A(metaclass=abc_ABCMeta): pass @@ -522,7 +695,6 @@ def foo(self): self.assertEqual(A.__abstractmethods__, set()) A() - def test_update_new_abstractmethods(self): class A(metaclass=abc_ABCMeta): @abc.abstractmethod diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py index f440fc28ee7b7d..1786f052f37923 100644 --- a/Lib/test/test_isinstance.py +++ b/Lib/test/test_isinstance.py @@ -353,6 +353,28 @@ class B: with support.infinite_recursion(25): self.assertRaises(RecursionError, issubclass, X(), int) + def test_custom_subclasses_are_ignored(self): + class A: pass + class B: pass + + class Parent1: + @classmethod + def __subclasses__(cls): + return [A, B] + + class Parent2: + __subclasses__ = lambda: [A, B] + + self.assertNotIsInstance(A(), Parent1) + self.assertNotIsInstance(B(), Parent1) + self.assertNotIsSubclass(A, Parent1) + self.assertNotIsSubclass(B, Parent1) + + self.assertNotIsInstance(A(), Parent2) + self.assertNotIsInstance(B(), Parent2) + self.assertNotIsSubclass(A, Parent2) + self.assertNotIsSubclass(B, Parent2) + def blowstack(fxn, arg, compare_to): # Make sure that calling isinstance with a deeply nested tuple for its From e92f1f45f71e32be392c4a12aa1b75ffb3cba58c Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 00:56:33 +0300 Subject: [PATCH 3/8] gh-92810: Avoid O(n^2) complexity in ABCMeta.__subclasscheck__ --- Lib/_py_abc.py | 38 +++++++- Lib/abc.py | 2 +- Modules/_abc.c | 198 ++++++++++++++++++++++++++++++---------- Modules/clinic/_abc.c.h | 38 +++++++- 4 files changed, 220 insertions(+), 56 deletions(-) diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f1..1028a47567e847 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -49,6 +49,14 @@ def __new__(mcls, name, bases, namespace, /, **kwargs): cls._abc_cache = WeakSet() cls._abc_negative_cache = WeakSet() cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter + + # Performance optimization for common case + cls._abc_should_check_subclasses = False + if "__subclasses__" in namespace: + cls._abc_should_check_subclasses = True + for base in bases: + if hasattr(base, "_abc_should_check_subclasses"): + base._abc_should_check_subclasses = True return cls def register(cls, subclass): @@ -65,8 +73,20 @@ def register(cls, subclass): if issubclass(cls, subclass): # This would create a cycle, which is bad for the algorithm below raise RuntimeError("Refusing to create an inheritance cycle") + # Actual registration cls._abc_registry.add(subclass) ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache + # Recursively register the subclass in all ABC bases, + # to avoid recursive lookups down the class tree. + # >>> class Ancestor1(ABC): pass + # >>> class Ancestor2(Ancestor1): pass + # >>> class Other: pass + # >>> Ancestor2.register(Other) # calls Ancestor1.register(Other) + # >>> issubclass(Other, Ancestor2) is True + # >>> issubclass(Other, Ancestor1) is True # already in registry + for base in cls.__bases__: + if hasattr(base, "_abc_registry"): + base.register(subclass) return subclass def _dump_registry(cls, file=None): @@ -133,15 +153,23 @@ def __subclasscheck__(cls, subclass): cls._abc_cache.add(subclass) return True # Check if it's a subclass of a registered class (recursive) + if subclass in cls._abc_registry: + cls._abc_cache.add(subclass) + return True for rcls in cls._abc_registry: if issubclass(subclass, rcls): cls._abc_cache.add(subclass) return True - # Check if it's a subclass of a subclass (recursive) - for scls in cls.__subclasses__(): - if issubclass(subclass, scls): - cls._abc_cache.add(subclass) - return True + # Check if it's a subclass of a subclass (recursive). + # If __subclasses__ contain only ABCs, + # calling issubclass(...) will trigger the same __subclasscheck__ + # on *every* element of class inheritance tree. + # Performing that only in resence of `def __subclasses__()` classmethod + if cls._abc_should_check_subclasses: + for scls in cls.__subclasses__(): + if issubclass(subclass, scls): + cls._abc_cache.add(subclass) + return True # No dice; update negative cache cls._abc_negative_cache.add(subclass) return False diff --git a/Lib/abc.py b/Lib/abc.py index f8a4e11ce9c3b1..dde28a0e5c58ee 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -104,7 +104,7 @@ class ABCMeta(type): """ def __new__(mcls, name, bases, namespace, /, **kwargs): cls = super().__new__(mcls, name, bases, namespace, **kwargs) - _abc_init(cls) + _abc_init(cls, bases, namespace) return cls def register(cls, subclass): diff --git a/Modules/_abc.c b/Modules/_abc.c index 883925aa71cdff..f76b5b132f25ff 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -57,6 +57,7 @@ typedef struct { PyObject *_abc_cache; PyObject *_abc_negative_cache; uint64_t _abc_negative_cache_version; + uint8_t _abc_should_check_subclasses; } _abc_data; #define _abc_data_CAST(op) ((_abc_data *)(op)) @@ -73,6 +74,18 @@ set_cache_version(_abc_data *impl, uint64_t version) FT_ATOMIC_STORE_UINT64_RELAXED(impl->_abc_negative_cache_version, version); } +static inline uint8_t +get_should_check_subclasses(_abc_data *impl) +{ + return FT_ATOMIC_LOAD_UINT8_RELAXED(impl->_abc_should_check_subclasses); +} + +static inline void +set_should_check_subclasses(_abc_data *impl) +{ + FT_ATOMIC_STORE_UINT8_RELAXED(impl->_abc_should_check_subclasses, 1); +} + static int abc_data_traverse(PyObject *op, visitproc visit, void *arg) { @@ -161,6 +174,30 @@ _get_impl(PyObject *module, PyObject *self) return (_abc_data *)impl; } +/* If class is inherited from ABC, set data to point to internal ABC state of class, and return 1. + If object is not inherited from ABC, return 0. + If error is encountered, return -1. + */ +static int +_get_optional_impl(_abcmodule_state *state, PyObject *self, _abc_data **data) +{ + assert(data != NULL); + PyObject *impl = NULL; + int res = PyObject_GetOptionalAttr(self, &_Py_ID(_abc_impl), &impl); + if (res <= 0) { + *data = NULL; + return res; + } + if (!Py_IS_TYPE(impl, state->_abc_data_type)) { + PyErr_SetString(PyExc_TypeError, "_abc_impl is set to a wrong type"); + Py_DECREF(impl); + *data = NULL; + return -1; + } + *data = (_abc_data *)impl; + return 1; +} + static int _in_weak_set(_abc_data *impl, PyObject **pset, PyObject *obj) { @@ -331,11 +368,12 @@ _abc__get_dump(PyObject *module, PyObject *self) } PyObject *res; Py_BEGIN_CRITICAL_SECTION(impl); - res = Py_BuildValue("NNNK", + res = Py_BuildValue("NNNKH", PySet_New(impl->_abc_registry), PySet_New(impl->_abc_cache), PySet_New(impl->_abc_negative_cache), - get_cache_version(impl)); + get_cache_version(impl), + impl->_abc_should_check_subclasses); Py_END_CRITICAL_SECTION(); Py_DECREF(impl); return res; @@ -343,7 +381,7 @@ _abc__get_dump(PyObject *module, PyObject *self) // Compute set of abstract method names. static int -compute_abstract_methods(PyObject *self) +compute_abstract_methods(PyObject *self, PyObject *bases, PyObject *ns) { int ret = -1; PyObject *abstracts = PyFrozenSet_New(NULL); @@ -351,17 +389,10 @@ compute_abstract_methods(PyObject *self) return -1; } - PyObject *ns = NULL, *items = NULL, *bases = NULL; // Py_XDECREF()ed on error. - /* Stage 1: direct abstract methods. */ - ns = PyObject_GetAttr(self, &_Py_ID(__dict__)); - if (!ns) { - goto error; - } - // We can't use PyDict_Next(ns) even when ns is dict because // _PyObject_IsAbstract() can mutate ns. - items = PyMapping_Items(ns); + PyObject *items = PyMapping_Items(ns); if (!items) { goto error; } @@ -398,15 +429,6 @@ compute_abstract_methods(PyObject *self) } /* Stage 2: inherited abstract methods. */ - bases = PyObject_GetAttr(self, &_Py_ID(__bases__)); - if (!bases) { - goto error; - } - if (!PyTuple_Check(bases)) { - PyErr_SetString(PyExc_TypeError, "__bases__ is not tuple"); - goto error; - } - for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { PyObject *item = PyTuple_GET_ITEM(bases, pos); // borrowed PyObject *base_abstracts, *iter; @@ -459,30 +481,56 @@ compute_abstract_methods(PyObject *self) ret = 0; error: Py_DECREF(abstracts); - Py_XDECREF(ns); Py_XDECREF(items); - Py_XDECREF(bases); return ret; } +/* + * Notify base classes that child one has __subclasses__ overriden. + * Used as performance optimization in __subclasscheck__ + */ +static int +_abc_notify_subclasses_override(_abcmodule_state *state, PyObject *data, PyObject *bases) +{ + set_should_check_subclasses((_abc_data*) data); + + for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { + PyObject *base_class = PyTuple_GET_ITEM(bases, pos); // borrowed + _abc_data *impl = NULL; + + int res = _get_optional_impl(state, base_class, &impl); + if (res < 0) { + return -1; + } + if (res > 0) { + set_should_check_subclasses(impl); + } + } + + return 0; +} + #define COLLECTION_FLAGS (Py_TPFLAGS_SEQUENCE | Py_TPFLAGS_MAPPING) /*[clinic input] _abc._abc_init self: object + bases: object(subclass_of="&PyTuple_Type") + namespace: object(subclass_of="&PyDict_Type") / Internal ABC helper for class set-up. Should be never used outside abc module. [clinic start generated code]*/ static PyObject * -_abc__abc_init(PyObject *module, PyObject *self) -/*[clinic end generated code: output=594757375714cda1 input=8d7fe470ff77f029]*/ +_abc__abc_init_impl(PyObject *module, PyObject *self, PyObject *bases, + PyObject *namespace) +/*[clinic end generated code: output=a410180fefc86056 input=6bf48b3e15ff54aa]*/ { _abcmodule_state *state = get_abc_state(module); PyObject *data; - if (compute_abstract_methods(self) < 0) { + if (compute_abstract_methods(self, bases, namespace) < 0) { return NULL; } @@ -491,6 +539,12 @@ _abc__abc_init(PyObject *module, PyObject *self) if (data == NULL) { return NULL; } + if (PyDict_ContainsString(namespace, "__subclasses__")) { + if (_abc_notify_subclasses_override(state, data, bases) < 0) { + Py_DECREF(data); + return NULL; + } + } if (PyObject_SetAttr(self, &_Py_ID(_abc_impl), data) < 0) { Py_DECREF(data); return NULL; @@ -562,6 +616,7 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) if (result < 0) { return NULL; } + /* Actual registration */ _abc_data *impl = _get_impl(module, self); if (impl == NULL) { return NULL; @@ -572,8 +627,46 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) } Py_DECREF(impl); + /* + * Recursively register the subclass in all ABC bases, + * to avoid recursive lookups down the class tree. + * >>> class Ancestor1(ABC): pass + * >>> class Ancestor2(Ancestor1): pass + * >>> class Other: pass + * >>> Ancestor2.register(Other) # calls Ancestor1.register(Other) + * >>> issubclass(Other, Ancestor2) is True + * >>> issubclass(Other, Ancestor1) is True # already in registry + */ + _abcmodule_state *state = get_abc_state(module); + PyObject *bases = PyObject_GetAttr(self, &_Py_ID(__bases__)); + if (!bases) { + return NULL; + } + if (!PyTuple_Check(bases)) { + PyErr_SetString(PyExc_TypeError, "__bases__ is not tuple"); + goto error; + } + for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { + PyObject *base = PyTuple_GET_ITEM(bases, pos); // borrowed + _abc_data *base_impl = NULL; + + int base_is_abc = _get_optional_impl(state, base, &base_impl); + if (base_is_abc < 0) { + goto error; + } + if (base_is_abc == 0) { + continue; + } + PyObject *base_result = PyObject_CallMethod(base, "register", "O", subclass); + if (base_result == NULL) { + goto error; + } + Py_DECREF(base_result); + } + Py_DECREF(bases); + /* Invalidate negative cache */ - increment_invalidation_counter(get_abc_state(module)); + increment_invalidation_counter(state); /* Set Py_TPFLAGS_SEQUENCE or Py_TPFLAGS_MAPPING flag */ if (PyType_Check(self)) { @@ -586,6 +679,10 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) } } return Py_NewRef(subclass); + +error: + //Py_DECREF(bases); + return NULL; } @@ -784,31 +881,38 @@ _abc__abc_subclasscheck_impl(PyObject *module, PyObject *self, goto end; } - /* 6. Check if it's a subclass of a subclass (recursive). */ - subclasses = PyObject_CallMethod(self, "__subclasses__", NULL); - if (subclasses == NULL) { - goto end; - } - if (!PyList_Check(subclasses)) { - PyErr_SetString(PyExc_TypeError, "__subclasses__() must return a list"); - goto end; - } - for (pos = 0; pos < PyList_GET_SIZE(subclasses); pos++) { - PyObject *scls = PyList_GetItemRef(subclasses, pos); - if (scls == NULL) { + /* 6. Check if it's a subclass of a subclass (recursive). + * If __subclasses__ contain only ABCs, + * calling issubclass(...) will trigger the same __subclasscheck__ + * on *every* element of class inheritance tree. + * Performing that only in resence of `def __subclasses__()` classmethod + */ + if (get_should_check_subclasses(impl)) { + subclasses = PyObject_CallMethod(self, "__subclasses__", NULL); + if (subclasses == NULL) { goto end; } - int r = PyObject_IsSubclass(subclass, scls); - Py_DECREF(scls); - if (r > 0) { - if (_add_to_weak_set(impl, &impl->_abc_cache, subclass) < 0) { - goto end; - } - result = Py_True; + if (!PyList_Check(subclasses)) { + PyErr_SetString(PyExc_TypeError, "__subclasses__() must return a list"); goto end; } - if (r < 0) { - goto end; + for (pos = 0; pos < PyList_GET_SIZE(subclasses); pos++) { + PyObject *scls = PyList_GetItemRef(subclasses, pos); + if (scls == NULL) { + goto end; + } + int r = PyObject_IsSubclass(subclass, scls); + Py_DECREF(scls); + if (r > 0) { + if (_add_to_weak_set(impl, &impl->_abc_cache, subclass) < 0) { + goto end; + } + result = Py_True; + goto end; + } + if (r < 0) { + goto end; + } } } diff --git a/Modules/clinic/_abc.c.h b/Modules/clinic/_abc.c.h index 04681fa2206a2a..4335fbbc0b02f0 100644 --- a/Modules/clinic/_abc.c.h +++ b/Modules/clinic/_abc.c.h @@ -40,13 +40,45 @@ PyDoc_STRVAR(_abc__get_dump__doc__, {"_get_dump", (PyCFunction)_abc__get_dump, METH_O, _abc__get_dump__doc__}, PyDoc_STRVAR(_abc__abc_init__doc__, -"_abc_init($module, self, /)\n" +"_abc_init($module, self, bases, namespace, /)\n" "--\n" "\n" "Internal ABC helper for class set-up. Should be never used outside abc module."); #define _ABC__ABC_INIT_METHODDEF \ - {"_abc_init", (PyCFunction)_abc__abc_init, METH_O, _abc__abc_init__doc__}, + {"_abc_init", _PyCFunction_CAST(_abc__abc_init), METH_FASTCALL, _abc__abc_init__doc__}, + +static PyObject * +_abc__abc_init_impl(PyObject *module, PyObject *self, PyObject *bases, + PyObject *namespace); + +static PyObject * +_abc__abc_init(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *self; + PyObject *bases; + PyObject *namespace; + + if (!_PyArg_CheckPositional("_abc_init", nargs, 3, 3)) { + goto exit; + } + self = args[0]; + if (!PyTuple_Check(args[1])) { + _PyArg_BadArgument("_abc_init", "argument 2", "tuple", args[1]); + goto exit; + } + bases = args[1]; + if (!PyDict_Check(args[2])) { + _PyArg_BadArgument("_abc_init", "argument 3", "dict", args[2]); + goto exit; + } + namespace = args[2]; + return_value = _abc__abc_init_impl(module, self, bases, namespace); + +exit: + return return_value; +} PyDoc_STRVAR(_abc__abc_register__doc__, "_abc_register($module, self, subclass, /)\n" @@ -161,4 +193,4 @@ _abc_get_cache_token(PyObject *module, PyObject *Py_UNUSED(ignored)) { return _abc_get_cache_token_impl(module); } -/*[clinic end generated code: output=1989b6716c950e17 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=9fa68621578b46d0 input=a9049054013a1b77]*/ From 7f089ed76263a48e87ffb05252d9c891bd59c2ce Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 01:03:57 +0300 Subject: [PATCH 4/8] gh-92810: Add What's New entry --- Doc/whatsnew/3.15.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5379ac3abba227..e167375d1c216b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -322,6 +322,19 @@ arguments (:pep:`791`). Improved modules ================ +abc +--- + +* Reduce memory usage of :func:`issubclass` checks for classes inheriting abstract classes. + + :class:`abc.ABCMeta` hook ``__subclasscheck__`` now includes + a guard which is triggered then the hook is called from a parent class + (``issubclass(cls, RootClass)`` -> ``issubclass(cls, NestedClass)`` -> ...). + This guard prevents adding ``cls`` to ``NestedClass`` positive and negative caches, + preventing memory bloat in some cases (thousands of classes inherited from ABC). + + (Contributed by Maxim Martynov in :gh:`92810`.) + argparse -------- From e35c4626782c57bf5608aaba0a22ef2cafbd113c Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 01:17:04 +0300 Subject: [PATCH 5/8] gh-92810: Fix test_abc --- Lib/test/test_abc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 04b5528a8f8c34..e5da65224f79c1 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -525,12 +525,12 @@ class Parent2(Root): self.check_isinstance(A(), Root) self.check_isinstance(B(), Root) - self.check_isinstance(C(), Root) - self.check_isinstance(D(), Root) self.check_issubclass(A, Root) self.check_issubclass(B, Root) - self.check_issubclass(C, Root) - self.check_issubclass(D, Root) + self.check_not_isinstance(C(), Root) + self.check_not_isinstance(D(), Root) + self.check_not_issubclass(C, Root) + self.check_not_issubclass(D, Root) def test_issubclass_bad_arguments(self): class A(metaclass=abc_ABCMeta): @@ -594,9 +594,9 @@ class B(metaclass=_py_abc.ABCMeta): pass B._abc_registry = 1 - error_msg = "argument of type 'int' is not a container or iterable" + error_msg = "'int' object is not iterable" with self.assertRaisesRegex(TypeError, error_msg): - issubclass(B, B) + issubclass(int, B) class C(metaclass=_py_abc.ABCMeta): pass From 2dd9999995e1bb88c832f718707e50f8708a7887 Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 01:42:35 +0300 Subject: [PATCH 6/8] gh-92810: Fix tests --- Lib/test/test_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index e5da65224f79c1..c0b8bbfd4834cb 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -594,7 +594,7 @@ class B(metaclass=_py_abc.ABCMeta): pass B._abc_registry = 1 - error_msg = "'int' object is not iterable" + error_msg = "argument of type 'int' is not a container or iterable" with self.assertRaisesRegex(TypeError, error_msg): issubclass(int, B) From d8dbd03720e825b7dc9295a83bd256eb5c0968c5 Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 01:42:45 +0300 Subject: [PATCH 7/8] gh-92810: Add blurb --- .../next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst diff --git a/Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst b/Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst new file mode 100644 index 00000000000000..06b3392869d7df --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-07-01-36-39.gh-issue-92810.H765mk.rst @@ -0,0 +1,2 @@ +Reduce memory usage by :meth:`~type.__subclasscheck__` for +:class:`abc.ABCMeta` with large class trees. From c89ab4bde0da59a4b7a75f2871a3f507b71a78ee Mon Sep 17 00:00:00 2001 From: Martynov Maxim Date: Fri, 7 Nov 2025 12:48:56 +0300 Subject: [PATCH 8/8] gh-92810: Fix refleaks --- Modules/_abc.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Modules/_abc.c b/Modules/_abc.c index 67a583dd6f0f8a..63b2eaa8ba1532 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -646,7 +646,8 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) } if (!PyTuple_Check(bases)) { PyErr_SetString(PyExc_TypeError, "__bases__ is not tuple"); - goto error; + Py_DECREF(bases); + return NULL; } for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(bases); pos++) { PyObject *base = PyTuple_GET_ITEM(bases, pos); // borrowed @@ -654,14 +655,16 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) int base_is_abc = _get_optional_impl(state, base, &base_impl); if (base_is_abc < 0) { - goto error; + Py_DECREF(bases); + return NULL; } if (base_is_abc == 0) { continue; } PyObject *base_result = PyObject_CallMethod(base, "register", "O", subclass); if (base_result == NULL) { - goto error; + Py_DECREF(bases); + return NULL; } Py_DECREF(base_result); } @@ -681,10 +684,6 @@ _abc__abc_register_impl(PyObject *module, PyObject *self, PyObject *subclass) } } return Py_NewRef(subclass); - -error: - //Py_DECREF(bases); - return NULL; }