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 language: system
types: [ python ] types: [ python ]
exclude: "scripts/|django_stubs_ext/" exclude: "scripts/|django_stubs_ext/"
args: [ "--cache-dir=/dev/null", "--no-incremental" ] args: [ "--config=mypy.ini", "--cache-dir=/dev/null", "--no-incremental" ]
- id: mypy - id: mypy
name: mypy (django_stubs_ext) name: mypy (django_stubs_ext)
entry: mypy entry: mypy
language: system language: system
types: [ python ] types: [ python ]
files: "django_stubs_ext/|django_stubs_ext/tests/" 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. 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. 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? ### 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. 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. 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? ### 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 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"] __all__ = ["monkeypatch"]

View File

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

View File

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