Динамически расширяемые модели Django с использованием метакласса - PullRequest
1 голос
/ 02 июля 2019

Проблема

Я пытаюсь построить фреймворк, который автоматически расширяет классы моделей дополнительными полями.Вот краткое резюме того, что я пытаюсь сделать:

Учитывая класс модели

class Pizza(models.Model):
    name = models.CharField(max_length=10)
    price = models.DecimalField(max_digits=10, decimal_places=2)

Я автоматически хочу создать класс с дополнительным полем для каждого поля класса, получая класс, аналогичныйк следующему:

class PizzaGenerated(models.Model):
    name = models.CharField(max_length=10)
    name_new = models.CharField(max_length=10)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    price_new = models.DecimalField(max_digits=10, decimal_places=2)

, как вы можете видеть, для каждого из свойств Pizza было добавлено дополнительное поле с суффиксом _new.

Мне нужен мойрешение работать независимо от структуры модели.В частности, я ищу способ, позволяющий реплицировать ForeignKey -Fields

Мой подход

Приведенный выше пример расширения класса Pizza можно решить с помощью следующего кода:

class ResMetaclass(models.base.ModelBase):
    def __new__(cls, name, bases, attrs):​
        fields = {
            k: v for k, v in attrs.items() if not k.startswith('_') and isinstance(v, models.Field)
        }
​
        attrs_extended = {
            **attrs,
            **{fieldname + '_new': fieldtype.clone() for fieldname, fieldtype in fields.items()}
        }
​​
        bases = (models.Model,)
        clsobj = super().__new__(cls, name, bases, attrs_extended)
​
        return clsobj

class EntityBase(models.Model, metaclass=ResMetaclass):
    class Meta:
        abstract = True
​
class Pizza(EntityBase):
    name = models.CharField(max_length=10)
    price = models.DecimalField(max_digits=10, decimal_places=2)

Класс Pizza успешно расширен метаклассом EntityMetaclass.

Проблема с этим

К сожалению, приведенный выше код завершается ошибкой, когда модельсодержит ForeignKey -Field, дающее следующую обратную трассировку:

  File "manage.py", line 17, in main
    execute_from_command_line(sys.argv)
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/core/management/__init__.py", line 357, in execute
    django.setup()
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/apps/registry.py", line 114, in populate
    app_config.import_models()
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/apps/config.py", line 211, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib64/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/niklas/dev/pyflx/mig/models.py", line 20, in <module>
    class Contact(EntityBase):
  File "/home/niklas/dev/pyflx/pyflx/models.py", line 17, in __new__
    **{fieldname + '_patch_value': fieldtype.clone() for fieldname, fieldtype in fields.items() if fieldname != 'id'},
  File "/home/niklas/dev/pyflx/pyflx/models.py", line 17, in <dictcomp>
    **{fieldname + '_patch_value': fieldtype.clone() for fieldname, fieldtype in fields.items() if fieldname != 'id'},
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/db/models/fields/__init__.py", line 492, in clone
    name, path, args, kwargs = self.deconstruct()
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/db/models/fields/related.py", line 856, in deconstruct
    name, path, args, kwargs = super().deconstruct()
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/db/models/fields/related.py", line 583, in deconstruct
    swappable_setting = self.swappable_setting
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/db/models/fields/related.py", line 374, in swappable_setting
    return apps.get_swappable_settings_name(to_string)
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/apps/registry.py", line 288, in get_swappable_settings_name
    for model in self.get_models(include_swapped=True):
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/apps/registry.py", line 178, in get_models
    self.check_models_ready()
  File "/home/niklas/dev/pyflx/venv/lib64/python3.7/site-packages/django/apps/registry.py", line 140, in check_models_ready
    raise AppRegistryNotReady("Models aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.

Есть ли способ обойти это?

1 Ответ

1 голос
/ 03 июля 2019

Tricky!

Проблема в том, что при попытке .clone поля в моделях они не полностью инициализируются, и, таким образом, механизмы Django находят стороннюю модель по ее строкеИмя qualifeid не может быть использовано.Проблема состоит в том, что код не проверяет, передается ли иностранная модель как ссылка на класс, а не как строка.

Единственный способ обойти это, кажется, это обезьянь-исправление этих проверок при создании классов,При этом, при клонировании поля стороннего объекта, поле related_field - обратная ссылка, автоматически создаваемая django ORM, чтобы можно было добраться от объекта «острие» до объекта «держатель», должно быть явно передано новому клонированному объекту., поле.В противном случае он будет указывать на исходное поле.Это требует немного большего количества monkeypatching, чтобы вставить явный параметр «related_name» во внутреннюю работу вызова .clone.

Те две вещи, которые были достигнуты, похоже, работают.Вот код, который я использовал, основываясь на вашем:

from django.db import models
from django.db.models.fields import related
from unittest.mock import patch

class ResMetaclass(models.base.ModelBase):
    def __new__(cls, name, bases, attrs):

        fields = {
            k: v for k, v in attrs.items() if not k.startswith('_') and isinstance(v, models.Field)
        }
        new_fields = {}
        for field_name, field in fields.items():
            new_field_name = field_name + "_new"
            if not isinstance(field, related.RelatedField):
                new_fields[new_field_name] = field.clone()
            else:
                real_deconstruct = field.deconstruct
                def _deconstruct():
                    name, path, args, kwargs = real_deconstruct()
                    kwargs["related_name"] = new_field_name
                    return name, path, args, kwargs

                with patch("django.apps.registry.apps.check_models_ready", lambda: True):
                    field.deconstruct = _deconstruct
                    # Assume foregnKeys are always within the same file, and
                    # disable model-ready checking:
                    new_fields[new_field_name] = field.clone()
                    del field.deconstruct


        attrs_extended = {
            **attrs,
            **new_fields
        }

        bases = (models.Model,)
        clsobj = super().__new__(cls, name, bases, attrs_extended)

        return clsobj

class EntityBase(models.Model, metaclass=ResMetaclass):
    class Meta:
        abstract = True

class Pizza(EntityBase):
    name = models.CharField(max_length=10)
    price = models.DecimalField(max_digits=10, decimal_places=2)


class MenuEntry(EntityBase):
    entry_number =  models.IntegerField()
    pizza = models.ForeignKey("Pizza", on_delete="cascade")

И результирующие поля в классе MenuEntry:

In [1]: from test1.models import Pizza, MenuEntry                                                                                                      

In [2]: MenuEntry._meta.fields                                                                                                                         
Out[2]: 
(<django.db.models.fields.AutoField: id>,
 <django.db.models.fields.IntegerField: entry_number>,
 <django.db.models.fields.related.ForeignKey: pizza>,
 <django.db.models.fields.IntegerField: entry_number_new>,
 <django.db.models.fields.related.ForeignKey: pizza_new>)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...