dyve
dyve

Reputation: 6023

Add a dynamically generated Model to a models.py in an Django project

I am generating a Django model based on an abstract model class AbstractAttr and a normal model (let's say Foo).

I want my foo/models.py to look like this:

from bar.models import Attrs
# ...
class Foo(models.Model):
    ....
    attrs = Attrs()

In the Attrs class which mimics a field I have a contribute_to_class that generates the required model using type(). The generated model c is called FooAttr.

Everything works. If I migrate, I see FooAttr appear in the proper table.

EXCEPT FOR ONE THING.

I want to be able to from foo.models import FooAttr. Somehow my generated FooAttr class is not bound to the models.py file in which it is generated.

If I change my models.py to this:

class Foo(models.Model):
    # ...

FooAttr = generate_foo_attr_class(...)

it works, but this is not what I want (for example, this forces the dev to guess the generate class name).

Is what I want possible, define the class somewhat like in the first example AND bind it to the specific models.py module?

The project (pre-Alpha) is here (in develop branch): https://github.com/zostera/django-mav

Some relevant code:

def create_model_attribute_class(model_class, class_name=None, related_name=None, meta=None):
    """
    Generate a value class (derived from AbstractModelAttribute) for a given model class
    :param model_class: The model to create a AbstractModelAttribute class for
    :param class_name: The name of the AbstractModelAttribute class to generate
    :param related_name: The related name
    :return: A model derives from AbstractModelAttribute with an object pointing to model_class
    """

    if model_class._meta.abstract:
        # This can't be done, because `object = ForeignKey(model_class)` would fail.
        raise TypeError("Can't create attrs for abstract class {0}".format(model_class.__name__))

    # Define inner Meta class
    if not meta:
        meta = {}
    meta['app_label'] = model_class._meta.app_label
    meta['db_tablespace'] = model_class._meta.db_tablespace
    meta['managed'] = model_class._meta.managed
    meta['unique_together'] = list(meta.get('unique_together', [])) + [('attribute', 'object')]
    meta.setdefault('db_table', '{0}_attr'.format(model_class._meta.db_table))

    # The name of the class to generate
    if class_name is None:
        value_class_name = '{name}Attr'.format(name=model_class.__name__)
    else:
        value_class_name = class_name

    # The related name to set
    if related_name is None:
        model_class_related_name = 'attrs'
    else:
        model_class_related_name = related_name

    # Make a type for our class
    value_class = type(
        str(value_class_name),
        (AbstractModelAttribute,),
        dict(
            # Set to same module as model_class
            __module__=model_class.__module__,
            # Add a foreign key to model_class
            object=models.ForeignKey(
                model_class,
                related_name=model_class_related_name
            ),
            # Add Meta class
            Meta=type(
                str('Meta'),
                (object,),
                meta
            ),
        ))

    return value_class


class Attrs(object):
    def contribute_to_class(self, cls, name):
        # Called from django.db.models.base.ModelBase.__new__
        mav_class = create_model_attribute_class(model_class=cls, related_name=name)
        cls.ModelAttributeClass = mav_class

Upvotes: 2

Views: 167

Answers (2)

dyve
dyve

Reputation: 6023

Thanks all for thinking about this. I have updated the source code of the project at GitHub and added more tests. See https://github.com/zostera/django-mav

Since the actual generation of the models is done outside of foo/models.py (it takes place in mav/models.py, it seems Pythonically impossible to link the model to foo/models.py. Also, after rethinking this, it seems to automagically for Python (explicit is better, no magic).

So my new strategy is to use simple functions, a decorator to make it easy to add mav, and link the generated models to mac/attrs.py, so I can universally from mav.attrs import FooAttr. I also link the generated class to the Foo model as Foo._mav_class.

(In this comment, Foo is of course used as an example model that we want to add model-attribute-value to).

Upvotes: 0

Adriaan Tijsseling
Adriaan Tijsseling

Reputation: 2015

I see you create the model from within models.py, so I think you should be able to add it to the module's globals. How about this:

new_class = create_model_attribute_class(**kwargs)
globals()[new_class.__name__] = new_class
del new_class  # no need to keep original around

Upvotes: 1

Related Questions