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 -------- diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index c31c33657002ec..3c1e53e123095e 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) #define FT_MUTEX_LOCK(lock) PyMutex_Lock(lock) #define FT_MUTEX_UNLOCK(lock) PyMutex_Unlock(lock) @@ -128,6 +134,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 @@ -136,6 +143,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 @@ -161,6 +169,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) #define FT_MUTEX_LOCK(lock) do {} while (0) #define FT_MUTEX_UNLOCK(lock) do {} while (0) 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/Lib/test/test_abc.py b/Lib/test/test_abc.py index 80ee9e0ba56e75..c0b8bbfd4834cb 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_issubclass(A, Root) + self.check_issubclass(B, 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): @@ -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(int, 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 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. diff --git a/Modules/_abc.c b/Modules/_abc.c index f87a5c702946bc..63b2eaa8ba1532 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. @@ -65,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)) @@ -72,21 +65,25 @@ 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 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 @@ -177,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) { @@ -347,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; @@ -359,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); @@ -367,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; } @@ -414,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; @@ -475,12 +481,35 @@ 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] @@ -488,18 +517,21 @@ compute_abstract_methods(PyObject *self) _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=0b3513f947736d39]*/ +_abc__abc_init_impl(PyObject *module, PyObject *self, PyObject *bases, + PyObject *namespace) +/*[clinic end generated code: output=a410180fefc86056 input=a984e4f7d36d6298]*/ { _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; } @@ -508,6 +540,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; @@ -580,6 +618,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; @@ -590,8 +629,49 @@ _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"); + 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 + _abc_data *base_impl = NULL; + + int base_is_abc = _get_optional_impl(state, base, &base_impl); + if (base_is_abc < 0) { + Py_DECREF(bases); + return NULL; + } + if (base_is_abc == 0) { + continue; + } + PyObject *base_result = PyObject_CallMethod(base, "register", "O", subclass); + if (base_result == NULL) { + Py_DECREF(bases); + return NULL; + } + 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)) { @@ -804,31 +884,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]*/