Question

I am making a metaclass where I customize the __new__ method to customize how the new class will be created according to the provided values in kwargs. This probably makes more sense in an example:

class FooMeta(type):
    def __new__(cls, name, bases, kwargs):
        # do something with the kwargs...
        # for example:
        if 'foo' in kwargs:
            kwargs.update({
                'fooattr': 'foovalue'
            })
        return super(FooMeta, cls).__new__(cls, name, bases, kwargs)

My problem is how can I make this compatible for both Python 2 and 3. Six is a great compatibility library however it does not solve my problem. You would use it as:

class FooClass(six.with_metaclass(FooMeta, FooBase)):
    pass

This does not work because six creates a new base class by using the given metaclass. Below is the six's code (link) (as of 1.3):

def with_metaclass(meta, base=object):
    return meta("NewBase", (base,), {})

As a result, my __new__ method will be called without any kwargs hence essentially "breaking" my function. The question is how can I accomplish the behavior I want without breaking the compatibility for both Python 2 and 3. Please note that I don't need Python 2.6 support so if something is possible for only Python 2.7.x and 3.x I am fine with that.

Background

I need this to work in Django. I want to create a model factory metaclass which depending on the model attributes will customize how the model is created.

class ModelMeta(ModelBase):
    def __new__(cls, name, bases, kwargs):
        # depending on kwargs a different model is constructed
        # ...

class FooModel(six.with_metaclass(ModelMeta, models.Model):
    # some attrs here to be passed to the metaclass
Was it helpful?

Solution

If I understand you right, there is no problem and it already works. Did you actually check whether your metaclass has the desired effect? Because I think it already does.

The class returned by with_metaclass is not meant to play the role of your class FooClass. It is a dummy class used as the base class of your class. Since this base class is of metaclass FooMeta, your derived class will also have metaclass FooMeta and so the metaclass will be called again with the appropriate arguments.

class FooMeta(type):
    def __new__(cls, name, bases, attrs):
        # do something with the kwargs...
        # for example:
        if 'foo' in attrs:
            attrs['fooattr'] = 'foovalue'
        return super(FooMeta, cls).__new__(cls, name, bases, attrs)
class FooBase(object):
    pass
class FooClass(with_metaclass(FooMeta, FooBase)):
    foo = "Yes"

>>> FooClass.fooattr
'foovalue'

Incidentally, it is confusing to call the third argument of your metaclass kwargs. It isn't really keyword arguments. It is the __dict__ of the class to be created --- that is, the class's attributes and their values. In my experience this attribute is conventionally called attrs or perhaps dct (see e.g., here and here).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top