#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.

Reply via email to