#34555: ModelBase metaclass implementation prevents addition of model fields via
__init_subclass__
-------------------------------------+-------------------------------------
Reporter: hottwaj | Owner: nobody
Type: Bug | Status: new
Component: Database layer | Version: 4.2
(models, ORM) |
Severity: Normal | Resolution:
Keywords: ModelBase | Triage Stage:
init_subclass | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by hottwaj):
* status: closed => new
* type: New feature => Bug
* resolution: invalid =>
Old description:
> I'd like to be able to write some abstract model "templates" that can be
> re-used/customised in various other django apps. Some of these templates
> require ForeignKeys to other "templates" which are also abstract.
> Obviously ForeignKeys to Abstract models are not possible
>
> Having used metaclasses in a similar situation before, but hoping to find
> a less complex approach, I thought I might be able to implement this by
> providing a __init_subclass__ method on my "template" model, with that
> method designed to set up the ForeignKey to a given concrete model, but
> this does not work. It seems that __init_subclass__ is called after
> ModelBase.__new__ collects fields that require "contribute_to_class", so
> the fields I add in __init_subclass__ are never "seen" by e.g. the
> migration builder.
>
> Example approach below (but note this does not work for reasons explained
> above):
>
> {{{#!python
> 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
> }}}
>
> Essentially what I'd like is some way of doing some extra work after
> BaseModel.__new__ is called without resorting to having to write a
> metaclass for BaseBookModel - I'm just adding some extra fields and a
> metaclass is complex for most mortals to read let alone write. There
> does not seem to be an appropriate hook method that I can override to do
> this for every subclass of BaseBookModel
>
> Thanks for reading and appreciate any thoughts!
New description:
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_subclass__is not collected by current implementation
of ModelBase.__new__ so is not created in migrations and not available as
might be expected in subclass Book.
{{{#!python
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!
--
Comment:
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!
--
Ticket URL: <https://code.djangoproject.com/ticket/34555#comment:2>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/01070188076810a1-135e905f-febd-4d5b-a178-5d4ef74d7361-000000%40eu-central-1.amazonses.com.