Skip to content

Commit

Permalink
Define __prepare__() in with_metaclass() (#178)
Browse files Browse the repository at this point in the history
Define `__prepare__()` in `with_metaclass()`'s temporary
metaclass, and make sure that it passes the correct bases to the
real metaclass's `__prepare__()`.

The temporary metaclass previously didn't extend the
`__prepare__()` method, which meant that if the real metaclass
had a `__prepare__()`, it wouldn't get called correctly. This
could lead to bugs in Python 3 code.

The temporary metaclass's `__prepare__()` gets called with
```bases=(temporary_class,)```.  Since there was no proxy in the
middle, that was getting passed directly to the real metaclass's
`__prepare__()`. But then, if the real class's `__prepare__()`
method depended on the bases, the logic would be incorrect.

This was a problem in projects that use `enum` / `enum34` and
try to use `with_metaclass(EnumMeta)`. `enum34.EnumMeta` doesn't
define `__prepare__()`, since it is a Python 2 backport. Python
3's `enum.EnumMeta` does define `__prepare__()`, but originally
didn't depend at all on the bases. But starting in Python 3.6,
`enum.EnumMeta.__prepare__()` will raise `TypeError` if the
bases aren't valid for an enum subclass. Thus, a codebase that
was successfully using `enum` / `enum34` and
`with_metaclass(EnumMeta)` could break on Python 3.6.
  • Loading branch information
jmoldow authored and benjaminp committed Sep 17, 2017
1 parent 024fcbb commit 96b9328
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 0 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ Peter Ruibal
Miroslav Shubernetskiy
Anthony Sottile
Lucas Wiman
Jordan Moldow

If you think you belong on this list, please let me know! --Benjamin
4 changes: 4 additions & 0 deletions six.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,10 @@ 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', (), {})


Expand Down
28 changes: 28 additions & 0 deletions test_six.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,34 @@ class X(six.with_metaclass(Meta, Base, Base2)):
assert X.__mro__ == (X, Base, Base2, object)


@py.test.mark.skipif("sys.version_info[:2] < (3, 0)")
def test_with_metaclass_prepare():
"""Test that with_metaclass causes Meta.__prepare__ to be called with the correct arguments."""

class MyDict(dict):
pass

class Meta(type):

@classmethod
def __prepare__(cls, name, bases):
namespace = MyDict(super().__prepare__(name, bases), cls=cls, bases=bases)
namespace['namespace'] = namespace
return namespace

class Base(object):
pass

bases = (Base,)

class X(six.with_metaclass(Meta, *bases)):
pass

assert getattr(X, 'cls', type) is Meta
assert getattr(X, 'bases', ()) == bases
assert isinstance(getattr(X, 'namespace', {}), MyDict)


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

0 comments on commit 96b9328

Please sign in to comment.