Files
django-stubs/django_stubs_ext/django_stubs_ext/patch.py
Nick Pope e8d52fe7da Monkeypatch some more generics. (#1050)
* Add generic monkeypatching for `FileProxyMixin`.

This fixes `TypeError: 'type' object is not subscriptable` for
`django.core.files.File` and `django.core.files.base.ContentFile`.

* Add generic monkeypatching for `ForeignKey`.

This matches the change coming in Django 4.1.

See https://github.com/django/django/pull/15571
2022-07-06 01:19:51 +03:00

84 lines
3.1 KiB
Python

from typing import Any, Generic, Iterable, List, Optional, Tuple, Type, TypeVar
from django import VERSION as VERSION
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import BaseModelAdmin
from django.contrib.syndication.views import Feed
from django.core.files.utils import FileProxyMixin
from django.core.paginator import Paginator
from django.db.models.fields import Field
from django.db.models.fields.related import ForeignKey
from django.db.models.manager import BaseManager
from django.db.models.query import QuerySet
from django.forms.formsets import BaseFormSet
from django.forms.models import BaseModelForm, BaseModelFormSet
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import DeletionMixin, FormMixin
from django.views.generic.list import MultipleObjectMixin
_T = TypeVar("_T")
_VersionSpec = Tuple[int, int]
class MPGeneric(Generic[_T]):
"""Create a data class to hold metadata about the generic classes needing monkeypatching.
The `version` param is optional, and a value of `None` means that the monkeypatch is
version-independent.
This is slightly overkill for our purposes, but useful for future-proofing against any
possible issues we may run into with this method.
"""
def __init__(self, cls: Type[_T], version: Optional[_VersionSpec] = None):
"""Set the data fields, basic constructor."""
self.version = version
self.cls = cls
def __repr__(self) -> str:
"""Better representation in tests and debug."""
return "<MPGeneric: {}, versions={}>".format(self.cls, self.version or "all")
# certain django classes need to be generic, but lack the __class_getitem__ dunder needed to
# annotate them: https://github.com/typeddjango/django-stubs/issues/507
# this list stores them so `monkeypatch` can fix them when called
_need_generic: List[MPGeneric[Any]] = [
MPGeneric(ModelAdmin),
MPGeneric(SingleObjectMixin),
MPGeneric(FormMixin),
MPGeneric(DeletionMixin),
MPGeneric(MultipleObjectMixin),
MPGeneric(BaseModelAdmin),
MPGeneric(Field),
MPGeneric(Paginator),
MPGeneric(BaseFormSet),
MPGeneric(BaseModelForm),
MPGeneric(BaseModelFormSet),
MPGeneric(Feed),
MPGeneric(FileProxyMixin),
# These types do have native `__class_getitem__` method since django 3.1:
MPGeneric(QuerySet, (3, 1)),
MPGeneric(BaseManager, (3, 1)),
# These types do have native `__class_getitem__` method since django 4.1:
MPGeneric(ForeignKey, (4, 1)),
]
def monkeypatch(extra_classes: Optional[Iterable[type]] = None) -> None:
"""Monkey patch django as necessary to work properly with mypy."""
# Add the __class_getitem__ dunder.
suited_for_this_version = filter(
lambda spec: spec.version is None or VERSION[:2] <= spec.version,
_need_generic,
)
for el in suited_for_this_version:
el.cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls)
if extra_classes:
for cls in extra_classes:
cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls) # type: ignore[attr-defined]
__all__ = ["monkeypatch"]