Skip to content

Commit 4e69cd3

Browse files
committed
New implementation of with_metaclass
1 parent 63b2661 commit 4e69cd3

File tree

2 files changed

+103
-12
lines changed

2 files changed

+103
-12
lines changed

six.py

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -816,20 +816,93 @@ def wrapper(f):
816816
wraps = functools.wraps
817817

818818

819+
class _WithMetaclass(type):
820+
"""
821+
Metaclasses to be used in six.with_metaclass: instances of
822+
_WithMetaclass are metaclasses, so this is a metametaclass.
823+
"""
824+
@classmethod
825+
def get(cls, meta, bases):
826+
"""
827+
Return a metaclass (an instance of _WithMetaclass) which, if an
828+
instance of it is used as base class, will create a class with
829+
metaclass "meta" and bases "bases".
830+
"""
831+
# We use "meta" as base class instead of "type" to support the
832+
# following wrong usage:
833+
#
834+
# class X(six.with_metaclass(M)):
835+
# __metaclass__ = M
836+
#
837+
return cls("metaclass", (meta,), dict(meta=meta, bases=bases))
838+
839+
def instance(self):
840+
"""
841+
Return an instance of the metaclass "self".
842+
"""
843+
return self.__new__(self, "temporary_class", (object,), {})
844+
845+
@property
846+
def __prepare__(self):
847+
# We forward __prepare__ to __prepare which is the actual
848+
# implementation.
849+
#
850+
# This is a property for 2 reasons:
851+
#
852+
# First of all, if the metaclass does not define __prepare__, we
853+
# pretend that we don't have a __prepare__ method either.
854+
# PEP 3115 says that a __prepare__ method is not required to
855+
# exist. Also, some user code might call __prepare__ on Py2 and
856+
# we gracefully handle that.
857+
#
858+
# Second, this is a property for a technical reason: an ordinary
859+
# __prepare__ method on the metametaclass would not be called,
860+
# since it is overridden by type.__prepare__ (as an application
861+
# of the general principle that instance attributes override
862+
# type attributes). A property works because it bypasses
863+
# attribute lookup on the instance.
864+
865+
# Check for __prepare__ attribute, propagate AttributeError
866+
self.meta.__prepare__
867+
return self.__prepare
868+
869+
def __prepare(self, name, __bases, **kwargs):
870+
"""
871+
Ensure that metaclass.__prepare__ is called with the correct
872+
arguments.
873+
"""
874+
return self.meta.__prepare__(name, self.bases, **kwargs)
875+
876+
def __call__(self, name, __bases, d):
877+
"""
878+
Create the eventual class with metaclass "self.meta" and bases
879+
"self.bases".
880+
"""
881+
if "__metaclass__" in d:
882+
from warnings import warn
883+
warn("when using six.with_metaclass, remove __metaclass__ from your class", DeprecationWarning)
884+
return self.meta(name, self.bases, d)
885+
886+
819887
def with_metaclass(meta, *bases):
820888
"""Create a base class with a metaclass."""
821-
# This requires a bit of explanation: the basic idea is to make a dummy
822-
# metaclass for one level of class instantiation that replaces itself with
823-
# the actual metaclass.
824-
class metaclass(type):
825-
826-
def __new__(cls, name, this_bases, d):
827-
return meta(name, bases, d)
828-
829-
@classmethod
830-
def __prepare__(cls, name, this_bases):
831-
return meta.__prepare__(name, bases)
832-
return type.__new__(metaclass, 'temporary_class', (), {})
889+
# This requires a bit of explanation: with_metaclass() returns
890+
# a temporary class which will setup the correct metaclass when
891+
# this temporary class is used as base class.
892+
#
893+
# In detail: let T = with_metaclass(meta, *bases). When the user
894+
# does "class X(with_metaclass(meta, *bases))", Python will first
895+
# determine the metaclass of X from its bases. In our case, there is
896+
# a single base class T. Therefore, the metaclass will be type(T).
897+
#
898+
# Next, Python will call type(T)("X", (T,), methods) and it is this
899+
# call that we want to override. So we need to define a __call__
900+
# method in the metaclass of type(T), which needs a metametaclass,
901+
# called "_WithMetaclass".
902+
# The metaclass type(T) is returned by _WithMetaclass.get(...) and
903+
# the instance() method creates an instance of this metaclass, which
904+
# is a regular class.
905+
return _WithMetaclass.get(meta, bases).instance()
833906

834907

835908
def add_metaclass(metaclass):

test_six.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,24 @@ class X(six.with_metaclass(Meta, *bases)):
772772
assert isinstance(getattr(X, 'namespace', {}), MyDict)
773773

774774

775+
def test_with_metaclass_no_prepare():
776+
"""Test with_metaclass when the metaclass has no __prepare__ method."""
777+
778+
class deleted_attribute(object):
779+
def __get__(self, instance, owner):
780+
raise AttributeError
781+
782+
class Meta(type):
783+
__prepare__ = deleted_attribute()
784+
785+
assert not hasattr(Meta, "__prepare__")
786+
787+
class X(six.with_metaclass(Meta)):
788+
pass
789+
790+
assert type(X) is Meta
791+
792+
775793
def test_wraps():
776794
def f(g):
777795
@six.wraps(g)

0 commit comments

Comments
 (0)