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

Mypy and unsubscriptable-object

Open XF-FW opened this issue 3 years ago • 2 comments

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!

XF-FW avatar Mar 30 '22 16:03 XF-FW

Maybe related: https://github.com/PyCQA/pylint/pull/6536

seyeong avatar Jun 26 '22 14:06 seyeong

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)

bernhardmiller avatar Feb 06 '23 11:02 bernhardmiller