Search code examples
pythonclassmetaclass

how to pass arguments into a nested class in python


I have the following model:

def get_my_model(name1, name2):
    class MyModel(Model):
       class Meta: 
             table1 = name1
             table2 = name2
    return MyModel
  
     

I am trying to write this without the function and want to pass the arguments name1 and name2 directly to the Meta class but I'm not sure how. I tried using constructors but it somehow doesn't work.

class MyModel(Model):
       def __init__(self, name1, name2):
             self.name1 = name1
             self.name2 = name2

       class Meta: 
             table1 = self.name1
             table2 = self.name2 

I also tried setting the constructor on the meta class but didn't work either. Anyone an idea?


Solution

  • TL;DR:

    • any reachable variable from MyModel or any higher scope's variable up from the class named Meta, that's already defined
    • modify ModelBase's metaclass (patch the file or inherit from)

    It's not a metaclass! It's just a class in other class's scope named Meta. What's happening is that Python has separate contexts/scopes once the execution environment starts building:

    • interpreter
    • global (the original namespace you're given once you start the interpreter, globals())

    then the nested ones within the global namespace:

    • module/file
    • class
    • function
    • perhaps some other(s)

    You can't pass a parameter to the Meta class because it's just declared in there. It's not called. Parallel to this would be passing a parameter to a class declaration from the module scope:

    # module.py
    class MyClass:
        value = <how?>
    

    Once you find the place where it's called, then you can inject the parameters by modifying the caller function.

    class Main:
        class Meta:
            def __init__(self, *args, **kwargs):
                print("Meta", args, kwargs)
    
        def __init__(self, name):
            Main.Meta(name)
    
    print(Main(123))
    

    If I don't explicitly call Main.Meta(), the Meta class in this example won't be instantiated.

    In case of Django, the Meta class is pulled via getattr() for the model class here, therefore you need to target ModelBase.__new__() with super()/copy-paste to modify the function so it accepts custom arguments, or, pass them as just class variables (how it's mostly done in Django / DRF).

    class Main:
        class Meta:
            class_var = 123
    

    Judging from the implementation of Model class in Django you might be able to swap the metaclass value, but I'm not sure about the inheritance as the __new__() of a metaclass is executed when you declare the class that uses it:

    # the original ModelBase from class Model(metaclass=ModelBase)
    class Meta(type):
        def __new__(cls, *_, **__):
            print("Hello")
            return cls
    
    class MyMeta(Meta):
        def __new__(cls, *_, **__):
            print("Hi")
            # implement your stuff here or copy-paste + patch
            return cls
    
    class Model(metaclass=Meta):
        pass
    
    class CustomModel(metaclass=MyMeta):
        pass
    
    class CustomModelWithInheritance(Model, metaclass=MyMeta):
        pass
    

    For metaclasses check:

    Regarding self: The naming itself doesn't matter, nor will work where you use it, because the self is just an implicitly passed instance of a class into a method (a function with a reference to a class instance):

    class MyClass:
        def func(self_or_other_name):
            print(self_or_other_name)
    MyClass().func()
    

    The same way behaves a cls argument in a __new__() method when creating a class within a metaclass i.e. it's a reference to the metaclass instance (a class declaration in a namespace), for which the "description" is the metaclass that creates it.

    cls = type("MyClass", (), {})  # create an instance of "type"
    cls  # <class '__main__.MyClass'>
    # "cls" is class and an instance at the same time
    

    The only "special" variable you can use to refer to the class scope are locals() + anything defined within the class scope as a variable + anything in a higher scope be it from a nested class, module or others:

    Edit:

    So for the class variable it was a screw up, this is the correct explanation. For your case it'll be either a function or metaclass patching because we're dealing here with finding the beginning of a "chicken & egg" problem. And that we can do by either looking from above - introducing a function scope and setting values prior the defining of the upper class is finished -, or looking from inside of the creation process (metaclass).

    We can't use a class variable, because we're still in the process of defining a class. We also can't use the __init__ to reference the class (or Meta nested class) to inject a value there, because we're already in a situation where the metaclass' __new__() method has already been executed, thus anything you set there may be used in your case, but won't be present while creating the MyModel class.

    class MyModel(Model):
        def __init__(self, name):
            MyModel.name = name  # MyModel not defined yet
            # but we can use self.__class__ in the post-poned execution
            # once MyModel is defined (case when calling __init__())
        # we haven't even started to define the Meta nested class
    
        # starting to define Meta nested class
        class Meta:
            # MyModel not defined yet because the code block
            # of MyModel class isn't finished yet
            table = MyModel.name
            # can't reference because it doesn't exist yet,
            # same applies for Meta, the only thing you have now is
            # the __qualname__ string
    

    But!

    from pprint import pprint
    
    class RealMetaClass(type):
        def __new__(cls, *_, **__):
            # create a new class - "Main" because it uses "metaclass" kwarg
            new_class = super().__new__(cls, *_, **__)
            # once we have the "Main" class, we can reference it the same way
            # like *after* we define it the normal way
            nested_meta = getattr(new_class, "NestedClassNamedMeta")
    
            # ~= pprint(getattr(Main, "NestedClassNamedMeta"))
            pprint(("in metaclass", nested_meta))
            pprint(("in metaclass", dir(nested_meta)))
            pprint(("in metaclass", vars(nested_meta)))
            return new_class
    
    
    class CustomMetaClass(RealMetaClass):
        def __new__(cls, *_, **__):
            new_class = super().__new__(cls, *_, **__)
            nested_meta = getattr(new_class, "NestedClassNamedMeta")
            # set a new class variable without affecting previous __new__() call
            new_class.custom_thing = getattr(nested_meta, "custom_thing", None)
            # do my stuff with custom attribute, that's not handled by Django
            return new_class
    
    
    class Main(metaclass=RealMetaClass):
        pprint("defining Main class")
    
        def __init__(self, name):
            pprint(("in instance", self.__class__.NestedClassNamedMeta.name))
            # works, because it's called after the Main class block
            # is finished and executed i.e. defined
            self.__class__.NestedClassNamedMeta.new_value = "hello"
    
        class NestedClassNamedMeta:
            name = "John"
            custom_thing = "custom"
    
    
    class CustomMain(Main, metaclass=CustomMetaClass):
        class NestedClassNamedMeta:
            name = "John"
            custom_thing = "custom"
    
    instance = Main("Fred")
    # custom_thing is ignored, not added to instance.__class__
    pprint(("after init", vars(instance.__class__), vars(instance.NestedClassNamedMeta)))
    
    instance = CustomMain("Fred")
    # custom_thing is processed, added to instance.__class__
    pprint(("after init", vars(instance.__class__), vars(instance.NestedClassNamedMeta)))