diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f1..af5c14964d8933 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -11,6 +11,39 @@ def get_cache_token(): return ABCMeta._abc_invalidation_counter +class _abc_data(object): + def __init__(self): + self._abc_registry = WeakSet() + self._abc_cache = WeakSet() + self._abc_negative_cache = WeakSet() + self._abc_negative_cache_version = ABCMeta._abc_invalidation_counter + + +def _compute_abstract_methods(cls): + """Compute set of abstract method names""" + abstracts = {name + for name, value in cls.__dict__.items() + if getattr(value, "__isabstractmethod__", False)} + for base in cls.__bases__: + for name in getattr(base, "__abstractmethods__", set()): + value = getattr(cls, name, None) + if getattr(value, "__isabstractmethod__", False): + abstracts.add(name) + cls.__abstractmethods__ = frozenset(abstracts) + + +def _abc_init(cls): + _compute_abstract_methods(cls) + # Set up inheritance registry + cls._abc_impl = _abc_data() + + +def _get_impl(cls): + if '_abc_impl' not in cls.__dict__: + _abc_init(cls) + return cls._abc_impl + + class ABCMeta(type): """Metaclass for defining Abstract Base Classes (ABCs). @@ -34,21 +67,7 @@ class ABCMeta(type): def __new__(mcls, name, bases, namespace, /, **kwargs): cls = super().__new__(mcls, name, bases, namespace, **kwargs) - # Compute set of abstract method names - abstracts = {name - for name, value in namespace.items() - if getattr(value, "__isabstractmethod__", False)} - for base in bases: - for name in getattr(base, "__abstractmethods__", set()): - value = getattr(cls, name, None) - if getattr(value, "__isabstractmethod__", False): - abstracts.add(name) - cls.__abstractmethods__ = frozenset(abstracts) - # Set up inheritance registry - cls._abc_registry = WeakSet() - cls._abc_cache = WeakSet() - cls._abc_negative_cache = WeakSet() - cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter + _abc_init(cls) return cls def register(cls, subclass): @@ -65,7 +84,8 @@ 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") - cls._abc_registry.add(subclass) + abc_impl = _get_impl(cls) + abc_impl._abc_registry.add(subclass) ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache return subclass @@ -82,24 +102,27 @@ def _dump_registry(cls, file=None): def _abc_registry_clear(cls): """Clear the registry (for debugging or testing).""" - cls._abc_registry.clear() + abc_impl = _get_impl(cls) + abc_impl._abc_registry.clear() def _abc_caches_clear(cls): """Clear the caches (for debugging or testing).""" - cls._abc_cache.clear() - cls._abc_negative_cache.clear() + abc_impl = _get_impl(cls) + abc_impl._abc_cache.clear() + abc_impl._abc_negative_cache.clear() def __instancecheck__(cls, instance): """Override for isinstance(instance, cls).""" # Inline the cache checking subclass = instance.__class__ - if subclass in cls._abc_cache: + abc_impl = _get_impl(cls) + if subclass in abc_impl._abc_cache: return True subtype = type(instance) if subtype is subclass: - if (cls._abc_negative_cache_version == + if (abc_impl._abc_negative_cache_version == ABCMeta._abc_invalidation_counter and - subclass in cls._abc_negative_cache): + subclass in abc_impl._abc_negative_cache): return False # Fall back to the subclass check. return cls.__subclasscheck__(subclass) @@ -109,39 +132,40 @@ def __subclasscheck__(cls, subclass): """Override for issubclass(subclass, cls).""" if not isinstance(subclass, type): raise TypeError('issubclass() arg 1 must be a class') + abc_impl = _get_impl(cls) # Check cache - if subclass in cls._abc_cache: + if subclass in abc_impl._abc_cache: return True # Check negative cache; may have to invalidate - if cls._abc_negative_cache_version < ABCMeta._abc_invalidation_counter: + if abc_impl._abc_negative_cache_version < ABCMeta._abc_invalidation_counter: # Invalidate the negative cache - cls._abc_negative_cache = WeakSet() - cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter - elif subclass in cls._abc_negative_cache: + abc_impl._abc_negative_cache = WeakSet() + abc_impl._abc_negative_cache_version = ABCMeta._abc_invalidation_counter + elif subclass in abc_impl._abc_negative_cache: return False # Check the subclass hook ok = cls.__subclasshook__(subclass) if ok is not NotImplemented: assert isinstance(ok, bool) if ok: - cls._abc_cache.add(subclass) + abc_impl._abc_cache.add(subclass) else: - cls._abc_negative_cache.add(subclass) + abc_impl._abc_negative_cache.add(subclass) return ok # Check if it's a direct subclass if cls in getattr(subclass, '__mro__', ()): - cls._abc_cache.add(subclass) + abc_impl._abc_cache.add(subclass) return True # Check if it's a subclass of a registered class (recursive) - for rcls in cls._abc_registry: + for rcls in abc_impl._abc_registry: if issubclass(subclass, rcls): - cls._abc_cache.add(subclass) + abc_impl._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) + abc_impl._abc_cache.add(subclass) return True # No dice; update negative cache - cls._abc_negative_cache.add(subclass) + abc_impl._abc_negative_cache.add(subclass) return False diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index c1d750dba83f99..a5ececf9a8855b 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -521,6 +521,30 @@ def foo(self): self.assertEqual(A.__abstractmethods__, set()) A() + def test_init_subclass_exception(self): + class Animal(metaclass=abc_ABCMeta): + pass + + class Plant(metaclass=abc_ABCMeta): + def __init_subclass__(cls): + super().__init_subclass__() + assert not issubclass(cls, Animal), "Plants cannot be Animals" + + class Dog(Animal): + pass + + try: + class Triffid(Animal, Plant): + pass + except Exception: + pass + + d = Dog() + self.assertTrue(issubclass(Dog, Animal)) + self.assertFalse(issubclass(Dog, Plant)) + self.assertTrue(isinstance(d, Animal)) + self.assertFalse(isinstance(d, Plant)) + def test_update_new_abstractmethods(self): class A(metaclass=abc_ABCMeta): diff --git a/Misc/NEWS.d/next/Library/2021-12-15-15-51-32.bpo-38085.FnIBaB.rst b/Misc/NEWS.d/next/Library/2021-12-15-15-51-32.bpo-38085.FnIBaB.rst new file mode 100644 index 00000000000000..cfe5fd835bfd3b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-12-15-15-51-32.bpo-38085.FnIBaB.rst @@ -0,0 +1,3 @@ +Fix the incorrect :func:`isinstance` and :func:`issubclass` results when +throwing exceptions in ``__init_subclass__`` in subclasses of +:class:`abc.ABCMeta`. Patch by Weipeng Hong. diff --git a/Modules/_abc.c b/Modules/_abc.c index b7465c379dddf4..de56a2a390e46e 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -117,9 +117,28 @@ static PyType_Spec _abc_data_type_spec = { .slots = _abc_data_type_spec_slots, }; +/* Forward declaration. */ +static PyObject * _abc__abc_init(PyObject *module, PyObject *self); + +static int +_already_abc_init(PyObject *self) { + PyObject *oname = _PyUnicode_FromId(&PyId__abc_impl); + if (!oname) { + return 0; + } + PyObject *dict = _PyObject_GetAttrId(self, &PyId___dict__); + int res = PyMapping_HasKey(dict, oname); + Py_DECREF(dict); + return res; +} + static _abc_data * _get_impl(PyObject *module, PyObject *self) { + if (!_already_abc_init(self)) { + _abc__abc_init(module, self); + } + _abcmodule_state *state = get_abc_state(module); PyObject *impl = _PyObject_GetAttrId(self, &PyId__abc_impl); if (impl == NULL) {
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: