Skip to content

Instantly share code, notes, and snippets.

@pjeby
Last active June 5, 2019 15:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pjeby/75ca26f8d2a7a0c68e30 to your computer and use it in GitHub Desktop.
Save pjeby/75ca26f8d2a7a0c68e30 to your computer and use it in GitHub Desktop.
Pure-Python PEP 487 Implementation, usable back to Python 3.1
"""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
"""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
@Kentzo
Copy link

Kentzo commented Oct 26, 2017

Since it customizes metaclass it somewhat contradicts utility application of PEP 487.

@tysonclugg
Copy link

@pjeby Would you be so kind as to release this code under the MIT license, or another license of your choosing?

@graingert
Copy link

@pjeby would it be possible to implement the call to __set_name__ on descriptors?

@pjeby
Copy link
Author

pjeby commented Jun 5, 2019

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