Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to recover the mro of a class given its bases?

Suppose we are implementing a metaclass that needs to know the method resolution order before the class is instantiated.

class Meta(type):
    def __new__(cls, name, bases, namespace):
        mro = ...

Is there a builtin way to compute the mro, that is a way other than reimplementing the C3 algorithm?

like image 477
Olivier Melançon Avatar asked Sep 14 '25 20:09

Olivier Melançon


2 Answers

The simpler thing is to just create a temporary class, extract its __mro__, compute your things, and then create the metaclass for real:

class Meta(type):
    def __new__(metacls, name, bases, namespace):
        tmp_cls = super().__new__(metacls, name, bases, namespace)
        mro = tmp_cls.__mro__
        del tmp_cls  # Not actually needed, just to show you are done with it.
        ...
        # do stuff
        ...
        new_class = super().__new__(metacls, name, bases, namespace)
        ...
        return new_class

Supposing that can't be done, due to crazy side-effects on the metaclasses of some of the super-classes on the hierarchy - then the same idea, but cloning the classes in the bases to "stub" classes before doing it - but possibly, reimplementing the C3 algorithm is easier than that - and certainly more efficient, since for each class you'd create an N ** 2 number of stub superclasses, where N is the depth of your class hierarchy (well that could be cached should you pick this route).

Anyway, the code for that could be something along:

stub_cache = {object: object}

def get_stub_class(cls):
    # yields an mro-equivalent with no metaclass side-effects.
    if cls is object:
        return object
    stub_bases = []
    for base in cls.__bases__:
        stub_bases.append(get_stub_class(base))
    if cls not in stub_cache:
        stub_cache[cls] = type(cls.__name__, tuple(stub_bases), {})
    return stub_cache[cls]

def get_future_mro(name, bases):
    stub_bases = tuple(get_stub_class(base) for base in bases)
    stub_cls = type(name, stub_bases, {})
    reversed_cache = {value:key for key, value in stub_cache.items()}
    return [reversed_cache[mro_base] for mro_base in  stub_cls.__mro__[1:]]

class Meta(type):
    def __new__(metacls, name, bases, namespace):
        mro = get_future_mro(name, bases)
        print(mro)
        return super().__new__(metacls, name, bases, namespace)

(This thing works for basic cases I've tried in interactive mode - but there might be complicated edge cases not covered, with multiple metaclasses and such)

like image 95
jsbueno Avatar answered Sep 16 '25 09:09

jsbueno


It turns out the functools module has a _c3_merge function that implements the C3 linearization algorithm:

import functools

def compute_mro(*bases):
  return functools._c3_merge(base.mro() for base in bases)

# Example:
class F: pass

class E: pass

class D: pass

class C(D,F): pass

class B(D,E): pass

class A(B,C): pass

print(compute_mro(B, C)) # [B, C, D, E, F, object]

print(A.mro()) # [A, B, C, D, E, F, object]
like image 23
Olivier Melançon Avatar answered Sep 16 '25 09:09

Olivier Melançon