1

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?

wim
  • 338,267
  • 99
  • 616
  • 750
Tom
  • 2,545
  • 5
  • 31
  • 71
  • 4
    There are no metaclasses here... Do you mean the class nested inside your `MyModel` class that is named `Meta`? – juanpa.arrivillaga Sep 24 '21 at 21:43
  • 1
    Why did you think the `__init__` in `MyModel` would be at all related to some nested class, `Meta`? I'm assuming this is django related? Outside fo that, nested classes aren't often used in Python. In any case, it isn't exactly clear what behavior you are expecting – juanpa.arrivillaga Sep 24 '21 at 21:45
  • 2
    It can be hacked to work with metaclasses too, but it is **not** related to metaclasses; if you want to use metaclasses regardless, I suggest you read [this great post](https://stackoverflow.com/a/6581949/11647025) about them. – crissal Sep 24 '21 at 21:48

1 Answers1

2

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)))
Peter Badida
  • 11,310
  • 10
  • 44
  • 90
  • woaaaahhhh,thank you so much for the long answer! – Tom Sep 24 '21 at 22:53
  • 1
    Welcome, hopefully it helps! If you by any chance find a way for `CustomModelWithInheritance(Model, metaclass=MyMeta)` feel free to ping or edit. I has to work somehow, but I'm probably too tired for today and missing some basic silly thing :D Also, `Meta` in Django and DRF from what I've observed, is just a fancy dictionary so you don't need to use custom functions to prepare the model, or in other words, the core-devs just moved the logic to more abstract layer to make it more user-friendly and automatic. – Peter Badida Sep 24 '21 at 22:56
  • @Tom please check again after the edit, the class variable won't work. Or at least not in the way you might expect it to work (have values in a `ModelBase` metaclass). – Peter Badida Sep 25 '21 at 13:49
  • @Tom also, re the inheritance, as I've thought, silly thing, I forgot adding `(type)` so the class inherited from `object` instead of type, thus the MRO borked. – Peter Badida Sep 25 '21 at 14:13