Skip to content

Commit 0c41d0c

Browse files
author
proxy
authored
create monkeypatching function for adding get_item dunder (#526)
* run black * create monkeypatching function for adding get_item dunder * whoops i forgot the test * change the name in INSTALLED_APPS to make test pass * turn the whole thing into a proper package * move django_stubs_ext to requirements.txt * also install requirements.txt * attempt to fix pre-commit * numerous small code review fixes * fix dependency issues * small dependency fixes * configure proper license file location * add the rest of the monkeypatching * use strict mypy * update contributing with a note monkeypatching generics * copy release script from parent package
1 parent e798b49 commit 0c41d0c

File tree

18 files changed

+223
-4
lines changed

18 files changed

+223
-4
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,12 @@ repos:
3535
entry: mypy
3636
language: system
3737
types: [ python ]
38-
exclude: "scripts/*"
38+
exclude: "scripts/|django_stubs_ext/"
3939
args: [ "--cache-dir=/dev/null", "--no-incremental" ]
40+
- id: mypy
41+
name: mypy (django_stubs_ext)
42+
entry: mypy
43+
language: system
44+
types: [ python ]
45+
files: "django_stubs_ext/|django_stubs_ext/tests/"
46+
args: [ "--cache-dir=/dev/null", "--no-incremental", "--strict" ]

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,9 @@ The workflow for contributions is fairly simple:
104104
3. make whatever changes you want to contribute.
105105
4. ensure your contribution does not introduce linting issues or breaks the tests by linting and testing the code.
106106
5. make a pull request with an adequate description.
107+
108+
## A Note About Generics
109+
110+
As Django uses a lot of the more dynamic features of Python (i.e. metaobjects), statically typing it requires heavy use of generics. Unfortunately, the syntax for generics is also valid python syntax. For instance, the statement `class SomeClass(SuperType[int])` implicitly translates to `class SomeClass(SuperType.__class_getitem__(int))`. If `SuperType` doesn't define the `__class_getitem__` method, this causes a runtime error, even if the code typechecks.
111+
112+
When adding a new generic class, or changing an existing class to use generics, run a quick test to see if it causes a runtime error. If it does, please add the new generic class to the `_need_generic` list in the [django_stubs_ext monkeypatch function](https://github.com/typeddjango/django-stubs/tree/master/django_stubs_ext/django_stubs_ext/monkeypatch.py)

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ pre-commit==2.7.1
88
pytest==6.1.1
99
pytest-mypy-plugins==1.6.1
1010
psycopg2-binary
11+
-e ./django_stubs_ext
1112
-e .

django-stubs/contrib/auth/tokens.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,4 @@ class PasswordResetTokenGenerator:
1313
def _num_days(self, dt: date) -> float: ...
1414
def _today(self) -> date: ...
1515

16-
1716
default_token_generator: Any

django-stubs/contrib/humanize/templatetags/humanize.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,5 @@ class NaturalTimeFormatter:
1818
time_strings: Dict[str, str]
1919
past_substrings: Dict[str, str]
2020
future_substrings: Dict[str, str]
21-
2221
@classmethod
2322
def string_for(cls: Type[NaturalTimeFormatter], value: Any) -> Any: ...

django-stubs/utils/translation/__init__.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ def ngettext_lazy(singular: str, plural: str, number: Union[int, str, None]) ->
4848
ungettext_lazy = ngettext_lazy
4949

5050
def npgettext_lazy(context: str, singular: str, plural: str, number: Union[int, str, None]) -> str: ...
51-
5251
def activate(language: str) -> None: ...
5352
def deactivate() -> None: ...
5453

django_stubs_ext/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Extensions and monkey-patching for django-stubs
2+
3+
[![Build Status](https://travis-ci.com/typeddjango/django-stubs.svg?branch=master)](https://travis-ci.com/typeddjango/django-stubs)
4+
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
5+
[![Gitter](https://badges.gitter.im/mypy-django/Lobby.svg)](https://gitter.im/mypy-django/Lobby)
6+
7+
8+
This package contains extensions and monkey-patching functions for the [django-stubs](https://github.com/typeddjango/django-stubs) package. Certain features of django-stubs (i.e. generic django classes that don't define the `__class_getitem__` method) require runtime monkey-patching, which can't be done with type stubs. These extensions were split into a separate package so library consumers don't need `mypy` as a runtime dependency ([#526](https://github.com/typeddjango/django-stubs/pull/526#pullrequestreview-525798031)).
9+
10+
## Installation
11+
12+
```bash
13+
pip install django-stubs-ext
14+
```
15+
16+
## Usage
17+
18+
In your Django application, use the following code:
19+
20+
```py
21+
import django_stubs_ext
22+
23+
django_stubs_ext.monkeypath()
24+
```
25+
26+
This only needs to be called once, so the call to `monkeypatch` should be placed in your top-level urlconf.
27+
28+
## Version compatibility
29+
30+
Since django-stubs supports multiple Django versions, this package takes care to only monkey-patch the features needed by your django version, and decides which features to patch at runtime. This is completely safe, as (currently) we only add a `__class_getitem__` method that does nothing:
31+
32+
```py
33+
@classmethod
34+
def __class_getitem__(cls, *args, **kwargs):
35+
return cls
36+
```
37+
38+
## To get help
39+
40+
For help with django-stubs, please view the main repository at <https://github.com/typeddjango/django-stubs>
41+
42+
We have a Gitter chat here: <https://gitter.im/mypy-django/Lobby>
43+
If you think you have a more generic typing issue, please refer to <https://github.com/python/mypy> and their Gitter.
44+
45+
## Contributing
46+
47+
The django-stubs-ext package is part of the [django-stubs](https://github.com/typeddjango/django-stubs) monorepo. If you would like to contribute, please view the django-stubs [contribution guide](https://github.com/typeddjango/django-stubs/blob/master/CONTRIBUTING.md).
48+
49+
You can always also reach out in gitter to discuss your contributions!
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .monkeypatch import monkeypatch
2+
3+
__all__ = ["monkeypatch"]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Any, Generic, List, Optional, Type, TypeVar
2+
3+
import django
4+
from django.contrib.admin import ModelAdmin
5+
from django.contrib.admin.options import BaseModelAdmin
6+
from django.views.generic.edit import FormMixin
7+
8+
_T = TypeVar("_T")
9+
10+
11+
class MPGeneric(Generic[_T]):
12+
"""Create a data class to hold metadata about the gneric classes needing monkeypatching.
13+
14+
The `version` param is optional, and a value of `None` means that the monkeypatch is
15+
version-independent.
16+
17+
This is slightly overkill for our purposes, but useful for future-proofing against any
18+
possible issues we may run into with this method.
19+
"""
20+
21+
version: Optional[int]
22+
cls: Type[_T]
23+
24+
def __init__(self, cls: Type[_T], version: Optional[int] = None):
25+
"""Set the data fields, basic constructor."""
26+
self.version = version
27+
self.cls = cls
28+
29+
30+
# certain django classes need to be generic, but lack the __class_getitem__ dunder needed to
31+
# annotate them: https://github.com/typeddjango/django-stubs/issues/507
32+
# this list stores them so `monkeypatch` can fix them when called
33+
_need_generic: List[MPGeneric[Any]] = [
34+
MPGeneric(ModelAdmin),
35+
MPGeneric(FormMixin),
36+
MPGeneric(BaseModelAdmin),
37+
]
38+
39+
40+
# currently just adds the __class_getitem__ dunder. if more monkeypatching is needed, add it here
41+
def monkeypatch() -> None:
42+
"""Monkey patch django as necessary to work properly with mypy."""
43+
for el in filter(lambda x: django.VERSION[0] == x.version or x.version is None, _need_generic):
44+
el.cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls)
45+
46+
47+
__all__ = ["monkeypatch"]

django_stubs_ext/mypy.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[mypy]
2+
strict_optional = True
3+
ignore_missing_imports = True
4+
check_untyped_defs = True
5+
warn_no_return = False
6+
show_traceback = True
7+
allow_redefinition = True
8+
incremental = True
9+
10+
plugins =
11+
mypy_django_plugin.main
12+
13+
[mypy.plugins.django-stubs]
14+
django_settings_module = scripts.django_tests_settings

django_stubs_ext/pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[tool.black]
2+
line-length = 120
3+
include = '\.pyi?$'
4+
5+
[tool.isort]
6+
line_length = 120
7+
multi_line_output = 3
8+
include_trailing_comma = true

django_stubs_ext/pytest.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[pytest]
2+
testpaths =
3+
./tests
4+
addopts =
5+
--tb=native
6+
-s
7+
-v
8+
--cache-clear

django_stubs_ext/release.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
set -ex
3+
4+
if [[ -z $(git status -s) ]]
5+
then
6+
if [[ "$VIRTUAL_ENV" != "" ]]
7+
then
8+
pip install --upgrade setuptools wheel twine
9+
python setup.py sdist bdist_wheel
10+
twine upload dist/*
11+
rm -rf dist/ build/
12+
else
13+
echo "this script must be executed inside an active virtual env, aborting"
14+
fi
15+
else
16+
echo "git working tree is not clean, aborting"
17+
fi

django_stubs_ext/setup.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[flake8]
2+
exclude = .*/
3+
select = F401, Y
4+
max_line_length = 120
5+
6+
[metadata]
7+
license_file = ../LICENSE.txt

django_stubs_ext/setup.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from distutils.core import setup
2+
3+
from setuptools import find_packages
4+
5+
with open("README.md") as f:
6+
readme = f.read()
7+
8+
dependencies = [
9+
"django",
10+
]
11+
12+
setup(
13+
name="django-stubs-ext",
14+
version="0.1.0",
15+
description="Monkey-patching and extensions for django-stubs",
16+
long_description=readme,
17+
long_description_content_type="text/markdown",
18+
license="MIT",
19+
url="https://github.com/typeddjango/django-stubs",
20+
author="Simula Proxy",
21+
author_email="[email protected]",
22+
py_modules=[],
23+
python_requires=">=3.6",
24+
install_requires=dependencies,
25+
packages=["django_stubs_ext", *find_packages(exclude=["scripts"])],
26+
classifiers=[
27+
"License :: OSI Approved :: MIT License",
28+
"Operating System :: OS Independent",
29+
"Programming Language :: Python :: 3.6",
30+
"Programming Language :: Python :: 3.7",
31+
"Programming Language :: Python :: 3.8",
32+
"Programming Language :: Python :: 3.9",
33+
"Typing :: Typed",
34+
"Framework :: Django",
35+
"Framework :: Django :: 2.2",
36+
"Framework :: Django :: 3.0",
37+
"Framework :: Django :: 3.1",
38+
],
39+
project_urls={
40+
"Release notes": "https://github.com/typeddjango/django-stubs/releases",
41+
},
42+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import django_stubs_ext
2+
from django_stubs_ext.monkeypatch import _need_generic
3+
4+
django_stubs_ext.monkeypatch()
5+
6+
7+
def test_patched_generics() -> None:
8+
"""Test that the generics actually get patched."""
9+
for el in _need_generic:
10+
# This only throws an exception if the monkeypatch failed
11+
assert el.cls[type] == el.cls # `type` is arbitrary

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[pytest]
22
testpaths =
33
./tests
4+
./django_stubs_ext/tests
45
addopts =
56
--tb=native
67
-s

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def find_stub_files(name: str) -> List[str]:
2424
"mypy>=0.790",
2525
"typing-extensions",
2626
"django",
27+
"django-stubs-ext",
2728
]
2829

2930
setup(

0 commit comments

Comments
 (0)