pylint-django
pylint-django copied to clipboard
Mypy and unsubscriptable-object
When using mypy with --strict, it requires you to fill in the TypeVar for whichever generic type you're using. For example:
from django.contrib import admin
from .models import Example
@admin.register(Example)
class ExampleAdmin(admin.ModelAdmin[Example]):
...
This raises: E1136: Value 'admin.ModelAdmin' is unsubscriptable (unsubscriptable-object) .
It's not happening for all types though. For example, DRF serializers, seem to be just fine. These are the ones that are raising the error on my project: admin.ModelAdmin, forms.ModelForm and m.PositiveSmallIntegerField .
To be honest, I'm not sure where this issue fits. Here? django-stubs? pylint? I thought this was a good place to start. If you feel otherwise, feel free to close the issue and I'll open another in somewhere more relevant.
I felt like I should open the issue as I didn't find a single instance of someone having the exact same problem.
See also: https://github.com/PyCQA/pylint/issues/3882
I'm using the latest version for Django (and respective stubs), DRF (and respective stubs), pylint, mypy and pylint-django.
And thank you!
Maybe related: https://github.com/PyCQA/pylint/pull/6536
I ran into the same problem, and wrote a small pylint-plugin to mitigate the issue. See the code below.
This is my first time ever dipping into pylint plugins, so any input on how to improve the code is greatly appreciated. I can extend and modify the code to create a pull request later on; but I'd appreciate input by the devs before I put in the effort:
- Since this solves an issue that only occurs when using pylint and mypy together, are you generally willing to extend the pylint-django plugin with code such as this?
- I tried to pick a set of common classes that introduced typevars in django and DRF. However, I probably missed some. Could you give me some pointers on how to create a separate pylint config entry for additional classes? I was not able to figure out how to do this for transform plugins.
Here's the code:
"""
pylint plugin to remove subscription type annotations required for django-stubs.
django-stubs, when ran in mypy --strict, requires annotation types like CreateView
or DetailView with the used model class, e.g. CreateView[myapp.models.MyModel].
These annotation are monkeypatched to work with django-stubs-ext, but not
understood by pylint.
This plugin removes the annotations from the AST.
"""
from typing import TYPE_CHECKING
import astroid
if TYPE_CHECKING:
from pylint.lint import PyLinter
_DJANGO_STUBS_MONKEYPATCH_LIST = (
"ModelAdmin",
"SingleObjectMixin",
"FormMixin",
"DeletionMixin",
"MultipleObjectMixin",
"BaseModelAdmin",
"Field",
"Paginator",
"BaseFormSet",
"BaseModelForm",
"BaseModelFormSet",
"Feed",
"Sitemap",
"FileProxyMixin",
"Lookup",
"BaseConnectionHandler",
"QuerySet",
"BaseManager",
"ForeignKey",
"ModelFormMixin",
"ModelForm",
"FormView",
"DetailView",
"ListView",
"UpdateView",
"CreateView",
)
_DRF_STUBS_MONKEYPATCH_LIST = (
"ListModelMixin",
"CreateModelMixin",
"RetrieveModelMixin",
"UpdateModelMixin",
"DestroyModelMixin",
"CreateAPIView",
"ListAPIView",
"RetrieveAPIView",
"DestroyAPIView",
"UpdateAPIView",
"ListCreateAPIView",
"RetrieveUpdateAPIView",
"RetrieveDestroyAPIView",
"RetrieveUpdateDestroyAPIView",
"ModelViewSet",
)
def register(linter: "PyLinter") -> None:
"""This required method auto registers the checker during initialization."""
pass
def transform(cls) -> None:
"""Remove the subscription annotation for classes monkeypatched by django-stubs-ext"""
new_bases = []
for base in cls.bases:
if (
type(base) == astroid.Subscript
and type(base.value) == astroid.Attribute
and (
base.value.attrname in _DJANGO_STUBS_MONKEYPATCH_LIST
or base.value.attrname in _DRF_STUBS_MONKEYPATCH_LIST
)
):
new_bases.append(base.value)
else:
new_bases.append(base)
if new_bases:
cls.bases = new_bases
astroid.MANAGER.register_transform(astroid.ClassDef, transform)