Skip to content
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

Merged
merged 103 commits into from
Feb 18, 2018
Merged
Show file tree
Hide file tree
Changes from 97 commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
5c34508
Initial work on C implementation of ABCMeta
ilevkivskyi Jul 23, 2017
cb7ffcf
Basic implementation of ABCMeta.__new__
ilevkivskyi Jul 25, 2017
b83ee80
Bare-bone implementation of register and subclass checks
ilevkivskyi Jul 25, 2017
181e83f
Fix mock failure and silence compiler warnings
ilevkivskyi Jul 26, 2017
c084a7f
Provide nicer dump of registry
ilevkivskyi Jul 26, 2017
a192d5d
Add better docstrings
ilevkivskyi Jul 26, 2017
35a2472
Expose the internal cavhes and registry (backward compatibility)
ilevkivskyi Jul 27, 2017
4812450
Merge remote-tracking branch 'upstream/master' into c-abc
ilevkivskyi Sep 3, 2017
b9038e2
Merge remote-tracking branch 'upstream/master' into c-abc
Jan 20, 2018
bbee578
Add _abc to Setup.dist
Jan 20, 2018
a3464fd
Fix _abc in Setup.dist
Jan 20, 2018
947bf7d
Update a comment
Jan 20, 2018
7ffc59e
Settle the .py version
Jan 20, 2018
41287a7
Fix some TODOs and refleaks
Jan 21, 2018
569cc44
Fix some more refleaks; use weak refs in registry
Jan 21, 2018
34665a8
Some more fixes; add some caching
Jan 21, 2018
576acac
Fix a crash due to erroneous DECREF
Jan 21, 2018
30098b4
Simplify some code; reorganize TODOs
Jan 22, 2018
51ede5d
Finish caches; add more comments
Jan 22, 2018
5263e1a
Use Py_RETURN_TRUE/FALSE and fix refleak
methane Jan 24, 2018
1f7aee9
Use Py_RETURN_NONE
methane Jan 24, 2018
11fea70
Use _PyObject_IsAbstract()
methane Jan 24, 2018
7ff3fbb
Use _PySet_NextEntry
methane Jan 24, 2018
9b4eb2f
Minor review comments
Jan 25, 2018
ab20a33
Sketch the new API
Jan 25, 2018
39f2692
Merge remote-tracking branch 'upstream/master' into c-abc
Jan 25, 2018
493d0ec
Use _PyObject_LookupAttr
Jan 25, 2018
2fe2c54
Some more progress
Jan 26, 2018
9476af6
Implement weakref callbacks and guarded iteration
Jan 26, 2018
ab68cdb
Fix some errors
Jan 26, 2018
3eb0a60
Fix two review comments
Jan 26, 2018
ed36b76
More fixes, test_abc passes
Jan 27, 2018
25fc5b9
Merge remote-tracking branch 'upstream/master' into c-abc
Jan 27, 2018
b2f75b9
Fix some remaining problems
Jan 27, 2018
cdb5cdf
Update TODO
Jan 27, 2018
a1a3a52
Add missing statics
Jan 27, 2018
a66b08c
Build on Windows
methane Jan 27, 2018
86af9ae
Refactor __abstractmethods__ calculation.
methane Jan 27, 2018
b22232a
Refactor via _abc_impl
Jan 27, 2018
e51c5ca
Fix some refleaks
Jan 27, 2018
bac7a43
Add docs (required by some tests) and initialization
Jan 27, 2018
4571649
Merge remote-tracking branch 'upstream/master' into c-abc
Jan 27, 2018
0d7513b
Outdated comment and detection of intrusions
Jan 27, 2018
c429f49
Minor fixes
Jan 27, 2018
357b56d
Few more refleaks
Jan 27, 2018
cd80fcb
Restore unwanted changes
Jan 28, 2018
34e13c3
Fix(?) some more refleaks
Jan 28, 2018
c5633b6
Reset caches between runs
Jan 28, 2018
3cbbc12
Fix abuse of borrowed reference
methane Jan 28, 2018
23bcb07
Fix remaining refleaks
Jan 28, 2018
0aab479
Updare TODO, switch to common result agreement, few more checks
Jan 28, 2018
86e0660
Fix typos
Jan 28, 2018
c55e482
Remove irrelevant TODOs, add few comments
Jan 28, 2018
5f9526a
Use Py_ssize_t for iterating
methane Jan 28, 2018
8174b61
Use c99 designated initializer.
methane Jan 28, 2018
bb8d623
Fix gset_new() and abc_data_new()
methane Jan 28, 2018
ef59e54
Massive refactoring...
methane Jan 28, 2018
22699fe
fixup
methane Jan 28, 2018
95cbf34
Review comments
Jan 28, 2018
4d596cc
More refactoring
Jan 28, 2018
fa3cba3
Typos and minor fixes
Jan 28, 2018
6f18293
Minor fixes and code style
Jan 28, 2018
9100891
Remove some unreachable code
Jan 28, 2018
36c5643
Review comments
Jan 28, 2018
dd2abda
Split the class into two separate implementations
Jan 28, 2018
99d950c
Add version independent (Py vs C) cache clearing
Jan 28, 2018
3762d49
Test the six-like tricky type.__new__(metaclass) with ABCMeta
Jan 28, 2018
404d1ce
Test both versions
Jan 28, 2018
0dc5fae
Add comment about testing
Jan 28, 2018
287b26a
Always DECREF after PyTuplr_Pack
Jan 28, 2018
ef34364
Use PySet_New() instead of calling copy() method.
methane Jan 29, 2018
d4d78a1
Copy set before iterating, and remove guarded set
methane Jan 29, 2018
f58822e
Add NULL check after PyWeakref_GetObject
methane Jan 30, 2018
3b74bdc
Add fast path for looking register
methane Jan 30, 2018
db1c852
Check negative cache version before cache lookup
methane Jan 30, 2018
6e62be7
Merge pull request #5 from methane/c-abc-no-guard-set
ilevkivskyi Feb 11, 2018
5384726
Fix nits pointed by pppery.
methane Feb 12, 2018
a48eecc
Create set lazily
methane Feb 14, 2018
16a8db1
Explicitly set NULL
methane Feb 14, 2018
5ad3ea8
Merge pull request #6 from methane/c-abc-lazyset
ilevkivskyi Feb 14, 2018
86a9b8d
Check PyObject_IsTrue() error
methane Feb 14, 2018
36c2013
Strip TODO comments.
methane Feb 14, 2018
09c5370
Add NEWS entry
methane Feb 14, 2018
207d8e9
Fix _reset_caches
methane Feb 15, 2018
a15377b
Add default: Py_UNREACHABLE()
methane Feb 15, 2018
d31da13
Rephrase NEWS entry
methane Feb 15, 2018
eaff1cb
Merge remote-tracking branch 'upstream/master' into c-abc
Feb 16, 2018
48de70e
Factor out Python version to a separate file
Feb 16, 2018
3bd0666
Factor out Python version to a separate file
Feb 16, 2018
702347a
Remove extra whitespace
Feb 16, 2018
e0c978b
Add more details to NEWS
Feb 16, 2018
b370dfe
Fix an import in refleak test
Feb 16, 2018
a1ae0a7
Restart tests
Feb 16, 2018
4746211
Make order of subclass checks in Python version stable and consistent…
Feb 16, 2018
001b416
Convert _abc to Argument Clinic
Feb 17, 2018
fc528df
Merge remote-tracking branch 'upstream/master' into c-abc
Feb 17, 2018
289c414
Regenerate clinic
Feb 17, 2018
ac0c639
Switch from Python invalidation counter to C long long
Feb 17, 2018
079e3be
The rest of the comments
Feb 17, 2018
9c49e5a
Merge remote-tracking branch 'upstream/master' into c-abc
Feb 17, 2018
4146588
Regenerate clinics.
Feb 17, 2018
c133605
Two more comments
Feb 17, 2018
f82e04d
Few more comments by Serhiy; add Whats New item
Feb 18, 2018
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
145 changes: 145 additions & 0 deletions Lib/_py_abc.py
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
203 changes: 61 additions & 142 deletions Lib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

"""Abstract Base Classes (ABCs) according to PEP 3119."""

from _weakrefset import WeakSet


def abstractmethod(funcobj):
"""A decorator indicating abstract methods.
Expand All @@ -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.

Expand All @@ -51,8 +48,7 @@ def __init__(self, callable):


class abstractstaticmethod(staticmethod):
"""
A decorator indicating abstract staticmethods.
"""A decorator indicating abstract staticmethods.

Similar to abstractmethod.

Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we fix ABCMeta.__module__ to abc?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 pickle can already find the correct class at _py_abc.ABCMeta. Also it is informative for a quick check which version is used, the C one or the Python one. So this is up to you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If pickle abc.ABCMeta as _py_abc.ABCMeta it will be not unpickleable in 3.6. Or in future Python versions if we will decide to remove or rename _py_abc.

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
Loading