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
This commit is contained in:
proxy
2020-11-11 02:04:13 -05:00
committed by GitHub
parent e798b496c0
commit 0c41d0c6e9
18 changed files with 223 additions and 4 deletions

View File

@@ -0,0 +1,49 @@
# Extensions and monkey-patching for django-stubs
[![Build Status](https://travis-ci.com/typeddjango/django-stubs.svg?branch=master)](https://travis-ci.com/typeddjango/django-stubs)
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![Gitter](https://badges.gitter.im/mypy-django/Lobby.svg)](https://gitter.im/mypy-django/Lobby)
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)).
## Installation
```bash
pip install django-stubs-ext
```
## Usage
In your Django application, use the following code:
```py
import django_stubs_ext
django_stubs_ext.monkeypath()
```
This only needs to be called once, so the call to `monkeypatch` should be placed in your top-level urlconf.
## Version compatibility
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:
```py
@classmethod
def __class_getitem__(cls, *args, **kwargs):
return cls
```
## To get help
For help with django-stubs, please view the main repository at <https://github.com/typeddjango/django-stubs>
We have a Gitter chat here: <https://gitter.im/mypy-django/Lobby>
If you think you have a more generic typing issue, please refer to <https://github.com/python/mypy> and their Gitter.
## Contributing
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).
You can always also reach out in gitter to discuss your contributions!

View File

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

View File

@@ -0,0 +1,47 @@
from typing import Any, Generic, List, Optional, Type, TypeVar
import django
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import BaseModelAdmin
from django.views.generic.edit import FormMixin
_T = TypeVar("_T")
class MPGeneric(Generic[_T]):
"""Create a data class to hold metadata about the gneric 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.
"""
version: Optional[int]
cls: Type[_T]
def __init__(self, cls: Type[_T], version: Optional[int] = None):
"""Set the data fields, basic constructor."""
self.version = version
self.cls = cls
# 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(FormMixin),
MPGeneric(BaseModelAdmin),
]
# currently just adds the __class_getitem__ dunder. if more monkeypatching is needed, add it here
def monkeypatch() -> None:
"""Monkey patch django as necessary to work properly with mypy."""
for el in filter(lambda x: django.VERSION[0] == x.version or x.version is None, _need_generic):
el.cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls)
__all__ = ["monkeypatch"]

14
django_stubs_ext/mypy.ini Normal file
View File

@@ -0,0 +1,14 @@
[mypy]
strict_optional = True
ignore_missing_imports = True
check_untyped_defs = True
warn_no_return = False
show_traceback = True
allow_redefinition = True
incremental = True
plugins =
mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = scripts.django_tests_settings

View File

@@ -0,0 +1,8 @@
[tool.black]
line-length = 120
include = '\.pyi?$'
[tool.isort]
line_length = 120
multi_line_output = 3
include_trailing_comma = true

View File

@@ -0,0 +1,8 @@
[pytest]
testpaths =
./tests
addopts =
--tb=native
-s
-v
--cache-clear

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -ex
if [[ -z $(git status -s) ]]
then
if [[ "$VIRTUAL_ENV" != "" ]]
then
pip install --upgrade setuptools wheel twine
python setup.py sdist bdist_wheel
twine upload dist/*
rm -rf dist/ build/
else
echo "this script must be executed inside an active virtual env, aborting"
fi
else
echo "git working tree is not clean, aborting"
fi

View File

@@ -0,0 +1,7 @@
[flake8]
exclude = .*/
select = F401, Y
max_line_length = 120
[metadata]
license_file = ../LICENSE.txt

42
django_stubs_ext/setup.py Normal file
View File

@@ -0,0 +1,42 @@
from distutils.core import setup
from setuptools import find_packages
with open("README.md") as f:
readme = f.read()
dependencies = [
"django",
]
setup(
name="django-stubs-ext",
version="0.1.0",
description="Monkey-patching and extensions for django-stubs",
long_description=readme,
long_description_content_type="text/markdown",
license="MIT",
url="https://github.com/typeddjango/django-stubs",
author="Simula Proxy",
author_email="3nki.nam.shub@gmail.com",
py_modules=[],
python_requires=">=3.6",
install_requires=dependencies,
packages=["django_stubs_ext", *find_packages(exclude=["scripts"])],
classifiers=[
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Typing :: Typed",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Framework :: Django :: 3.1",
],
project_urls={
"Release notes": "https://github.com/typeddjango/django-stubs/releases",
},
)

View File

@@ -0,0 +1,11 @@
import django_stubs_ext
from django_stubs_ext.monkeypatch import _need_generic
django_stubs_ext.monkeypatch()
def test_patched_generics() -> None:
"""Test that the generics actually get patched."""
for el in _need_generic:
# This only throws an exception if the monkeypatch failed
assert el.cls[type] == el.cls # `type` is arbitrary