I'm trying to dynamically generate a new Model, based on fields from an existing Model. Both are defined in /apps/main/models.py
. The existing model looks something like this:
from django.db import models
class People(models.Model):
name = models.CharField(max_length=32)
age = models.IntegerField()
height = models.IntegerField()
I have a list containing the names of fields that I would like to copy:
target_fields = ["name", "age"]
I want to generate a new model the has all of the Fields named in target_fields
, but in this case they should be indexed (db_index = True
).
I originally hoped that I would just be able to iterate over the class properties of People
and use copy.copy
to copy the field descriptions that are defined on it. Like this:
from copy import copy
d = {}
for field_name in target_fields:
old_field = getattr(People, field_name) # alas, AttributeError
new_field = copy(old_field)
new_field.db_index = True
d[field_name] = new_field
IndexedPeople = type("IndexedPeople", (models.Model,), d)
I wasn't sure if copy.copy()
ing Fields would work, but I didn't get far enough to find out: the fields listed in the class definition don't aren't actually included as properties on the class object. I assume they're used for some metaclass shenanigans instead.
After poking around in the debugger, I found some type of Field objects listed in People._meta.local_fields
. However, these aren't just simple description that can be copy.copy()
ed and used to describe another model. For example, they include a .model
property referring to People
.
How can I create a field description for a new model based on a field of an existing model?
From poking around in the debugger and the source: all Django models use the ModelBase
metaclass defined in /db/models/base.py
. For each field in a model's class definition, ModelBase
's .add_to_class
method will call the field's .contribute_to_class
method.
Field.contribute_to_class
is defined in /db/models/fields/__init__.py
and it is what's responsible for associating a field definition with a particular model. The field is modified by adding the .model
property and by calling the .set_attributes_from_name
method with the name used in the model's class definition. This in turn adds adds the .attname
and .column
properties and sets .name
and .verbose_name
if necessary.
When I inspect the __dict__
property of a newly-defined CharField
and compare it with that of a CharField
that was already associated with a model, I also see that these are the only differences:
.creation_counter
property is unique for each instance..attrname
, .column
and .model
properties do not exist on the new instance..name
and .verbose_name
properties is None
on the new instance.It doesn't seem possible to distinguish between .name
/.verbose_name
properties that were manually specified to the constructor and ones that were automatically generated. You'll need to chose either to always reset them, ignoring any manually-specified values, or never clear them, which would cause them to always ignore any new name they were given in the new model. I want to use the same name as the original fields, so I am not going to touch them.
Knowing what differences exist, I am using copy.copy()
to clone the existing instance, then apply these changes to make it behave like a new instance.
import copy
from django.db import models
def copy_field(f):
fp = copy.copy(f)
fp.creation_counter = models.Field.creation_counter
models.Field.creation_counter += 1
if hasattr(f, "model"):
del fp.attname
del fp.column
del fp.model
# you may set .name and .verbose_name to None here
return fp
Given this function, I create the new Model with the following:
target_field_name = "name"
target_field = People._meta.get_field_by_name(target_field_name)[0]
model_fields = {}
model_fields["value"] = copy_field(target_field)
model_fields["value"].db_index = True
model_fields["__module__"] = People.__module__
NewModel = type("People_index_" + field_name, (models.Model,), model_fields)
It works!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With