Changing an MRO with a metaclass


When I first learned about metaclasses I wondered when exactly I should use them. This is generally an issue in programming for me that you can tackle a problem in many ways, each way has pros and cons, and you never really know what the best way is until you have tried it.

Back to metaclasses. I had an issue that one endpoint in an application I was working on was timing out. After profiling with cProfiler (and visualizing with snakeviz - I highly recommend this tool!) I found out that the issue was with a django.models.Model subclass that also inherited from a bunch of other superclasses and mixins. I will call this class MyModel.

One of these mixins (let's call it CulpritMixin) did some calculations when MyModel instance was initialized and these calculations were the reason why the endpoint timed-out. I was pretty sure we didn't need these calculations in this endpoint. So my thinking was: since we don't need the calculations, why not just remove the mixin?

Of course, I couldn't do just that because MyModel was being used in a lot of other places in the system where the mixin was actually useful. So I had to remove the mixin only for the one endpoint where MyModel was used. How do you do that? I figured I can just remove the mixin from the MRO of MyModel and that would solve the issue. One way to remove a class from an MRO is to use a metaclass!

class NoCulpritMixinMetaClass(type):
    def mro(cls) -> list[type]:
        tmp_mro = super().mro()
        tmp_mro.remove(CulpritMixin)
        return tmp_mro


class MyModelNoCulpritMixin(
    MyModel, metaclass=NoCulpritMixinMetaClass
):
    class Meta:
        proxy = True

Ok, so that was my first attempt. I run unit tests, and Django had some issues. For some reason there was a discrepancy in class attributes in Django machinery when it was loading the models.

After some debugging I was able to get it work:

class NoCulpritMixinMetaClass(type):
    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        cls._to_delete_attributes_ = set()
        for name in dir(cls):
            try:
                inspect.getattr_static(cls, name)
            except AttributeError:
                cls._to_delete_attributes_.add(name)

    def __dir__(cls):
        """There is some discrepancy with inspect.getattr_static and dir(model) in
        django guts after we remove CulpritMixin from mro which throws errors,
        so this fixes it."""

        original_dir = super().__dir__()
        for attr_to_delete in cls._to_delete_attributes_:
            original_dir.remove(attr_to_delete)
        return original_dir

    def mro(cls) -> list[type]:
        tmp_mro = super().mro()
        tmp_mro.remove(CulpritMixin)
        return tmp_mro

Ok, so now this solution is messy. At this point I was hesitant to show this code to colleagues for a code review. Firstly, I guess, using metaclass could be described as controversial. But in my first solution I had only modified def mro(cls) so I had thought it had been straightforward, and maybe it had justified the use of the metaclass.

But now I have been also getting mixed up with Django inner workings. Well, lets see if unit tests pass.

100% run successfully. Ok, I guess this maybe is not that bad. I've created an MR and wrote a detailed description of the problem.

Next day nobody really said anything about using the metaclass - the solution was to simply not initialize MyModel instance at all, and use MyModel.objects.filter(...).values_list(...) instead of MyModel.objects.filter(...)! This way the code of CulpritMixin is not called at all, and there are no heavy computations! Much simpler than my solution!

So in the end I haven't yet had an opportunity to use metaclasses "in the real world". I was actually pretty happy about that because it wasn't comfortable to tamper with Django guts.

To sum up, it turned out not to be the best use case for a metaclass, but I am still happy I have it in my toolbox!