django-stubs icon indicating copy to clipboard operation
django-stubs copied to clipboard

Abstract generic models not supported

Open frnhr opened this issue 4 years ago • 9 comments

Bug report

As the title says, abstract generic models (a.k.a. model mixins with a generic) are not supported with Django and django-stubs.

Well, they are almost supported. Everything works in mypy and at runtime, except when running ./manage.py makemigrations, then things explode.

What's wrong

Here is what I mean by "abstract generic model":


class NukableModel(models.Model, Generic[_T]):
    is_nuked = models.BooleanField(default=False, null=False)
    
    objects = NukableManager['NukableModel[_T]']()  # or any other reason for needing a generic

    class Meta:
        abstract = True

It would be used like so:

class Poll(NukableModel['Poll'], models.Model):
    ...

Class NukableModel has typing.Generic as one of its bases. This causes problems with ./manage.py makemigrations:

...
  File "/path/to/django/django/db/migrations/autodetector.py", line 129, in _detect_changes
    self.new_apps = self.to_state.apps
  File "/path/to/django/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/path/to/django/django/db/migrations/state.py", line 210, in apps
    return StateApps(self.real_apps, self.models)
  File "/path/to/django/django/db/migrations/state.py", line 273, in __init__
    self.render_multiple([*models.values(), *self.real_models])
  File "/path/to/django/django/db/migrations/state.py", line 308, in render_multiple
    model.render(self)
  File "/path/to/django/django/db/migrations/state.py", line 580, in render
    return type(self.name, bases, body)
  File "/path/to/django/django/db/models/base.py", line 98, in __new__
    new_class = super_new(cls, name, bases, new_attrs, **kwargs)
  File "/path/to/.pyenv/versions/3.8.0/lib/python3.8/typing.py", line 905, in __init_subclass__
    raise TypeError("Cannot inherit from plain Generic")
TypeError: Cannot inherit from plain Generic

How is that should be

Well... it should, you know, just work :)

Monkeypatch solution

This is a monkeypatch solution that works for me:


_original = ModelState.render

def _new(self, apps):
    self.bases = tuple(base for base in self.bases if not issubclass(base, Generic))  # type: ignore
    return _original(self, apps)

ModelState.render = _new  # type: ignore

It removes Generic from bases list here. This way, when type(self.name, bases, body) is called at the end of that method, Genericdoesn't explode.

Would a fix like that be a thing for django-stubs?

BTW, here is a quick implementation of NukeableModel in full: https://github.com/frnhr/django-polls/blob/generic_abstract_models/polls/nukable_model.py

System information

  • OS: macOS 10.14.6
  • python version: 3.8.0
  • django version: 3.0.3
  • mypy version: 0.761
  • django-stubs version: 1.4.0

frnhr avatar Jan 15 '20 02:01 frnhr

Have the same problem too ! :/

Monkey patch works for me, make sure to import ModelState from migrations. You can put this for example in your settings.py.

from django.db.migrations.state import ModelState

felixmeziere avatar Nov 18 '20 11:11 felixmeziere

There seem to have been some changes in Django where bases in self.bases may also be strings. This works for me:

from django.db.migrations.state import ModelState

original_render = ModelState.render

def patched_render(self, apps):
    self.bases = tuple(
        item
        for item in self.bases
        if isinstance(item, str) or not issubclass(item, Generic)  # type: ignore
    )
    return original_render(self, apps)

ModelState.render = patched_render  # type: ignore

yrd avatar Dec 15 '21 12:12 yrd

Hi @yrd , in what version of Django ? I don't have any issues so far in Django 3.2.10 but this worries me! Should I use your code snippet?

felixmeziere avatar Dec 15 '21 12:12 felixmeziere

I'm using 3.2.9. The original method also seems to test for strings: https://github.com/django/django/blob/ac5cc6cf01463d90aa333d5f6f046c311019827b/django/db/migrations/state.py#L858-L861

According to git, the linked code segment is a few years old, though. So it seems there always has been the possibility of getting a string. I don't know my way around the Django internals enough to tell you when the base is a string and when it isn't, but for me it happened first try ;)

yrd avatar Dec 15 '21 12:12 yrd

An educated guess after some quick debugging: it seems to me that the base is provided as a string when you have a superclass that isn't an abstract model. At least in my codebase that seems to be the case... But again, not sure on this.

yrd avatar Dec 15 '21 12:12 yrd

thank you!

felixmeziere avatar Dec 15 '21 18:12 felixmeziere

This seems like a Django bug to me - have you reported it to them?

RJPercival avatar Mar 03 '22 14:03 RJPercival

I've reported this to Django, as I couldn't find anything similar in their bug tracker.

RJPercival avatar Mar 03 '22 15:03 RJPercival

Django don't intend to fix this: https://code.djangoproject.com/ticket/33174

RJPercival avatar Mar 04 '22 14:03 RJPercival