Opened 18 months ago

Last modified 4 weeks ago

#34555 new Bug

ModelBase metaclass implementation prevents addition of model fields via __init_subclass__ — at Version 2

Reported by: hottwaj Owned by: nobody
Component: Database layer (models, ORM) Version: 4.2
Severity: Normal Keywords: ModelBase init_subclass
Cc: Carlton Gibson Triage Stage: Someday/Maybe
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by hottwaj)

The current implementation of ModelBase.new prevents addition of model fields via python's init_subclass method (described in PEP 487 and provided since python 3.6).

My understanding is that currently, fields can be added to models outside of a class body by:
i) calling "model_cls.add_to_class(...)" after the definition of model_cls e.g. in module level code after class definition,
ii) using a class decorator to call .add_to_class as in point i), or
iii) using a metaclass to completely customise class creation

Inheritance and init_subclass should in theory provide a relatively straightforward way to customise class creation without using a metaclass (option iii above), as described/encouraged in PEP 487, but Django does not currently support this for Field attributes of subclasses of Model because the ModelBase metaclass does not correctly pickup Fields added to a class during the execution of init_subclass

It seems that init_subclass is called after ModelBase.new collects Fields that require calling of their "contribute_to_class" method, so ModelBase does not do appropriate bookkeeping on fields added in init_subclass and such Fields are then ultimately not "seen" by e.g. the migration builder.

Correctly collecting Fields added by init_subclass in ModelBase.new would allow for easier customisation of model fields outside of a model class body and provide an implementation that works with init_subclass in a way that matches (rather than contrary to) behaviour supported elsewhere in python.

A simple example that currently does not work, but I believe ought to work, is provided below. In this example, the "author" attribute added in BaseBookModel.init_subclassis not collected by current implementation of ModelBase.new so is not created in migrations and not available as might be expected in subclass Book.

from django.db.models import Model, ForeignKey, CASCADE, CharField

class BaseBookModel(Model):
    class Meta:
        abstract = True

    def __init_subclass__(cls, author_model_cls: Type[Model], **kwargs,):
        super().__init_subclass__(**kwargs)
        author = ForeignKey(author_model_cls, on_delete = CASCADE)
        cls.add_to_class('author', author)

class Author(Model):
    name = CharField(max_len = 256, unique = True)

class Book(BaseBookModel, author_model_cls = Author):
    pass

Thanks for reading and appreciate any thoughts!

Change History (2)

comment:1 by Mariusz Felisiak, 18 months ago

Resolution: invalid
Status: newclosed

Thanks for this ticket, however, it seems that you're rather asking for help in your implementation. Trac is not an appropriate channel for this. You can start a discussion on the DevelopersMailingList or on the Django Forum, where you'll reach a wider audience and see what other think.

comment:2 by hottwaj, 18 months ago

Description: modified (diff)
Resolution: invalid
Status: closednew
Summary: Dynamic model fields without using a metaclassModelBase metaclass implementation prevents addition of model fields via __init_subclass__
Type: New featureBug

Thanks for taking time to review. I guess I did not phrase the issue very well - I am not looking for help implementing something, but rather think there is an issue with the way the ModelBase metaclass interacts with init_subclass which prevents model fields being added dynamically via the latter.

I have re-written the issue to clarify and would appreciate if you could take a second look. Thanks!

Note: See TracTickets for help on using tickets.
Back to Top