This commit is contained in:
sobolevn
2020-11-14 21:18:09 +03:00
parent 517ae648e5
commit 16326e999c
5 changed files with 49 additions and 22 deletions

View File

@@ -36,11 +36,11 @@ repos:
language: system
types: [ python ]
exclude: "scripts/|django_stubs_ext/"
args: [ "--cache-dir=/dev/null", "--no-incremental" ]
args: [ "--config=mypy.ini", "--cache-dir=/dev/null", "--no-incremental" ]
- id: mypy
name: mypy (django_stubs_ext)
entry: mypy
language: system
types: [ python ]
files: "django_stubs_ext/|django_stubs_ext/tests/"
args: [ "--cache-dir=/dev/null", "--no-incremental", "--strict" ]
args: [ "--config=mypy.ini", "--cache-dir=/dev/null", "--no-incremental", "--strict" ]

View File

@@ -58,7 +58,7 @@ We rely on different `django` and `mypy` versions:
No, it is not. We are independent from Django at the moment.
There's a [proposal](https://github.com/django/deps/pull/65) to merge our project into the Django itself.
You show your support by linking the PR.
You can show your support by liking the PR.
### Is it safe to use this in production?
@@ -82,12 +82,24 @@ when you will try to use `QuerySet[MyModel]`, `Manager[MyModel]` or some other D
This happens because these Django classes do not support [`__class_getitem__`](https://www.python.org/dev/peps/pep-0560/#class-getitem) magic method in runtime.
1. You can go with our
1. You can go with our [`django_stubs_ext`](https://github.com/typeddjango/django-stubs/tree/master/django_stubs_ext) helper, that patches all the types we use as Generic in django.
Install it:
```bash
pip install django-stubs-ext # as a production dependency
```
And then place in your `manage.py`, `wsgi.py`, and `asgi.py` files:
```python
import django_stubs_ext
django_stubs_ext.monkeypath()
```
2. You can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel]'`, this way it will work as a type for `mypy` and as a regular `str` in runtime.
Currently we [are working](https://github.com/django/django/pull/12405) on providing `__class_getitem__` to the classes where we need them.
### How can I create a HttpRequest that's guaranteed to have an authenticated user?
Django's built in `HttpRequest` has the attribute `user` that resolves to the type

View File

@@ -1,3 +1,3 @@
from .monkeypatch import monkeypatch
from .patch import monkeypatch as monkeypatch
__all__ = ["monkeypatch"]

View File

@@ -1,6 +1,6 @@
from typing import Any, Generic, List, Optional, Tuple, Type, TypeVar
import django
from django import VERSION as VERSION
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import BaseModelAdmin
from django.db.models.manager import BaseManager
@@ -21,9 +21,6 @@ class MPGeneric(Generic[_T]):
possible issues we may run into with this method.
"""
version: Optional[int]
cls: Type[_T]
def __init__(self, cls: Type[_T], version: Optional[_VersionSpec] = None):
"""Set the data fields, basic constructor."""
self.version = version
@@ -31,7 +28,7 @@ class MPGeneric(Generic[_T]):
def __repr__(self) -> str:
"""Better representation in tests and debug."""
return "<MPGeneric: {0}, versions={1}>".format(self.cls, self.version or "all")
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
@@ -51,7 +48,7 @@ _need_generic: List[MPGeneric[Any]] = [
def monkeypatch() -> None:
"""Monkey patch django as necessary to work properly with mypy."""
suited_for_this_version = filter(
spec.version is None or django.VERSION[:2] <= spec.version,
lambda spec: spec.version is None or VERSION[:2] <= spec.version,
_need_generic,
)
for el in suited_for_this_version:

View File

@@ -1,25 +1,43 @@
from contextlib import suppress
from typing import Optional
import pytest
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
from typing_extensions import Protocol
import django_stubs_ext
from django_stubs_ext.monkeypatch import _need_generic, _VersionSpec, django
from django_stubs_ext import patch
from django_stubs_ext.patch import _need_generic, _VersionSpec
class _MakeGenericClasses(Protocol):
"""Used to represent a type of ``make_generic_classes`` fixture."""
def __call__(self, django_version: Optional[_VersionSpec] = None) -> None:
...
@pytest.fixture(scope="function")
def make_generic_classes(request, monkeypatch):
def fin():
def make_generic_classes(
request: FixtureRequest,
monkeypatch: MonkeyPatch,
) -> _MakeGenericClasses:
def fin() -> None:
for el in _need_generic:
delattr(el.cls, "__class_getitem__")
with suppress(AttributeError):
delattr(el.cls, "__class_getitem__")
def factory(django_version=None):
def factory(django_version: Optional[_VersionSpec] = None) -> None:
if django_version is not None:
monkeypatch.setattr(django, "VERSION", django_version)
monkeypatch.setattr(patch, "VERSION", django_version)
django_stubs_ext.monkeypatch()
request.addfinalizer(fin)
return factory
def test_patched_generics(make_generic_classes) -> None:
def test_patched_generics(make_generic_classes: _MakeGenericClasses) -> None:
"""Test that the generics actually get patched."""
make_generic_classes()
@@ -39,11 +57,11 @@ def test_patched_generics(make_generic_classes) -> None:
)
def test_patched_version_specific(
django_version: _VersionSpec,
make_generic_classes,
make_generic_classes: _MakeGenericClasses,
) -> None:
"""Test version speicific types."""
make_generic_classes(django_version)
for el in _need_generic:
if el.version is not None and el.version[:2] <= django_version:
if el.version is not None and django_version <= el.version:
assert el.cls[int] is el.cls