Skip to content

Commit abf4bfe

Browse files
committed
gh-92810: Return __subclasses__clause back
1 parent 7960480 commit abf4bfe

File tree

4 files changed

+222
-50
lines changed

4 files changed

+222
-50
lines changed

Lib/_py_abc.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from _weakrefset import WeakSet
2+
from weakref import WeakKeyDictionary
23

34

45
def get_cache_token():
@@ -31,6 +32,7 @@ class ABCMeta(type):
3132
# Note: this counter is private. Use `abc.get_cache_token()` for
3233
# external code.
3334
_abc_invalidation_counter = 0
35+
_abc_issubclass_context = WeakKeyDictionary()
3436

3537
def __new__(mcls, name, bases, namespace, /, **kwargs):
3638
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
@@ -65,21 +67,7 @@ def register(cls, subclass):
6567
if issubclass(cls, subclass):
6668
# This would create a cycle, which is bad for the algorithm below
6769
raise RuntimeError("Refusing to create an inheritance cycle")
68-
69-
# Actual registration
7070
cls._abc_registry.add(subclass)
71-
72-
# Recursively register the subclass in all ABC bases, to avoid recursive lookups.
73-
# >>> class Ancestor1(ABC): pass
74-
# >>> class Ancestor2(Ancestor1): pass
75-
# >>> class Other: pass
76-
# >>> Ancestor2.register(Other) # same result for Ancestor1.register(Other)
77-
# >>> issubclass(Other, Ancestor2) is True
78-
# >>> issubclass(Other, Ancestor1) is True
79-
for pcls in cls.__mro__:
80-
if hasattr(pcls, "_abc_registry"):
81-
pcls._abc_registry.add(subclass)
82-
8371
# Invalidate negative cache
8472
ABCMeta._abc_invalidation_counter += 1
8573
return subclass
@@ -152,6 +140,26 @@ def __subclasscheck__(cls, subclass):
152140
if issubclass(subclass, rcls):
153141
cls._abc_cache.add(subclass)
154142
return True
155-
# No dice; update negative cache
156-
cls._abc_negative_cache.add(subclass)
143+
144+
# Check if it's a subclass of a subclass (recursive)
145+
for scls in cls.__subclasses__():
146+
# If inside recursive issubclass check, avoid adding classes to any cache because this
147+
# may drastically increase memory usage.
148+
# Unfortunately, issubclass/__subclasscheck__ don't accept third argument with context,
149+
# so using global context within ABCMeta.
150+
# This is done only on first method call, others will use cached result.
151+
scls_context = ABCMeta._abc_issubclass_context.setdefault(scls, WeakSet())
152+
try:
153+
scls_context.add(cls)
154+
result = issubclass(subclass, scls)
155+
finally:
156+
scls_context.remove(cls)
157+
158+
if result:
159+
if not ABCMeta._abc_issubclass_context.get(cls, None):
160+
cls._abc_cache.add(subclass)
161+
return True
162+
163+
if not ABCMeta._abc_issubclass_context.get(cls, None):
164+
cls._abc_negative_cache.add(subclass)
157165
return False

Lib/test/test_abc.py

Lines changed: 166 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -270,29 +270,100 @@ def x(self):
270270
class C(metaclass=meta):
271271
pass
272272

273+
def test_isinstance_direct_inheritance(self):
274+
class A(metaclass=abc_ABCMeta):
275+
pass
276+
class B(A):
277+
pass
278+
class C(A):
279+
pass
280+
a = A()
281+
b = B()
282+
c = C()
283+
# trigger caching
284+
for _ in range(2):
285+
self.assertIsInstance(a, A)
286+
self.assertIsInstance(a, (A,))
287+
self.assertNotIsInstance(a, B)
288+
self.assertNotIsInstance(a, (B,))
289+
self.assertNotIsInstance(a, C)
290+
self.assertNotIsInstance(a, (C,))
291+
292+
self.assertIsInstance(b, B)
293+
self.assertIsInstance(b, (B,))
294+
self.assertIsInstance(b, A)
295+
self.assertIsInstance(b, (A,))
296+
self.assertNotIsInstance(b, C)
297+
self.assertNotIsInstance(b, (C,))
298+
299+
self.assertIsInstance(c, C)
300+
self.assertIsInstance(c, (C,))
301+
self.assertIsInstance(c, A)
302+
self.assertIsInstance(c, (A,))
303+
self.assertNotIsInstance(c, B)
304+
self.assertNotIsInstance(c, (B,))
305+
306+
self.assertIsSubclass(B, A)
307+
self.assertIsSubclass(B, (A,))
308+
self.assertIsSubclass(C, A)
309+
self.assertIsSubclass(C, (A,))
310+
self.assertNotIsSubclass(B, C)
311+
self.assertNotIsSubclass(B, (C,))
312+
self.assertNotIsSubclass(C, B)
313+
self.assertNotIsSubclass(C, (B,))
314+
self.assertNotIsSubclass(A, B)
315+
self.assertNotIsSubclass(A, (B,))
316+
self.assertNotIsSubclass(A, C)
317+
self.assertNotIsSubclass(A, (C,))
318+
273319
def test_registration_basics(self):
274320
class A(metaclass=abc_ABCMeta):
275321
pass
276322
class B(object):
277323
pass
324+
a = A()
278325
b = B()
279-
self.assertNotIsSubclass(B, A)
280-
self.assertNotIsSubclass(B, (A,))
281-
self.assertNotIsInstance(b, A)
282-
self.assertNotIsInstance(b, (A,))
326+
327+
# trigger caching
328+
for _ in range(2):
329+
self.assertNotIsSubclass(B, A)
330+
self.assertNotIsSubclass(B, (A,))
331+
self.assertNotIsInstance(b, A)
332+
self.assertNotIsInstance(b, (A,))
333+
334+
self.assertNotIsSubclass(A, B)
335+
self.assertNotIsSubclass(A, (B,))
336+
self.assertNotIsInstance(a, B)
337+
self.assertNotIsInstance(a, (B,))
338+
283339
B1 = A.register(B)
284-
self.assertIsSubclass(B, A)
285-
self.assertIsSubclass(B, (A,))
286-
self.assertIsInstance(b, A)
287-
self.assertIsInstance(b, (A,))
288-
self.assertIs(B1, B)
340+
# trigger caching
341+
for _ in range(2):
342+
self.assertIsSubclass(B, A)
343+
self.assertIsSubclass(B, (A,))
344+
self.assertIsInstance(b, A)
345+
self.assertIsInstance(b, (A,))
346+
self.assertIs(B1, B)
347+
348+
self.assertNotIsSubclass(A, B)
349+
self.assertNotIsSubclass(A, (B,))
350+
self.assertNotIsInstance(a, B)
351+
self.assertNotIsInstance(a, (B,))
352+
289353
class C(B):
290354
pass
291355
c = C()
292-
self.assertIsSubclass(C, A)
293-
self.assertIsSubclass(C, (A,))
294-
self.assertIsInstance(c, A)
295-
self.assertIsInstance(c, (A,))
356+
# trigger caching
357+
for _ in range(2):
358+
self.assertIsSubclass(C, A)
359+
self.assertIsSubclass(C, (A,))
360+
self.assertIsInstance(c, A)
361+
self.assertIsInstance(c, (A,))
362+
363+
self.assertNotIsSubclass(A, C)
364+
self.assertNotIsSubclass(A, (C,))
365+
self.assertNotIsInstance(a, C)
366+
self.assertNotIsInstance(a, (C,))
296367

297368
def test_register_as_class_deco(self):
298369
class A(metaclass=abc_ABCMeta):
@@ -377,41 +448,75 @@ class A(metaclass=abc_ABCMeta):
377448
pass
378449
self.assertIsSubclass(A, A)
379450
self.assertIsSubclass(A, (A,))
451+
380452
class B(metaclass=abc_ABCMeta):
381453
pass
382454
self.assertNotIsSubclass(A, B)
383455
self.assertNotIsSubclass(A, (B,))
384456
self.assertNotIsSubclass(B, A)
385457
self.assertNotIsSubclass(B, (A,))
458+
386459
class C(metaclass=abc_ABCMeta):
387460
pass
388461
A.register(B)
389462
class B1(B):
390463
pass
391-
self.assertIsSubclass(B1, A)
392-
self.assertIsSubclass(B1, (A,))
464+
# trigger caching
465+
for _ in range(2):
466+
self.assertIsSubclass(B1, A)
467+
self.assertIsSubclass(B1, (A,))
468+
393469
class C1(C):
394470
pass
395471
B1.register(C1)
396-
self.assertNotIsSubclass(C, B)
397-
self.assertNotIsSubclass(C, (B,))
398-
self.assertNotIsSubclass(C, B1)
399-
self.assertNotIsSubclass(C, (B1,))
400-
self.assertIsSubclass(C1, A)
401-
self.assertIsSubclass(C1, (A,))
402-
self.assertIsSubclass(C1, B)
403-
self.assertIsSubclass(C1, (B,))
404-
self.assertIsSubclass(C1, B1)
405-
self.assertIsSubclass(C1, (B1,))
472+
# trigger caching
473+
for _ in range(2):
474+
self.assertNotIsSubclass(C, B)
475+
self.assertNotIsSubclass(C, (B,))
476+
self.assertNotIsSubclass(C, B1)
477+
self.assertNotIsSubclass(C, (B1,))
478+
self.assertIsSubclass(C1, A)
479+
self.assertIsSubclass(C1, (A,))
480+
self.assertIsSubclass(C1, B)
481+
self.assertIsSubclass(C1, (B,))
482+
self.assertIsSubclass(C1, B1)
483+
self.assertIsSubclass(C1, (B1,))
484+
406485
C1.register(int)
407486
class MyInt(int):
408487
pass
409-
self.assertIsSubclass(MyInt, A)
410-
self.assertIsSubclass(MyInt, (A,))
411-
self.assertIsInstance(42, A)
412-
self.assertIsInstance(42, (A,))
488+
# trigger caching
489+
for _ in range(2):
490+
self.assertIsSubclass(MyInt, A)
491+
self.assertIsSubclass(MyInt, (A,))
492+
self.assertIsInstance(42, A)
493+
self.assertIsInstance(42, (A,))
413494

414-
def test_subclasses_bad_arguments(self):
495+
def test_custom_subclasses(self):
496+
class A: pass
497+
class B: pass
498+
499+
class Parent1(metaclass=abc_ABCMeta):
500+
@classmethod
501+
def __subclasses__(cls):
502+
return [A]
503+
504+
class Parent2(metaclass=abc_ABCMeta):
505+
__subclasses__ = lambda: [A]
506+
507+
# trigger caching
508+
for _ in range(2):
509+
self.assertIsInstance(A(), Parent1)
510+
self.assertIsSubclass(A, Parent1)
511+
self.assertNotIsInstance(B(), Parent1)
512+
self.assertNotIsSubclass(B, Parent1)
513+
514+
self.assertIsInstance(A(), Parent2)
515+
self.assertIsSubclass(A, Parent2)
516+
self.assertNotIsInstance(B(), Parent2)
517+
self.assertNotIsSubclass(B, Parent2)
518+
519+
def test_issubclass_bad_arguments(self):
415520
class A(metaclass=abc_ABCMeta):
416521
pass
417522

@@ -429,6 +534,37 @@ class C:
429534
with self.assertRaises(TypeError):
430535
issubclass(C(), A)
431536

537+
# bpo-34441: Check that issubclass() doesn't crash on bogus
538+
# classes.
539+
bogus_subclasses = [
540+
None,
541+
lambda x: [],
542+
lambda: 42,
543+
lambda: [42],
544+
]
545+
546+
for i, func in enumerate(bogus_subclasses):
547+
class S(metaclass=abc_ABCMeta):
548+
__subclasses__ = func
549+
550+
with self.subTest(i=i):
551+
with self.assertRaises(TypeError):
552+
issubclass(int, S)
553+
554+
# Also check that issubclass() propagates exceptions raised by
555+
# __subclasses__.
556+
class CustomError(Exception): ...
557+
exc_msg = "exception from __subclasses__"
558+
559+
def raise_exc():
560+
raise CustomError(exc_msg)
561+
562+
class S(metaclass=abc_ABCMeta):
563+
__subclasses__ = raise_exc
564+
565+
with self.assertRaisesRegex(CustomError, exc_msg):
566+
issubclass(int, S)
567+
432568
def test_subclasshook(self):
433569
class A(metaclass=abc.ABCMeta):
434570
@classmethod
@@ -491,7 +627,6 @@ def foo(self):
491627
self.assertEqual(A.__abstractmethods__, set())
492628
A()
493629

494-
495630
def test_update_new_abstractmethods(self):
496631
class A(metaclass=abc_ABCMeta):
497632
@abc.abstractmethod

Lib/test/test_isinstance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,13 @@ class B:
351351
with support.infinite_recursion(25):
352352
self.assertRaises(RecursionError, issubclass, X(), int)
353353

354-
def test_override_subclasses(self):
354+
def test_custom_subclasses_are_ignored(self):
355355
class A: pass
356356
class B: pass
357357

358358
class Parent1:
359359
@classmethod
360-
def __subclasses__(self):
360+
def __subclasses__(cls):
361361
return [A, B]
362362

363363
class Parent2:

Modules/_abc.c

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@ _abc__abc_subclasscheck_impl(PyObject *module, PyObject *self,
763763

764764
PyObject *ok, *subclasses = NULL, *result = NULL;
765765
_abcmodule_state *state = NULL;
766+
Py_ssize_t pos;
766767
int incache;
767768
_abc_data *impl = _get_impl(module, self);
768769
if (impl == NULL) {
@@ -849,7 +850,35 @@ _abc__abc_subclasscheck_impl(PyObject *module, PyObject *self,
849850
goto end;
850851
}
851852

852-
/* No dice; update negative cache. */
853+
/* 6. Check if it's a subclass of a subclass (recursive). */
854+
subclasses = PyObject_CallMethod(self, "__subclasses__", NULL);
855+
if (subclasses == NULL) {
856+
goto end;
857+
}
858+
if (!PyList_Check(subclasses)) {
859+
PyErr_SetString(PyExc_TypeError, "__subclasses__() must return a list");
860+
goto end;
861+
}
862+
for (pos = 0; pos < PyList_GET_SIZE(subclasses); pos++) {
863+
PyObject *scls = PyList_GetItemRef(subclasses, pos);
864+
if (scls == NULL) {
865+
goto end;
866+
}
867+
int r = PyObject_IsSubclass(subclass, scls);
868+
Py_DECREF(scls);
869+
if (r > 0) {
870+
if (_add_to_weak_set(impl, &impl->_abc_cache, subclass) < 0) {
871+
goto end;
872+
}
873+
result = Py_True;
874+
goto end;
875+
}
876+
if (r < 0) {
877+
goto end;
878+
}
879+
}
880+
881+
/* Recursive calls lead to uncontrolled negative cache growth, avoid this */
853882
if (_add_to_weak_set(impl, &impl->_abc_negative_cache, subclass) < 0) {
854883
goto end;
855884
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy