Force A Class With Multiple Inheritance To Have A Specific Metaclass In Python

- 1 answer

I have a class (a Marshmallow schema) that subclasses two parents:

class OptionalLinkSchema(JsonApiSchema, ModelSchema): pass
  • JsonApiSchema has the metaclass SchemaMeta
  • ModelSchema has the metaclass ModelSchemaMeta(SchemaMeta) (it is a subclass of ModelSchema).

Now, I don't want the ModelSchemaMeta metaclass for my class, I just want the simpler SchemaMeta. However, according to the Python docs, "The most derived metaclass is selected", meaning that no matter what I do, ModelSchemaMeta will be selected as the metaclass.

Even if I try to manually choose a metaclass, the same thing happens:

class OptionalLinkSchema(JsonApiSchema, ModelSchema, metaclass=SchemaMeta): pass

<class 'marshmallow_sqlalchemy.schema.ModelSchemaMeta'>

Is there no way to override Python's behaviour of always choosing the most derived metaclass?



First things first: I should warn you that you likely have an "xy" problem there: If a class is designed to work counting with the mechanisms in its metaclass, if those are not run when it is created, chances are the class won't work.

Second thing: the language won't do that "on its own". And it won't because it would break things for the reason above. So, there are some approaches that can be taken, all intrusive, and expecting that you know what you are doing. If your OptionaLinkSchema depends on any features on ModelSchemaMeta, it will simply break.

I can think of 3 ways of achieving the equivaent of "stripping an inherited metaclass"

First approach

You don't say anything on your question, but I see no ModelSchemaMeta in Marshmallow's source tree - is it a metaclass that you have created? If so, since Marshmallow uses the "djangoish" idiom of having a nested Meta class to declare things about the class itself, you could use an attribute in this Meta namespace to skip your own metaclass.

If so, add code like this to your undesired metaclass' __new__ method (I am picking the "meta" parsing code from Marshmallow's source code itself):

class ModelSchemaMeta(SchemaMeta):

    def __new__(mcs, name, bases, attrs):
        meta = attrs.get("Meta")
        if getattr(meta, "skip_model_schema_meta", False):
            # force ShemaMeta metaclass
            cls = super().__new__(SchemaMeta, name, bases, attrs)
            # manually call `__init__` because Python does not do that
            # if the value returned from `__new__` is not an instance
            # of `mcs`
            cls.__init__(name, bases, attrs)
            return cls
        # Your original ModelSchemaMeta source code goes here

class OptionalLinkSchema(...):

    class Meta:
        skip_model_schema_meta = True

Second approach

A severely more invasive way of doing this, but that would work for any class inheritance tree is having code that will take the superclass with the unwanted metaclass and create a clone of it. However, since there is a custom metaclass other than type involved, chances of this working are slim in this case - as the "cloning" process won't feed the "grandparent" metaclass the expected raw fields it wants to convert to the attributes in the final class. Also, Marsmallow uses a class registry - creating such an intermediate clone would have the clone registered there as well.

In short: this approach does not apply to your case, but I describe it here since it might be useful for others that hit this question:

def strip_meta(cls,):
    "builds a clone of cls, stripping the most derived metaclass it has.

    There are no warranties the resulting cls will work at all - specially
    if one or more of the metaclasses that are being kept make transformations
    on the attributes as they are declared in the class body.
    new_meta = type(cls).__mro__[1:]
    return (new_meta(cls.__name__, cls.__mro__, dict(cls.__dict__)))

class OptionalLinkSchema(FirstClass, strip_meta(SecondClass)):

(Anyone trying to use this not that if the SecondClass is itself derived from others that use the unwanted metaclass, the strategy have to be modified to recursively strip the metaclass of the superclasses as well)

Third approach

Another, simpler approach would be to create manually the derived metaclass and just hardcode the calls to the desired metaclass, skipping the superclass. This thing might yield "sober" enough code to be actually usable in production.

Therefore, you do not "set an arbitrary metaclass" - instead, you create a valid metaclass that does what you want.

class NewMeta(ModelSchemaMeta, SchemaMeta):
    """Order of inheritance matters: we ant to skip methods on the first 
    baseclass, by hardwiring calls to SchemaMeta. If we do it the other way around,
    `super` calls on SchemaMeta will go to ModelSchemaMeta).

    Also, if ModeSchemaMeta inherits from SchemaMeta, you can inherit from it alone


    def __new__(*args, **kw):
        return SchemaMeta.__new__(*args, **kw)

    def __init__(*args, **kw):
        return SchemaMeta.__init__(*args, **kw)

    # repeat for other methos you want to use from SchemaMeta
    # also, undesired attributes can be set to "None":

    undesired_method_in_model_schema_meta = None

The difference from this to the first approach is that ModelSchemaMeta will be the inheritance tree for the metaclass, and the final metaclass will be this new, derived metaclass. But this won't depend on you having control of ModelSchemaMeta code - and, as I said before, this is more "within the expected rules" of the language than the second approach.