django-stubs
django-stubs copied to clipboard
Abstract generic models not supported
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, Generic
doesn'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
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
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
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?
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 ;)
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.
thank you!
This seems like a Django bug to me - have you reported it to them?
I've reported this to Django, as I couldn't find anything similar in their bug tracker.
Django don't intend to fix this: https://code.djangoproject.com/ticket/33174