-
-
Notifications
You must be signed in to change notification settings - Fork 30k
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
bpo-31333: Re-implement ABCMeta in C #5273
Changes from 93 commits
5c34508
cb7ffcf
b83ee80
181e83f
c084a7f
a192d5d
35a2472
4812450
b9038e2
bbee578
a3464fd
947bf7d
7ffc59e
41287a7
569cc44
34665a8
576acac
30098b4
51ede5d
5263e1a
1f7aee9
11fea70
7ff3fbb
9b4eb2f
ab20a33
39f2692
493d0ec
2fe2c54
9476af6
ab68cdb
3eb0a60
ed36b76
25fc5b9
b2f75b9
cdb5cdf
a1a3a52
a66b08c
86af9ae
b22232a
e51c5ca
bac7a43
4571649
0d7513b
c429f49
357b56d
cd80fcb
34e13c3
c5633b6
3cbbc12
23bcb07
0aab479
86e0660
c55e482
5f9526a
8174b61
bb8d623
ef59e54
22699fe
95cbf34
4d596cc
fa3cba3
6f18293
9100891
36c5643
dd2abda
99d950c
3762d49
404d1ce
0dc5fae
287b26a
ef34364
d4d78a1
f58822e
3b74bdc
db1c852
6e62be7
5384726
a48eecc
16a8db1
5ad3ea8
86a9b8d
36c2013
09c5370
207d8e9
a15377b
d31da13
eaff1cb
48de70e
3bd0666
702347a
e0c978b
b370dfe
a1ae0a7
4746211
001b416
fc528df
289c414
ac0c639
079e3be
9c49e5a
4146588
c133605
f82e04d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
from _weakrefset import WeakSet | ||
|
||
|
||
def get_cache_token(): | ||
"""Returns the current ABC cache token. | ||
|
||
The token is an opaque object (supporting equality testing) identifying the | ||
current version of the ABC cache for virtual subclasses. The token changes | ||
with every call to ``register()`` on any ABC. | ||
""" | ||
return ABCMeta._abc_invalidation_counter | ||
|
||
|
||
class ABCMeta(type): | ||
"""Metaclass for defining Abstract Base Classes (ABCs). | ||
|
||
Use this metaclass to create an ABC. An ABC can be subclassed | ||
directly, and then acts as a mix-in class. You can also register | ||
unrelated concrete classes (even built-in classes) and unrelated | ||
ABCs as 'virtual subclasses' -- these and their descendants will | ||
be considered subclasses of the registering ABC by the built-in | ||
issubclass() function, but the registering ABC won't show up in | ||
their MRO (Method Resolution Order) nor will method | ||
implementations defined by the registering ABC be callable (not | ||
even via super()). | ||
""" | ||
|
||
# A global counter that is incremented each time a class is | ||
# registered as a virtual subclass of anything. It forces the | ||
# negative cache to be cleared before its next use. | ||
# Note: this counter is private. Use `abc.get_cache_token()` for | ||
# external code. | ||
_abc_invalidation_counter = 0 | ||
|
||
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 | ||
return cls | ||
|
||
def register(cls, subclass): | ||
"""Register a virtual subclass of an ABC. | ||
|
||
Returns the subclass, to allow usage as a class decorator. | ||
""" | ||
if not isinstance(subclass, type): | ||
raise TypeError("Can only register classes") | ||
if issubclass(subclass, cls): | ||
return subclass # Already a subclass | ||
# Subtle: test for cycles *after* testing for "already a subclass"; | ||
# this means we allow X.register(X) and interpret it as a no-op. | ||
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) | ||
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache | ||
return subclass | ||
|
||
def _dump_registry(cls, file=None): | ||
"""Debug helper to print the ABC registry.""" | ||
print(f"Class: {cls.__module__}.{cls.__qualname__}", file=file) | ||
print(f"Inv. counter: {get_cache_token()}", file=file) | ||
for name in cls.__dict__: | ||
if name.startswith("_abc_"): | ||
value = getattr(cls, name) | ||
if isinstance(value, WeakSet): | ||
value = set(value) | ||
print(f"{name}: {value!r}", file=file) | ||
|
||
def _abc_registry_clear(cls): | ||
"""Clear the registry (for debugging or testing).""" | ||
cls._abc_registry.clear() | ||
|
||
def _abc_caches_clear(cls): | ||
"""Clear the caches (for debugging or testing).""" | ||
cls._abc_cache.clear() | ||
cls._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: | ||
return True | ||
subtype = type(instance) | ||
if subtype is subclass: | ||
if (cls._abc_negative_cache_version == | ||
ABCMeta._abc_invalidation_counter and | ||
subclass in cls._abc_negative_cache): | ||
return False | ||
# Fall back to the subclass check. | ||
return cls.__subclasscheck__(subclass) | ||
return any(cls.__subclasscheck__(c) for c in {subclass, subtype}) | ||
|
||
def __subclasscheck__(cls, subclass): | ||
"""Override for issubclass(subclass, cls).""" | ||
# Check cache | ||
if subclass in cls._abc_cache: | ||
return True | ||
# Check negative cache; may have to invalidate | ||
if cls._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: | ||
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) | ||
else: | ||
cls._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) | ||
return True | ||
# Check if it's a subclass of a registered class (recursive) | ||
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 | ||
# No dice; update negative cache | ||
cls._abc_negative_cache.add(subclass) | ||
return False |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,6 @@ | |
|
||
"""Abstract Base Classes (ABCs) according to PEP 3119.""" | ||
|
||
from _weakrefset import WeakSet | ||
|
||
|
||
def abstractmethod(funcobj): | ||
"""A decorator indicating abstract methods. | ||
|
@@ -27,8 +25,7 @@ def my_abstract_method(self, ...): | |
|
||
|
||
class abstractclassmethod(classmethod): | ||
""" | ||
A decorator indicating abstract classmethods. | ||
"""A decorator indicating abstract classmethods. | ||
|
||
Similar to abstractmethod. | ||
|
||
|
@@ -51,8 +48,7 @@ def __init__(self, callable): | |
|
||
|
||
class abstractstaticmethod(staticmethod): | ||
""" | ||
A decorator indicating abstract staticmethods. | ||
"""A decorator indicating abstract staticmethods. | ||
|
||
Similar to abstractmethod. | ||
|
||
|
@@ -75,8 +71,7 @@ def __init__(self, callable): | |
|
||
|
||
class abstractproperty(property): | ||
""" | ||
A decorator indicating abstract properties. | ||
"""A decorator indicating abstract properties. | ||
|
||
Requires that the metaclass is ABCMeta or derived from it. A | ||
class that has a metaclass derived from ABCMeta cannot be | ||
|
@@ -106,145 +101,69 @@ def setx(self, value): ... | |
__isabstractmethod__ = True | ||
|
||
|
||
class ABCMeta(type): | ||
|
||
"""Metaclass for defining Abstract Base Classes (ABCs). | ||
|
||
Use this metaclass to create an ABC. An ABC can be subclassed | ||
directly, and then acts as a mix-in class. You can also register | ||
unrelated concrete classes (even built-in classes) and unrelated | ||
ABCs as 'virtual subclasses' -- these and their descendants will | ||
be considered subclasses of the registering ABC by the built-in | ||
issubclass() function, but the registering ABC won't show up in | ||
their MRO (Method Resolution Order) nor will method | ||
implementations defined by the registering ABC be callable (not | ||
even via super()). | ||
|
||
""" | ||
|
||
# A global counter that is incremented each time a class is | ||
# registered as a virtual subclass of anything. It forces the | ||
# negative cache to be cleared before its next use. | ||
# Note: this counter is private. Use `abc.get_cache_token()` for | ||
# external code. | ||
_abc_invalidation_counter = 0 | ||
|
||
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 | ||
return cls | ||
|
||
def register(cls, subclass): | ||
"""Register a virtual subclass of an ABC. | ||
|
||
Returns the subclass, to allow usage as a class decorator. | ||
try: | ||
from _abc import (get_cache_token, _abc_init, _abc_register, | ||
_abc_instancecheck, _abc_subclasscheck, _get_dump, | ||
_reset_registry, _reset_caches) | ||
except ImportError: | ||
from _py_abc import ABCMeta, get_cache_token | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we fix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I am not sure. I think it is important for pickling the metaclass itself. But There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If pickle |
||
else: | ||
class ABCMeta(type): | ||
"""Metaclass for defining Abstract Base Classes (ABCs). | ||
|
||
Use this metaclass to create an ABC. An ABC can be subclassed | ||
directly, and then acts as a mix-in class. You can also register | ||
unrelated concrete classes (even built-in classes) and unrelated | ||
ABCs as 'virtual subclasses' -- these and their descendants will | ||
be considered subclasses of the registering ABC by the built-in | ||
issubclass() function, but the registering ABC won't show up in | ||
their MRO (Method Resolution Order) nor will method | ||
implementations defined by the registering ABC be callable (not | ||
even via super()). | ||
""" | ||
if not isinstance(subclass, type): | ||
raise TypeError("Can only register classes") | ||
if issubclass(subclass, cls): | ||
return subclass # Already a subclass | ||
# Subtle: test for cycles *after* testing for "already a subclass"; | ||
# this means we allow X.register(X) and interpret it as a no-op. | ||
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) | ||
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache | ||
return subclass | ||
|
||
def _dump_registry(cls, file=None): | ||
"""Debug helper to print the ABC registry.""" | ||
print("Class: %s.%s" % (cls.__module__, cls.__qualname__), file=file) | ||
print("Inv.counter: %s" % ABCMeta._abc_invalidation_counter, file=file) | ||
for name in cls.__dict__: | ||
if name.startswith("_abc_"): | ||
value = getattr(cls, name) | ||
if isinstance(value, WeakSet): | ||
value = set(value) | ||
print("%s: %r" % (name, value), file=file) | ||
|
||
def __instancecheck__(cls, instance): | ||
"""Override for isinstance(instance, cls).""" | ||
# Inline the cache checking | ||
subclass = instance.__class__ | ||
if subclass in cls._abc_cache: | ||
return True | ||
subtype = type(instance) | ||
if subtype is subclass: | ||
if (cls._abc_negative_cache_version == | ||
ABCMeta._abc_invalidation_counter and | ||
subclass in cls._abc_negative_cache): | ||
return False | ||
# Fall back to the subclass check. | ||
return cls.__subclasscheck__(subclass) | ||
return any(cls.__subclasscheck__(c) for c in {subclass, subtype}) | ||
|
||
def __subclasscheck__(cls, subclass): | ||
"""Override for issubclass(subclass, cls).""" | ||
# Check cache | ||
if subclass in cls._abc_cache: | ||
return True | ||
# Check negative cache; may have to invalidate | ||
if cls._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: | ||
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) | ||
else: | ||
cls._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) | ||
return True | ||
# Check if it's a subclass of a registered class (recursive) | ||
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 | ||
# No dice; update negative cache | ||
cls._abc_negative_cache.add(subclass) | ||
return False | ||
def __new__(mcls, name, bases, namespace, **kwargs): | ||
cls = super().__new__(mcls, name, bases, namespace, **kwargs) | ||
_abc_init(cls) | ||
return cls | ||
|
||
def register(cls, subclass): | ||
"""Register a virtual subclass of an ABC. | ||
|
||
Returns the subclass, to allow usage as a class decorator. | ||
""" | ||
return _abc_register(cls, subclass) | ||
|
||
def __instancecheck__(cls, instance): | ||
"""Override for isinstance(instance, cls).""" | ||
return _abc_instancecheck(cls, instance) | ||
|
||
def __subclasscheck__(cls, subclass): | ||
"""Override for issubclass(subclass, cls).""" | ||
return _abc_subclasscheck(cls, subclass) | ||
|
||
def _dump_registry(cls, file=None): | ||
"""Debug helper to print the ABC registry.""" | ||
print(f"Class: {cls.__module__}.{cls.__qualname__}", file=file) | ||
print(f"Inv. counter: {get_cache_token()}", file=file) | ||
(_abc_registry, _abc_cache, _abc_negative_cache, | ||
_abc_negative_cache_version) = _get_dump(cls) | ||
print(f"_abc_registry: {_abc_registry!r}", file=file) | ||
print(f"_abc_cache: {_abc_cache!r}", file=file) | ||
print(f"_abc_negative_cache: {_abc_negative_cache!r}", file=file) | ||
print(f"_abc_negative_cache_version: {_abc_negative_cache_version!r}", | ||
file=file) | ||
|
||
def _abc_registry_clear(cls): | ||
"""Clear the registry (for debugging or testing).""" | ||
_reset_registry(cls) | ||
|
||
def _abc_caches_clear(cls): | ||
"""Clear the caches (for debugging or testing).""" | ||
_reset_caches(cls) | ||
|
||
|
||
class ABC(metaclass=ABCMeta): | ||
"""Helper class that provides a standard way to create an ABC using | ||
inheritance. | ||
""" | ||
__slots__ = () | ||
|
||
|
||
def get_cache_token(): | ||
"""Returns the current ABC cache token. | ||
|
||
The token is an opaque object (supporting equality testing) identifying the | ||
current version of the ABC cache for virtual subclasses. The token changes | ||
with every call to ``register()`` on any ABC. | ||
""" | ||
return ABCMeta._abc_invalidation_counter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about changing
{subclass, subtype}
with(subclass, subtype)
for stable order?C version checks subclass first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Having stable order is better (easier for debugging/reproducibility).