Skip to content

bpo-38085: Fix throw exception in __init__subclass__ causes wrong isinstance() a… #30112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 58 additions & 34 deletions Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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):
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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
24 changes: 24 additions & 0 deletions Lib/test/test_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions Modules/_abc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
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