Skip to content

Commit

Permalink
New implementation of with_metaclass
Browse files Browse the repository at this point in the history
  • Loading branch information
jdemeyer committed Oct 20, 2017
1 parent 63b2661 commit 4e69cd3
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 12 deletions.
97 changes: 85 additions & 12 deletions six.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,20 +816,93 @@ def wrapper(f):
wraps = functools.wraps


class _WithMetaclass(type):
"""
Metaclasses to be used in six.with_metaclass: instances of
_WithMetaclass are metaclasses, so this is a metametaclass.
"""
@classmethod
def get(cls, meta, bases):
"""
Return a metaclass (an instance of _WithMetaclass) which, if an
instance of it is used as base class, will create a class with
metaclass "meta" and bases "bases".
"""
# We use "meta" as base class instead of "type" to support the
# following wrong usage:
#
# class X(six.with_metaclass(M)):
# __metaclass__ = M
#
return cls("metaclass", (meta,), dict(meta=meta, bases=bases))

def instance(self):
"""
Return an instance of the metaclass "self".
"""
return self.__new__(self, "temporary_class", (object,), {})

@property
def __prepare__(self):
# We forward __prepare__ to __prepare which is the actual
# implementation.
#
# This is a property for 2 reasons:
#
# First of all, if the metaclass does not define __prepare__, we
# pretend that we don't have a __prepare__ method either.
# PEP 3115 says that a __prepare__ method is not required to
# exist. Also, some user code might call __prepare__ on Py2 and
# we gracefully handle that.
#
# Second, this is a property for a technical reason: an ordinary
# __prepare__ method on the metametaclass would not be called,
# since it is overridden by type.__prepare__ (as an application
# of the general principle that instance attributes override
# type attributes). A property works because it bypasses
# attribute lookup on the instance.

# Check for __prepare__ attribute, propagate AttributeError
self.meta.__prepare__
return self.__prepare

def __prepare(self, name, __bases, **kwargs):
"""
Ensure that metaclass.__prepare__ is called with the correct
arguments.
"""
return self.meta.__prepare__(name, self.bases, **kwargs)

def __call__(self, name, __bases, d):
"""
Create the eventual class with metaclass "self.meta" and bases
"self.bases".
"""
if "__metaclass__" in d:
from warnings import warn
warn("when using six.with_metaclass, remove __metaclass__ from your class", DeprecationWarning)
return self.meta(name, self.bases, d)


def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(type):

def __new__(cls, name, this_bases, d):
return meta(name, bases, d)

@classmethod
def __prepare__(cls, name, this_bases):
return meta.__prepare__(name, bases)
return type.__new__(metaclass, 'temporary_class', (), {})
# This requires a bit of explanation: with_metaclass() returns
# a temporary class which will setup the correct metaclass when
# this temporary class is used as base class.
#
# In detail: let T = with_metaclass(meta, *bases). When the user
# does "class X(with_metaclass(meta, *bases))", Python will first
# determine the metaclass of X from its bases. In our case, there is
# a single base class T. Therefore, the metaclass will be type(T).
#
# Next, Python will call type(T)("X", (T,), methods) and it is this
# call that we want to override. So we need to define a __call__
# method in the metaclass of type(T), which needs a metametaclass,
# called "_WithMetaclass".
# The metaclass type(T) is returned by _WithMetaclass.get(...) and
# the instance() method creates an instance of this metaclass, which
# is a regular class.
return _WithMetaclass.get(meta, bases).instance()


def add_metaclass(metaclass):
Expand Down
18 changes: 18 additions & 0 deletions test_six.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,24 @@ class X(six.with_metaclass(Meta, *bases)):
assert isinstance(getattr(X, 'namespace', {}), MyDict)


def test_with_metaclass_no_prepare():
"""Test with_metaclass when the metaclass has no __prepare__ method."""

class deleted_attribute(object):
def __get__(self, instance, owner):
raise AttributeError

class Meta(type):
__prepare__ = deleted_attribute()

assert not hasattr(Meta, "__prepare__")

class X(six.with_metaclass(Meta)):
pass

assert type(X) is Meta


def test_wraps():
def f(g):
@six.wraps(g)
Expand Down

0 comments on commit 4e69cd3

Please sign in to comment.