Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
675bec5
gh-92810: Avoid O(n^2) complexity in ABCMeta.__subclasscheck__
dolfinus Mar 30, 2025
701ecc9
gh-92810: Apply fixes
dolfinus Mar 31, 2025
041f109
gh-92810: Apply fixes
dolfinus Mar 31, 2025
9bc4385
gh-92810: Apply fixes
dolfinus Mar 31, 2025
3d80b1e
gh-92810: Apply fixes
dolfinus Mar 31, 2025
b7603e0
gh-92810: Return __subclasses__clause back
dolfinus Apr 21, 2025
dd0d18c
gh-92810: Revert _abc.c changes
dolfinus Apr 21, 2025
8d695fd
gh-92810: Fix linter errors
dolfinus Apr 21, 2025
a2650b6
gh-92810: Add recursive issubclass check to _abc.c
dolfinus Jun 13, 2025
7afa5ea
gh-92810: Remove WeakKeyDictionary from _py_abc
dolfinus Jun 13, 2025
57980d3
gh-92810: Add news entry
dolfinus Jun 13, 2025
bbaf38a
gh-92810: Fix news entry
dolfinus Jun 13, 2025
6fc994d
gh-92810: Fixes after review
dolfinus Jun 22, 2025
b3b5895
gh-92810: Fixes after review
dolfinus Jun 22, 2025
69c5038
gh-92810: Fixes after review
dolfinus Jun 23, 2025
dc1b6d5
gh-92810: Fixes after review
dolfinus Jun 23, 2025
cd097ab
gh-92810: Introduce FT wrappers for uint64_t atomics
dolfinus Jun 23, 2025
f3a21a7
gh-92810: Use FT atomic wrappers for ABC invalidation counter
dolfinus Jun 23, 2025
e24e815
gh-92810: Fix missing FT wrapper
dolfinus Jun 23, 2025
b723912
gh-92810: Address review fixes
dolfinus Aug 4, 2025
0295846
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 4, 2025
16f39bd
gh-92810: Address review fixes
dolfinus Aug 4, 2025
2dc6453
gh-92810: Add What's New entry
dolfinus Aug 4, 2025
968766d
gh-92810: Fix What's New entry syntax
dolfinus Aug 4, 2025
a6e4461
gh-92810: Address review fixes
dolfinus Aug 4, 2025
80d3281
gh-92810: Address review fixes
dolfinus Aug 4, 2025
ff38b9e
gh-92810: Properly reset recursion check
dolfinus Aug 4, 2025
23df287
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 4, 2025
0bf7374
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 7, 2025
d537859
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Sep 9, 2025
d17de08
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Nov 4, 2025
858d4c0
gh-92810: Fix What's New entry
dolfinus Nov 4, 2025
7b6a0dc
gh-92810: Improve nested subclass check performance
dolfinus Nov 4, 2025
2f8f2b2
gh-92810: Automatically add ABC registry entries to cache
dolfinus Nov 4, 2025
b657cd6
gh-92810: Remove false fastpath
dolfinus Nov 6, 2025
854f2f6
Merge branch 'python:main' into improvement/ABCMeta_subclasscheck
dolfinus Nov 18, 2025
c4fe83a
gh-92810: Improve test_custom_subclasses
dolfinus Nov 30, 2025
84c5893
gh-92810: Improve test_custom_subclasses
dolfinus Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ 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
cls._abc_issubclasscheck_recursive = False
return cls

def register(cls, subclass):
Expand All @@ -66,7 +67,7 @@ def register(cls, subclass):
# This would create a cycle, which is bad for the algorithm below
raise RuntimeError("Refusing to create an inheritance cycle")
cls._abc_registry.add(subclass)
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
return subclass

def _dump_registry(cls, file=None):
Expand Down Expand Up @@ -139,9 +140,24 @@ def __subclasscheck__(cls, 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)
# If inside recursive issubclass check, avoid adding classes
# to any cache because this may drastically increase memory usage.
# Unfortunately, issubclass/__subclasscheck__ don't accept third
# argument with context, so using global context within ABCMeta.
# This is done only on first method call, next will use cache.
scls_is_abc = hasattr(scls, "_abc_issubclasscheck_recursive")
if scls_is_abc:
scls._abc_issubclasscheck_recursive = True
try:
result = issubclass(subclass, scls)
finally:
if scls_is_abc:
scls._abc_issubclasscheck_recursive = False
if result:
if not cls._abc_issubclasscheck_recursive:
cls._abc_cache.add(subclass)
return True
# No dice; update negative cache
cls._abc_negative_cache.add(subclass)
if not cls._abc_issubclasscheck_recursive:
cls._abc_negative_cache.add(subclass)
return False
166 changes: 136 additions & 30 deletions Lib/test/test_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,29 +270,102 @@ 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.assertIsInstance(a, A)
self.assertIsInstance(a, (A,))
self.assertNotIsInstance(a, B)
self.assertNotIsInstance(a, (B,))
self.assertNotIsInstance(a, C)
self.assertNotIsInstance(a, (C,))

self.assertIsInstance(b, B)
self.assertIsInstance(b, (B,))
self.assertIsInstance(b, A)
self.assertIsInstance(b, (A,))
self.assertNotIsInstance(b, C)
self.assertNotIsInstance(b, (C,))

self.assertIsInstance(c, C)
self.assertIsInstance(c, (C,))
self.assertIsInstance(c, A)
self.assertIsInstance(c, (A,))
self.assertNotIsInstance(c, B)
self.assertNotIsInstance(c, (B,))

self.assertIsSubclass(B, A)
self.assertIsSubclass(B, (A,))
self.assertIsSubclass(C, A)
self.assertIsSubclass(C, (A,))
self.assertNotIsSubclass(B, C)
self.assertNotIsSubclass(B, (C,))
self.assertNotIsSubclass(C, B)
self.assertNotIsSubclass(C, (B,))
self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsSubclass(A, C)
self.assertNotIsSubclass(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.assertNotIsSubclass(B, A)
self.assertNotIsSubclass(B, (A,))
self.assertNotIsInstance(b, A)
self.assertNotIsInstance(b, (A,))

self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsInstance(a, B)
self.assertNotIsInstance(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.assertIsSubclass(B, A)
self.assertIsSubclass(B, (A,))
self.assertIsInstance(b, A)
self.assertIsInstance(b, (A,))
self.assertIs(B1, B)

self.assertNotIsSubclass(A, B)
self.assertNotIsSubclass(A, (B,))
self.assertNotIsInstance(a, B)
self.assertNotIsInstance(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.assertIsSubclass(C, A)
self.assertIsSubclass(C, (A,))
self.assertIsInstance(c, A)
self.assertIsInstance(c, (A,))

self.assertNotIsSubclass(A, C)
self.assertNotIsSubclass(A, (C,))
self.assertNotIsInstance(a, C)
self.assertNotIsInstance(a, (C,))

def test_register_as_class_deco(self):
class A(metaclass=abc_ABCMeta):
Expand Down Expand Up @@ -377,39 +450,73 @@ 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: pass

class Parent1(metaclass=abc_ABCMeta):
@classmethod
def __subclasses__(cls):
return [A]

class Parent2(metaclass=abc_ABCMeta):
__subclasses__ = lambda: [A]

# trigger caching
for _ in range(2):
self.assertIsInstance(A(), Parent1)
self.assertIsSubclass(A, Parent1)
self.assertNotIsInstance(B(), Parent1)
self.assertNotIsSubclass(B, Parent1)

self.assertIsInstance(A(), Parent2)
self.assertIsSubclass(A, Parent2)
self.assertNotIsInstance(B(), Parent2)
self.assertNotIsSubclass(B, Parent2)

def test_issubclass_bad_arguments(self):
class A(metaclass=abc_ABCMeta):
Expand Down Expand Up @@ -522,7 +629,6 @@ def foo(self):
self.assertEqual(A.__abstractmethods__, set())
A()


def test_update_new_abstractmethods(self):
class A(metaclass=abc_ABCMeta):
@abc.abstractmethod
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_isinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Reduce memory usage by :meth:`~type.__subclasscheck__`
for :class:`abc.ABCMeta` and large class trees.
Loading
Loading