Last active
June 5, 2019 15:00
-
-
Save pjeby/75ca26f8d2a7a0c68e30 to your computer and use it in GitHub Desktop.
Pure-Python PEP 487 Implementation, usable back to Python 3.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Demonstrate the features""" | |
from pep487 import init_subclasses, noconflict | |
class Demo(init_subclasses): | |
def __init_subclass__(cls, ns, **kw): | |
print((cls, ns, kw)) | |
super().__init_subclass__(cls, ns, **kw) | |
# The above doesn't print anything, since it's not a subclass of itself | |
class sub(Demo): | |
x = 1 | |
# The above prints (<class '__main__.sub'>, {'x':1, ...}, {}) | |
# Now let's add more initialization in a specialized subclass: | |
class Demo2(Demo): | |
def __init_subclass__(cls, ns, **kw): | |
print("About to call super") | |
super().__init_subclass__(cls, ns, **kw) | |
print("Just called super") | |
# The above just prints <class '__main__.Demo2'>, {'__init_subclass__':...}, {} | |
# But then this prints the wrapper calls: | |
class sub2(Demo2): | |
something = 'whatever' | |
# i.e., you get: | |
# About to call super | |
# (<class '__main__.sub2'>, {'something':'whatever', ...}, {}) | |
# Just called super | |
### Now let's make a conflicting metaclass and base | |
class othermeta(type): pass | |
class otherbase(metaclass=othermeta): pass | |
try: | |
class mixed(sub, otherbase): pass | |
except TypeError: | |
# See, they aren't compatible | |
print("Failed as expected") | |
# These work, and print their members | |
class mixed(sub, otherbase, metaclass=noconflict): y=3 | |
class mixed2(otherbase, sub, metaclass=noconflict): z=4 | |
class mixed3(sub, metaclass=noconflict(othermeta)): x=99 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""The actual implementation""" | |
__all__ = ['init_subclasses', 'noconflict'] | |
class pep487_meta(type): | |
"""Metaclass that implements PEP 487 protocol""" | |
def __init__(cls, name, bases, ns, **kw): | |
super().__init__(name, bases, ns, **kw) | |
s = super(cls, cls) | |
if hasattr(s, '__init_subclass__'): | |
s.__init_subclass__(cls, ns, **kw) | |
class init_subclasses(metaclass=pep487_meta): | |
"""Include this as a base class to get PEP 487 support""" | |
def __init_subclass__(cls, ns, **kw): | |
"""This method just terminates the super() chain""" | |
def noconflict(*args, **kw): | |
"""Use this as an explicit metaclass to fix metaclass conflicts | |
You can use this as `class c3(c1, c2, metaclass=noconflict):` to | |
automatically fix metaclass conflicts between c1 and c2, or as | |
`class c3(c1, c2, metaclass=noconflict(othermeta))` to explicitly | |
add in another metaclass in addition to any used by c1 and c2. | |
""" | |
explicit_meta = None | |
def make_class(name, bases, ns, **kw): | |
meta = metaclass_for_bases(bases, explicit_meta) | |
return meta(name, bases, ns, **kw) | |
if len(args)==1 and not kw: | |
explicit_meta, = args | |
return make_class | |
else: | |
return make_class(*args, **kw) | |
class sentinel: | |
"""Marker for detecting incompatible root metaclasses""" | |
def metaclass_for_bases(bases, explicit_mc=None): | |
"""Determine metaclass from 1+ bases and optional explicit metaclass""" | |
meta = [getattr(b,'__class__',type(b)) for b in bases] | |
if explicit_mc is not None: | |
# The explicit metaclass needs to be verified for compatibility | |
# as well, and allowed to resolve the incompatible bases, if any | |
meta.insert(0, explicit_mc) | |
candidates = normalized_bases(meta) | |
if not candidates: | |
# No bases, use type | |
return type | |
elif len(candidates)>1: | |
return derived_meta(tuple(candidates)) | |
# Just one, return it | |
return candidates[0] | |
def normalized_bases(classes): | |
"""Remove redundant base classes from `classes`""" | |
candidates = [] | |
for m in classes: | |
for n in classes: | |
if issubclass(n,m) and m is not n: | |
break | |
else: | |
# m has no subclasses in 'classes' | |
if m in candidates: | |
candidates.remove(m) # ensure that we're later in the list | |
candidates.append(m) | |
return candidates | |
# Weak registry, so unused derived metaclasses will die off | |
from weakref import WeakValueDictionary | |
meta_reg = WeakValueDictionary() | |
def derived_meta(metaclasses): | |
"""Synthesize a new metaclass that mixes the given `metaclasses`""" | |
derived = meta_reg.get(metaclasses) | |
if derived is sentinel: | |
# We should only get here if you have a metaclass that | |
# doesn't inherit from `type` -- in Python 2, this is | |
# possible if you use ExtensionClass or some other exotic | |
# metaclass implemented in C, whose metaclass isn't `type`. | |
# In practice, this isn't likely to be a problem in Python 3, | |
# or even in Python 2, but we shouldn't just crash with | |
# unbounded recursion here, so we give a better error message. | |
raise TypeError("Incompatible root metatypes", metaclasses) | |
elif derived is None: | |
# prevent unbounded recursion | |
meta_reg[metaclasses] = sentinel | |
# get the common meta-metaclass of the metaclasses (usually `type`) | |
metameta = metaclass_for_bases(metaclasses) | |
# create a new metaclass | |
meta_reg[metaclasses] = derived = metameta( | |
'_'.join(m.__name__ for m in metaclasses), metaclasses, {} | |
) | |
return derived |
@pjeby Would you be so kind as to release this code under the MIT license, or another license of your choosing?
@pjeby would it be possible to implement the call to __set_name__
on descriptors?
Wow, I completely forgot I wrote this, and I also apparently haven't been getting comment notifications on it, either.
Anyway, please consider it public domain.
As for the __set_name__
protocol, that would be something I guess you'd add to pep487_meta.__init__
? Or more precisely, I guess you'd need to add pep487_meta.__new__
, since the current version of PEP 487 does everything from __new__
, not __init__
. It does make things more complicated, though.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Since it customizes metaclass it somewhat contradicts utility application of PEP 487.