updated package setup (#485)

* updated package setup

* updated to use python 3.9

* fixed test runner

* fixed typecheck tests

* fixed discrepencies

* added override to runner

* updated travis

* updated pre-commit hooks

* updated dep
This commit is contained in:
Na'aman Hirschfeld
2020-10-29 09:59:48 +01:00
committed by GitHub
parent a3624dec36
commit 44151c485d
74 changed files with 1141 additions and 1446 deletions

16
.gitignore vendored
View File

@@ -1,15 +1,11 @@
*.egg-info
__pycache__/
out/
/test_sqlite.py
/django
.DS_Store
.idea/
.mypy_cache/
build/
dist/
pip-wheel-metadata/
.pytest_cache/
/.envrc
/.direnv
django-sources/
.venv/
__pycache__/
django-source/
out/
pip-wheel-metadata/
stubgen/

39
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,39 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_language_version:
python: python3.9
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: check-yaml
- id: trailing-whitespace
- id: check-executables-have-shebangs
- id: debug-statements
- id: check-merge-conflict
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.3
hooks:
- id: pyupgrade
args: ["--py36-plus"]
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.6.4
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
types: [ python ]
exclude: "scripts/*"
args: [ "--cache-dir=/dev/null", "--no-incremental" ]

View File

@@ -2,55 +2,51 @@ language: python
cache: pip
dist: xenial
sudo: required
jobs:
include:
- name: Run plugin test suite with python 3.8
python: 3.8
script: 'pytest'
- name: Run plugin test suite with python 3.7
python: 3.7
script: 'pytest'
- name: Typecheck Django 3.0 test suite with python 3.9-dev
python: 3.9-dev
script: |
python ./scripts/typecheck_tests.py --django_version=3.0
- name: Typecheck Django 3.0 test suite with python 3.8
python: 3.8
script: |
python ./scripts/typecheck_tests.py --django_version=3.0
- name: Typecheck Django 3.0 test suite with python 3.7
python: 3.7
script: |
python ./scripts/typecheck_tests.py --django_version=3.0
- name: Typecheck Django 3.0 test suite with python 3.6
python: 3.6
script: |
python ./scripts/typecheck_tests.py --django_version=3.0
- name: Typecheck Django 2.2 test suite with python 3.9-dev
python: 3.9-dev
script: |
python ./scripts/typecheck_tests.py --django_version=2.2
- name: Typecheck Django 2.2 test suite with python 3.8
python: 3.8
script: |
python ./scripts/typecheck_tests.py --django_version=2.2
- name: Typecheck Django 2.2 test suite with python 3.7
python: 3.7
script: |
python ./scripts/typecheck_tests.py --django_version=2.2
- name: Typecheck Django 2.2 test suite with python 3.6
python: 3.6
script: |
python ./scripts/typecheck_tests.py --django_version=2.2
- name: Mypy for plugin code
python: 3.7
script: 'mypy ./mypy_django_plugin'
- name: Lint using pre-commit
python: 3.9-dev
script:
- pre-commit install && pre-commit run --all-files
- name: Lint with black
python: 3.7
script: 'black --check django-stubs/ setup.py'
- name: Lint plugin code with flake8
python: 3.7
script: 'flake8'
- name: Lint stubs with flake8-pyi and check for unused imports
python: 3.7
script: 'flake8 --config flake8-pyi.ini'
- name: Lint plugin code with isort
python: 3.7
script: 'isort --check --diff'
- name: Run plugin test suite with python 3.9-dev
python: 3.9-dev
script: pytest
before_install: |
sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable -y

View File

@@ -1,111 +1,106 @@
# How to contribute
# Contribution Guide
This project is open source and community driven. As such we encourage code contributions of all kinds. Some areas you can contribute in:
1. Improve the stubs
2. Sync stubs with the latest version of Django
3. Improve plugin code and extend its capabilities
4. Write tests
5. Update dependencies
## Tutorials
If you want to start working on this project,
you will need to get familiar with these projects:
If you want to start working on this project, you will need to get familiar with python typings.
The Mypy documentation offers an excellent resource for this, as well as the python official documentation:
- [Mypy typing documentation](https://mypy.readthedocs.io/en/stable/#overview-type-system-reference)
- [Python official typing documentation](https://docs.python.org/3/library/typing.html)
- [Typing in Python](https://inventwithpython.com/blog/2019/11/24/type-hints-for-busy-python-programmers/) article
Additionally, the following resources might be useful:
- [Django docs](https://docs.djangoproject.com/en/dev/)
- [Typing in Python](https://inventwithpython.com/blog/2019/11/24/type-hints-for-busy-python-programmers/)
- [How to write custom mypy plugins](https://mypy.readthedocs.io/en/stable/extending_mypy.html)
- [Typechecking Django and DRF](https://sobolevn.me/2019/08/typechecking-django-and-drf) guide
- [Testing mypy stubs, plugins, and types](https://sobolevn.me/2019/08/testing-mypy-types) guide
- [Awesome Python Typing](https://github.com/typeddjango/awesome-python-typing) list
It is also recommended to take a look at these resources:
## Dev setup
- [Awesome Python Typing](https://github.com/typeddjango/awesome-python-typing)
### Repository Setup
As a first step you will need to fork this repository and clone your fork locally.
In order to be able to continously sync your fork with the origin repository's master branch, you will need to set up an upstream master. To do so follow this [official github guide](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/syncing-a-fork).
## Dev documentation
### Dependency Setup
TODO
After your repository is setup you will then need to create and activate a git ignored virtual env, e.g.:
```bash
python3 -m venv .venv
source .venv/bin/activate
```
## Dependencies
We use `pip` to manage the dependencies.
To install them you would need to activate your `virtualenv` and run `install` command:
Then install the dev requirements:
```bash
pip install -r ./dev-requirements.txt
```
## Tests and linters
We use `mypy`, `pytest`, `flake8`, and `black` for quality control.
Here's [how we run our CI](https://github.com/typeddjango/django-stubs/blob/master/.travis.yml).
### Typechecking
To run typechecking use:
Finally, install the pre-commit hooks:
```bash
mypy ./mypy_django_plugin
pre-commit install
```
### Testing
### Testing and Linting
There are unit tests and type-related tests.
We use `mypy`, `pytest`, `flake8`, and `black` for quality control. All tools except pytest are executed using pre-commit when you make a commit.
To ensure there are not formatting or typing issues in the entire repository you can run:
To run unit tests:
```bash
pre-commit run --all-files
```
NOTE: This command will not only lint but also modify files - so make sure to commit whatever changes you've made before hand.
You can also run pre-commit per file or for a specific path, simply replace "--all-files" with a target (see [this guide](https://codeburst.io/tool-your-django-project-pre-commit-hooks-e1799d84551f) for more info).
To execute the unit tests, simply run:
```bash
pytest
```
Type-related tests ensure that different Django versions do work correctly.
To run type-related tests:
We also test the stubs against the Django's own test suite. This is done in CI but you can also do this locally.
To execute the script run:
```bash
python ./scripts/typecheck_tests.py --django_version=2.2
python ./scripts/typecheck_tests.py --django_version=3.0
```
Currently we only support two Django versions.
### Linting
To run auto-formatting:
```bash
isort -rc .
black django-stubs/
```
To run linting:
```bash
flake8
flake8 --config flake8-pyi.ini
python ./scripts/typecheck_tests.py --django_version 3.0
```
## Submitting your code
### Generating Stubs using Stubgen
We use [trunk based](https://trunkbaseddevelopment.com/)
development (we also sometimes call it `wemake-git-flow`).
The stubs are based on auto-generated code created by Mypy's stubgen tool (see: [the stubgen docs](https://mypy.readthedocs.io/en/stable/stubgen.html)).
To make life easier we have a helper script that auto generates these stubs. To use it you can run:
What the point of this method?
```bash
python ./scripts/stubgen-django.py --django_version 3.1
```
1. We use protected `master` branch,
so the only way to push your code is via pull request
2. We use issue branches: to implement a new feature or to fix a bug
create a new branch named `issue-$TASKNUMBER`
3. Then create a pull request to `master` branch
4. We use `git tag`s to make releases, so we can track what has changed
since the latest release
You can also pass an optional commit hash as a second kwarg to checkout a specific commit, e.g.
So, this way we achieve an easy and scalable development process
which frees us from merging hell and long-living branches.
```bash
python ./scripts/stubgen-django.py --django_version 3.1 --commit_sha <commit_sha>
```
In this method, the latest version of the app is always in the `master` branch.
The output for this is a gitignored folder called "stubgen" in the repo's root.
## Submission Guidelines
## Other help
The workflow for contributions is fairly simple:
You can contribute by spreading a word about this library.
It would also be a huge contribution to write
a short article on how you are using this project.
You can also share your best practices with us.
1. fork and setup the repository as in the previous step.
2. create a local branch.
3. make whatever changes you want to contribute.
4. ensure your contribution does not introduce linting issues or breaks the tests by linting and testing the code.
5. make a pull request with an adequate description.

View File

@@ -1,11 +1,12 @@
<img src="http://mypy-lang.org/static/mypy_light.svg" alt="mypy logo" width="300px"/>
# Typesafe Django Framework
# pep484 stubs for Django
[![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 [type stubs](https://www.python.org/dev/peps/pep-0561/) and a custom mypy plugin to provide more precise static types and type inference for Django framework. Django uses some Python "magic" that makes having precise types for some code patterns problematic. This is why we need this project. The final goal is to be able to get precise types for most common patterns.
@@ -15,12 +16,7 @@ This package contains [type stubs](https://www.python.org/dev/peps/pep-0561/) an
pip install django-stubs
```
See [Configuration](#configuration) section to get started.
## Configuration
To make `mypy` happy, you will need to add:
To make mypy aware of the plugin, you need to add
```ini
[mypy]
@@ -40,7 +36,6 @@ Two things happeining here:
This fully working [typed boilerplate](https://github.com/wemake-services/wemake-django-template) can serve you as an example.
## Version compatibility
We rely on different `django` and `mypy` versions:
@@ -73,11 +68,11 @@ But, it does not make any sense to use this project without `mypy`.
### mypy crashes when I run it with this plugin installed
The current implementation uses Django runtime to extract models information, so it will crash, if your installed apps or `models.py` is not correct. For this same reason, you cannot use `reveal_type` inside global scope of any Python file that will be executed for `django.setup()`.
Current implementation uses Django runtime to extract models information, so it will crash, if your installed apps or `models.py` is not correct. For this same reason, you cannot use `reveal_type` inside global scope of any Python file that will be executed for `django.setup()`.
In other words, if your `manage.py runserver` crashes, mypy will crash too.
You can also run `mypy` with the [`--tb`](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-show-traceback)
option to get extra information about errors.
You can also run `mypy` with [`--tb`](https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-show-traceback)
option to get extra information about the error.
### I cannot use QuerySet or Manager with type annotations
@@ -122,5 +117,14 @@ And then use `AuthenticatedHttpRequest` instead of the standard `HttpRequest` fo
## To get help
We have Gitter here: <https://gitter.im/mypy-django/Lobby>
If you think you have more generic typing issue, please refer to <https://github.com/python/mypy> and their Gitter.
If you think you have a more generic typing issue, please refer to <https://github.com/python/mypy> and their Gitter.
## Contributing
This project is open source and community driven. As such we encourage contributions big and small. You can contribute by doing any of the following:
1. Contribute code (e.g. improve stubs, add plugin capabilities, write tests etc) - to do so please follow the [contribution guide](./CONTRIBUTING.md).
2. Assist in code reviews and discussions in issues.
3. Identify bugs and issues and report these
You can always also reach out in gitter to discuss your contributions!

View File

@@ -1,8 +1,11 @@
black
wheel
mypy==0.790
requests==2.24.0
coreapi==2.3.3
typing-extensions==3.7.4.3
gitpython==3.1.9
pre-commit==2.7.1
pytest==6.1.1
pytest-mypy-plugins==1.6.1
psycopg2-binary
flake8==3.7.9
flake8-pyi==19.3.0
isort==4.3.21
gitpython==3.1.0
-e .

View File

@@ -70,7 +70,7 @@ class ParallelTestSuite(TestSuite):
processes: Any = ...
failfast: Any = ...
def __init__(self, suite: Any, processes: Any, failfast: bool = ...) -> None: ...
def run(self, result: Any): ... # type: ignore
def run(self, result: Any): ... # type: ignore[override]
class DiscoverRunner:
test_suite: Any = ...

View File

@@ -1,14 +0,0 @@
[flake8]
filename =
*.pyi
exclude =
django-sources
test-data
mypy_django_plugin
scripts
select = F401, Y
max_line_length = 120
per-file-ignores =
*__init__.pyi: F401
base_user.pyi: Y003
models.pyi: Y003

View File

@@ -1,2 +1,14 @@
[mypy]
warn_unused_ignores = True
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

@@ -2,9 +2,7 @@ import os
import sys
from collections import defaultdict
from contextlib import contextmanager
from typing import (
TYPE_CHECKING, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union,
)
from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union
from django.core.exceptions import FieldError
from django.db import models
@@ -26,9 +24,11 @@ from mypy_django_plugin.lib import fullnames, helpers
try:
from django.contrib.postgres.fields import ArrayField
except ImportError:
class ArrayField: # type: ignore
pass
if TYPE_CHECKING:
from django.apps.registry import Apps # noqa: F401
from django.conf import LazySettings # noqa: F401
@@ -45,9 +45,9 @@ def temp_environ():
os.environ.update(environ)
def initialize_django(settings_module: str) -> Tuple['Apps', 'LazySettings']:
def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
with temp_environ():
os.environ['DJANGO_SETTINGS_MODULE'] = settings_module
os.environ["DJANGO_SETTINGS_MODULE"] = settings_module
# add current directory to sys.path
sys.path.append(os.getcwd())
@@ -60,8 +60,8 @@ def initialize_django(settings_module: str) -> Tuple['Apps', 'LazySettings']:
models.QuerySet.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore
models.Manager.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore
from django.conf import settings
from django.apps import apps
from django.conf import settings
apps.get_models.cache_clear() # type: ignore
apps.get_swappable_settings_name.cache_clear() # type: ignore
@@ -100,15 +100,13 @@ class DjangoContext:
modules[concrete_model_cls.__module__].add(concrete_model_cls)
# collect abstract=True models
for model_cls in concrete_model_cls.mro()[1:]:
if (issubclass(model_cls, Model)
and hasattr(model_cls, '_meta')
and model_cls._meta.abstract):
if issubclass(model_cls, Model) and hasattr(model_cls, "_meta") and model_cls._meta.abstract:
modules[model_cls.__module__].add(model_cls)
return modules
def get_model_class_by_fullname(self, fullname: str) -> Optional[Type[Model]]:
# Returns None if Model is abstract
module, _, model_cls_name = fullname.rpartition('.')
module, _, model_cls_name = fullname.rpartition(".")
for model_cls in self.model_modules.get(module, set()):
if model_cls.__name__ == model_cls_name:
return model_cls
@@ -128,7 +126,7 @@ class DjangoContext:
if isinstance(field, (RelatedField, ForeignObjectRel)):
related_model_cls = field.related_model
primary_key_field = self.get_primary_key_field(related_model_cls)
primary_key_type = self.get_field_get_type(api, primary_key_field, method='init')
primary_key_type = self.get_field_get_type(api, primary_key_field, method="init")
rel_model_info = helpers.lookup_class_typeinfo(api, related_model_cls)
if rel_model_info is None:
@@ -140,15 +138,14 @@ class DjangoContext:
field_info = helpers.lookup_class_typeinfo(api, field.__class__)
if field_info is None:
return AnyType(TypeOfAny.explicit)
return helpers.get_private_descriptor_type(field_info, '_pyi_lookup_exact_type',
is_nullable=field.null)
return helpers.get_private_descriptor_type(field_info, "_pyi_lookup_exact_type", is_nullable=field.null)
def get_primary_key_field(self, model_cls: Type[Model]) -> Field:
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
if field.primary_key:
return field
raise ValueError('No primary key defined')
raise ValueError("No primary key defined")
def get_expected_types(self, api: TypeChecker, model_cls: Type[Model], *, method: str) -> Dict[str, MypyType]:
from django.contrib.contenttypes.fields import GenericForeignKey
@@ -158,7 +155,7 @@ class DjangoContext:
if not model_cls._meta.abstract:
primary_key_field = self.get_primary_key_field(model_cls)
field_set_type = self.get_field_set_type(api, primary_key_field, method=method)
expected_types['pk'] = field_set_type
expected_types["pk"] = field_set_type
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
@@ -188,11 +185,10 @@ class DjangoContext:
continue
is_nullable = self.get_field_nullability(field, method)
foreign_key_set_type = helpers.get_private_descriptor_type(foreign_key_info,
'_pyi_private_set_type',
is_nullable=is_nullable)
model_set_type = helpers.convert_any_to_type(foreign_key_set_type,
Instance(related_model_info, []))
foreign_key_set_type = helpers.get_private_descriptor_type(
foreign_key_info, "_pyi_private_set_type", is_nullable=is_nullable
)
model_set_type = helpers.convert_any_to_type(foreign_key_set_type, Instance(related_model_info, []))
expected_types[field_name] = model_set_type
@@ -200,8 +196,7 @@ class DjangoContext:
# it's generic, so cannot set specific model
field_name = field.name
gfk_info = helpers.lookup_class_typeinfo(api, field.__class__)
gfk_set_type = helpers.get_private_descriptor_type(gfk_info, '_pyi_private_set_type',
is_nullable=True)
gfk_set_type = helpers.get_private_descriptor_type(gfk_info, "_pyi_private_set_type", is_nullable=True)
expected_types[field_name] = gfk_set_type
return expected_types
@@ -230,11 +225,10 @@ class DjangoContext:
nullable = field.null
if not nullable and isinstance(field, CharField) and field.blank:
return True
if method == '__init__':
if ((isinstance(field, Field) and field.primary_key)
or isinstance(field, ForeignKey)):
if method == "__init__":
if (isinstance(field, Field) and field.primary_key) or isinstance(field, ForeignKey):
return True
if method == 'create':
if method == "create":
if isinstance(field, AutoField):
return True
if isinstance(field, Field) and field.has_default():
@@ -251,8 +245,9 @@ class DjangoContext:
if field_info is None:
return AnyType(TypeOfAny.from_error)
field_set_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_set_type',
is_nullable=self.get_field_nullability(field, method))
field_set_type = helpers.get_private_descriptor_type(
field_info, "_pyi_private_set_type", is_nullable=self.get_field_nullability(field, method)
)
if isinstance(target_field, ArrayField):
argument_field_type = self.get_field_set_type(api, target_field.base_field, method=method)
field_set_type = helpers.convert_any_to_type(field_set_type, argument_field_type)
@@ -270,7 +265,7 @@ class DjangoContext:
if related_model_cls is None:
return AnyType(TypeOfAny.from_error)
if method == 'values':
if method == "values":
primary_key_field = self.get_primary_key_field(related_model_cls)
return self.get_field_get_type(api, primary_key_field, method=method)
@@ -280,8 +275,7 @@ class DjangoContext:
return Instance(model_info, [])
else:
return helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type',
is_nullable=is_nullable)
return helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=is_nullable)
def get_field_related_model_cls(self, field: Union[RelatedField, ForeignObjectRel]) -> Optional[Type[Model]]:
if isinstance(field, RelatedField):
@@ -290,26 +284,25 @@ class DjangoContext:
related_model_cls = field.field.model
if isinstance(related_model_cls, str):
if related_model_cls == 'self':
if related_model_cls == "self":
# same model
related_model_cls = field.model
elif '.' not in related_model_cls:
elif "." not in related_model_cls:
# same file model
related_model_fullname = field.model.__module__ + '.' + related_model_cls
related_model_fullname = field.model.__module__ + "." + related_model_cls
related_model_cls = self.get_model_class_by_fullname(related_model_fullname)
else:
related_model_cls = self.apps_registry.get_model(related_model_cls)
return related_model_cls
def _resolve_field_from_parts(self,
field_parts: Iterable[str],
model_cls: Type[Model]
def _resolve_field_from_parts(
self, field_parts: Iterable[str], model_cls: Type[Model]
) -> Union[Field, ForeignObjectRel]:
currently_observed_model = model_cls
field = None
for field_part in field_parts:
if field_part == 'pk':
if field_part == "pk":
field = self.get_primary_key_field(currently_observed_model)
continue
@@ -317,8 +310,7 @@ class DjangoContext:
if isinstance(field, RelatedField):
currently_observed_model = field.related_model
model_name = currently_observed_model._meta.model_name
if (model_name is not None
and field_part == (model_name + '_id')):
if model_name is not None and field_part == (model_name + "_id"):
field = self.get_primary_key_field(currently_observed_model)
if isinstance(field, ForeignObjectRel):
@@ -368,13 +360,13 @@ class DjangoContext:
if lookup_base.args and isinstance(lookup_base.args[0], Instance):
lookup_type: MypyType = lookup_base.args[0]
# if it's Field, consider lookup_type a __get__ of current field
if (isinstance(lookup_type, Instance)
and lookup_type.type.fullname == fullnames.FIELD_FULLNAME):
if isinstance(lookup_type, Instance) and lookup_type.type.fullname == fullnames.FIELD_FULLNAME:
field_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), field.__class__)
if field_info is None:
return AnyType(TypeOfAny.explicit)
lookup_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type',
is_nullable=field.null)
lookup_type = helpers.get_private_descriptor_type(
field_info, "_pyi_private_get_type", is_nullable=field.null
)
return lookup_type
return AnyType(TypeOfAny.explicit)

View File

@@ -1,39 +1,34 @@
MODEL_CLASS_FULLNAME = "django.db.models.base.Model"
FIELD_FULLNAME = "django.db.models.fields.Field"
CHAR_FIELD_FULLNAME = "django.db.models.fields.CharField"
ARRAY_FIELD_FULLNAME = "django.contrib.postgres.fields.array.ArrayField"
AUTO_FIELD_FULLNAME = "django.db.models.fields.AutoField"
GENERIC_FOREIGN_KEY_FULLNAME = "django.contrib.contenttypes.fields.GenericForeignKey"
FOREIGN_KEY_FULLNAME = "django.db.models.fields.related.ForeignKey"
ONETOONE_FIELD_FULLNAME = "django.db.models.fields.related.OneToOneField"
MANYTOMANY_FIELD_FULLNAME = "django.db.models.fields.related.ManyToManyField"
DUMMY_SETTINGS_BASE_CLASS = "django.conf._DjangoConfLazyObject"
MODEL_CLASS_FULLNAME = 'django.db.models.base.Model'
FIELD_FULLNAME = 'django.db.models.fields.Field'
CHAR_FIELD_FULLNAME = 'django.db.models.fields.CharField'
ARRAY_FIELD_FULLNAME = 'django.contrib.postgres.fields.array.ArrayField'
AUTO_FIELD_FULLNAME = 'django.db.models.fields.AutoField'
GENERIC_FOREIGN_KEY_FULLNAME = 'django.contrib.contenttypes.fields.GenericForeignKey'
FOREIGN_KEY_FULLNAME = 'django.db.models.fields.related.ForeignKey'
ONETOONE_FIELD_FULLNAME = 'django.db.models.fields.related.OneToOneField'
MANYTOMANY_FIELD_FULLNAME = 'django.db.models.fields.related.ManyToManyField'
DUMMY_SETTINGS_BASE_CLASS = 'django.conf._DjangoConfLazyObject'
QUERYSET_CLASS_FULLNAME = "django.db.models.query.QuerySet"
BASE_MANAGER_CLASS_FULLNAME = "django.db.models.manager.BaseManager"
MANAGER_CLASS_FULLNAME = "django.db.models.manager.Manager"
RELATED_MANAGER_CLASS = "django.db.models.manager.RelatedManager"
QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet'
BASE_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.BaseManager'
MANAGER_CLASS_FULLNAME = 'django.db.models.manager.Manager'
RELATED_MANAGER_CLASS = 'django.db.models.manager.RelatedManager'
BASEFORM_CLASS_FULLNAME = "django.forms.forms.BaseForm"
FORM_CLASS_FULLNAME = "django.forms.forms.Form"
MODELFORM_CLASS_FULLNAME = "django.forms.models.ModelForm"
BASEFORM_CLASS_FULLNAME = 'django.forms.forms.BaseForm'
FORM_CLASS_FULLNAME = 'django.forms.forms.Form'
MODELFORM_CLASS_FULLNAME = 'django.forms.models.ModelForm'
FORM_MIXIN_CLASS_FULLNAME = 'django.views.generic.edit.FormMixin'
FORM_MIXIN_CLASS_FULLNAME = "django.views.generic.edit.FormMixin"
MANAGER_CLASSES = {
MANAGER_CLASS_FULLNAME,
BASE_MANAGER_CLASS_FULLNAME,
}
RELATED_FIELDS_CLASSES = {
FOREIGN_KEY_FULLNAME,
ONETOONE_FIELD_FULLNAME,
MANYTOMANY_FIELD_FULLNAME
}
RELATED_FIELDS_CLASSES = {FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME, MANYTOMANY_FIELD_FULLNAME}
MIGRATION_CLASS_FULLNAME = 'django.db.migrations.migration.Migration'
OPTIONS_CLASS_FULLNAME = 'django.db.models.options.Options'
HTTPREQUEST_CLASS_FULLNAME = 'django.http.request.HttpRequest'
MIGRATION_CLASS_FULLNAME = "django.db.migrations.migration.Migration"
OPTIONS_CLASS_FULLNAME = "django.db.models.options.Options"
HTTPREQUEST_CLASS_FULLNAME = "django.http.request.HttpRequest"
F_EXPRESSION_FULLNAME = 'django.db.models.expressions.F'
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"

View File

@@ -1,7 +1,5 @@
from collections import OrderedDict
from typing import (
TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union,
)
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union
from django.db.models.fields import Field
from django.db.models.fields.related import RelatedField
@@ -10,11 +8,31 @@ from mypy import checker
from mypy.checker import TypeChecker
from mypy.mro import calculate_mro
from mypy.nodes import (
GDEF, MDEF, Argument, Block, ClassDef, Expression, FuncDef, MemberExpr, MypyFile, NameExpr, PlaceholderNode,
StrExpr, SymbolNode, SymbolTable, SymbolTableNode, TypeInfo, Var,
GDEF,
MDEF,
Argument,
Block,
ClassDef,
Expression,
FuncDef,
MemberExpr,
MypyFile,
NameExpr,
PlaceholderNode,
StrExpr,
SymbolNode,
SymbolTable,
SymbolTableNode,
TypeInfo,
Var,
)
from mypy.plugin import (
AttributeContext, CheckerPluginInterface, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext,
AttributeContext,
CheckerPluginInterface,
ClassDefContext,
DynamicClassDefContext,
FunctionContext,
MethodContext,
)
from mypy.plugins.common import add_method
from mypy.semanal import SemanticAnalyzer
@@ -29,7 +47,7 @@ if TYPE_CHECKING:
def get_django_metadata(model_info: TypeInfo) -> Dict[str, Any]:
return model_info.metadata.setdefault('django', {})
return model_info.metadata.setdefault("django", {})
class IncompleteDefnException(Exception):
@@ -37,9 +55,9 @@ class IncompleteDefnException(Exception):
def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
if '.' not in fullname:
if "." not in fullname:
return None
module, cls_name = fullname.rsplit('.', 1)
module, cls_name = fullname.rsplit(".", 1)
module_file = all_modules.get(module)
if module_file is None:
@@ -71,12 +89,11 @@ def lookup_class_typeinfo(api: TypeChecker, klass: type) -> Optional[TypeInfo]:
def reparametrize_instance(instance: Instance, new_args: List[MypyType]) -> Instance:
return Instance(instance.type, args=new_args,
line=instance.line, column=instance.column)
return Instance(instance.type, args=new_args, line=instance.line, column=instance.column)
def get_class_fullname(klass: type) -> str:
return klass.__module__ + '.' + klass.__qualname__
return klass.__module__ + "." + klass.__qualname__
def get_call_argument_by_name(ctx: Union[FunctionContext, MethodContext], name: str) -> Optional[Expression]:
@@ -115,9 +132,9 @@ def make_optional(typ: MypyType) -> MypyType:
def parse_bool(expr: Expression) -> Optional[bool]:
if isinstance(expr, NameExpr):
if expr.fullname == 'builtins.True':
if expr.fullname == "builtins.True":
return True
if expr.fullname == 'builtins.False':
if expr.fullname == "builtins.False":
return False
return None
@@ -164,27 +181,24 @@ def get_field_lookup_exact_type(api: TypeChecker, field: Field) -> MypyType:
field_info = lookup_class_typeinfo(api, field.__class__)
if field_info is None:
return AnyType(TypeOfAny.explicit)
return get_private_descriptor_type(field_info, '_pyi_lookup_exact_type',
is_nullable=field.null)
return get_private_descriptor_type(field_info, "_pyi_lookup_exact_type", is_nullable=field.null)
def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]:
metaclass_sym = info.names.get('Meta')
metaclass_sym = info.names.get("Meta")
if metaclass_sym is not None and isinstance(metaclass_sym.node, TypeInfo):
return metaclass_sym.node
return None
def add_new_class_for_module(module: MypyFile,
name: str,
bases: List[Instance],
fields: Optional[Dict[str, MypyType]] = None
def add_new_class_for_module(
module: MypyFile, name: str, bases: List[Instance], fields: Optional[Dict[str, MypyType]] = None
) -> TypeInfo:
new_class_unique_name = checker.gen_unique_name(name, module.names)
# make new class expression
classdef = ClassDef(new_class_unique_name, Block([]))
classdef.fullname = module.fullname + '.' + new_class_unique_name
classdef.fullname = module.fullname + "." + new_class_unique_name
# make new TypeInfo
new_typeinfo = TypeInfo(SymbolTable(), classdef, module.fullname)
@@ -197,7 +211,7 @@ def add_new_class_for_module(module: MypyFile,
for field_name, field_type in fields.items():
var = Var(field_name, type=field_type)
var.info = new_typeinfo
var._fullname = new_typeinfo.fullname + '.' + field_name
var._fullname = new_typeinfo.fullname + "." + field_name
new_typeinfo.names[field_name] = SymbolTableNode(MDEF, var, plugin_generated=True)
classdef.info = new_typeinfo
@@ -215,18 +229,17 @@ def get_current_module(api: TypeChecker) -> MypyFile:
return current_module
def make_oneoff_named_tuple(api: TypeChecker, name: str, fields: 'OrderedDict[str, MypyType]') -> TupleType:
def make_oneoff_named_tuple(api: TypeChecker, name: str, fields: "OrderedDict[str, MypyType]") -> TupleType:
current_module = get_current_module(api)
namedtuple_info = add_new_class_for_module(current_module, name,
bases=[api.named_generic_type('typing.NamedTuple', [])],
fields=fields)
namedtuple_info = add_new_class_for_module(
current_module, name, bases=[api.named_generic_type("typing.NamedTuple", [])], fields=fields
)
return TupleType(list(fields.values()), fallback=Instance(namedtuple_info, []))
def make_tuple(api: 'TypeChecker', fields: List[MypyType]) -> TupleType:
def make_tuple(api: "TypeChecker", fields: List[MypyType]) -> TupleType:
# fallback for tuples is any builtins.tuple instance
fallback = api.named_generic_type('builtins.tuple',
[AnyType(TypeOfAny.special_form)])
fallback = api.named_generic_type("builtins.tuple", [AnyType(TypeOfAny.special_form)])
return TupleType(fields, fallback=fallback)
@@ -235,8 +248,7 @@ def convert_any_to_type(typ: MypyType, referred_to_type: MypyType) -> MypyType:
converted_items = []
for item in typ.items:
converted_items.append(convert_any_to_type(item, referred_to_type))
return UnionType.make_union(converted_items,
line=typ.line, column=typ.column)
return UnionType.make_union(converted_items, line=typ.line, column=typ.column)
if isinstance(typ, Instance):
args = []
for default_arg in typ.args:
@@ -252,21 +264,22 @@ def convert_any_to_type(typ: MypyType, referred_to_type: MypyType) -> MypyType:
return typ
def make_typeddict(api: CheckerPluginInterface, fields: 'OrderedDict[str, MypyType]',
required_keys: Set[str]) -> TypedDictType:
object_type = api.named_generic_type('mypy_extensions._TypedDict', [])
def make_typeddict(
api: CheckerPluginInterface, fields: "OrderedDict[str, MypyType]", required_keys: Set[str]
) -> TypedDictType:
object_type = api.named_generic_type("mypy_extensions._TypedDict", [])
typed_dict_type = TypedDictType(fields, required_keys=required_keys, fallback=object_type)
return typed_dict_type
def resolve_string_attribute_value(attr_expr: Expression, django_context: 'DjangoContext') -> Optional[str]:
def resolve_string_attribute_value(attr_expr: Expression, django_context: "DjangoContext") -> Optional[str]:
if isinstance(attr_expr, StrExpr):
return attr_expr.value
# support extracting from settings, in general case it's unresolvable yet
if isinstance(attr_expr, MemberExpr):
member_name = attr_expr.name
if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == 'django.conf.settings':
if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == "django.conf.settings":
if hasattr(django_context.settings, member_name):
return getattr(django_context.settings, member_name)
return None
@@ -274,27 +287,27 @@ def resolve_string_attribute_value(attr_expr: Expression, django_context: 'Djang
def get_semanal_api(ctx: Union[ClassDefContext, DynamicClassDefContext]) -> SemanticAnalyzer:
if not isinstance(ctx.api, SemanticAnalyzer):
raise ValueError('Not a SemanticAnalyzer')
raise ValueError("Not a SemanticAnalyzer")
return ctx.api
def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionContext]) -> TypeChecker:
if not isinstance(ctx.api, TypeChecker):
raise ValueError('Not a TypeChecker')
raise ValueError("Not a TypeChecker")
return ctx.api
def is_model_subclass_info(info: TypeInfo, django_context: 'DjangoContext') -> bool:
return (info.fullname in django_context.all_registered_model_class_fullnames
or info.has_base(fullnames.MODEL_CLASS_FULLNAME))
def is_model_subclass_info(info: TypeInfo, django_context: "DjangoContext") -> bool:
return info.fullname in django_context.all_registered_model_class_fullnames or info.has_base(
fullnames.MODEL_CLASS_FULLNAME
)
def check_types_compatible(ctx: Union[FunctionContext, MethodContext],
*, expected_type: MypyType, actual_type: MypyType, error_message: str) -> None:
def check_types_compatible(
ctx: Union[FunctionContext, MethodContext], *, expected_type: MypyType, actual_type: MypyType, error_message: str
) -> None:
api = get_typechecker_api(ctx)
api.check_subtype(actual_type, expected_type,
ctx.context, error_message,
'got', 'expected')
api.check_subtype(actual_type, expected_type, ctx.context, error_message, "got", "expected")
def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> None:
@@ -302,11 +315,10 @@ def add_new_sym_for_info(info: TypeInfo, *, name: str, sym_type: MypyType) -> No
var = Var(name=name, type=sym_type)
# var.info: type of the object variable is bound to
var.info = info
var._fullname = info.fullname + '.' + name
var._fullname = info.fullname + "." + name
var.is_initialized_in_class = True
var.is_inferred = True
info.names[name] = SymbolTableNode(MDEF, var,
plugin_generated=True)
info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True)
def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument], MypyType]:
@@ -322,8 +334,9 @@ def build_unannotated_method_args(method_node: FuncDef) -> Tuple[List[Argument],
return prepared_arguments, return_type
def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
new_method_name: str, method_node: FuncDef) -> None:
def copy_method_to_another_class(
ctx: ClassDefContext, self_type: Instance, new_method_name: str, method_node: FuncDef
) -> None:
semanal_api = get_semanal_api(ctx)
if method_node.type is None:
if not semanal_api.final_iteration:
@@ -331,11 +344,7 @@ def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
return
arguments, return_type = build_unannotated_method_args(method_node)
add_method(ctx,
new_method_name,
args=arguments,
return_type=return_type,
self_type=self_type)
add_method(ctx, new_method_name, args=arguments, return_type=return_type, self_type=self_type)
return
method_type = method_node.type
@@ -345,17 +354,16 @@ def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
return
arguments = []
bound_return_type = semanal_api.anal_type(method_type.ret_type,
allow_placeholder=True)
bound_return_type = semanal_api.anal_type(method_type.ret_type, allow_placeholder=True)
assert bound_return_type is not None
if isinstance(bound_return_type, PlaceholderNode):
return
for arg_name, arg_type, original_argument in zip(method_type.arg_names[1:],
method_type.arg_types[1:],
method_node.arguments[1:]):
for arg_name, arg_type, original_argument in zip(
method_type.arg_names[1:], method_type.arg_types[1:], method_node.arguments[1:]
):
bound_arg_type = semanal_api.anal_type(arg_type, allow_placeholder=True)
if bound_arg_type is None and not semanal_api.final_iteration:
semanal_api.defer()
@@ -366,19 +374,16 @@ def copy_method_to_another_class(ctx: ClassDefContext, self_type: Instance,
if isinstance(bound_arg_type, PlaceholderNode):
return
var = Var(name=original_argument.variable.name,
type=arg_type)
var = Var(name=original_argument.variable.name, type=arg_type)
var.line = original_argument.variable.line
var.column = original_argument.variable.column
argument = Argument(variable=var,
argument = Argument(
variable=var,
type_annotation=bound_arg_type,
initializer=original_argument.initializer,
kind=original_argument.kind)
kind=original_argument.kind,
)
argument.set_line(original_argument)
arguments.append(argument)
add_method(ctx,
new_method_name,
args=arguments,
return_type=bound_return_type,
self_type=self_type)
add_method(ctx, new_method_name, args=arguments, return_type=bound_return_type, self_type=self_type)

View File

@@ -8,28 +8,28 @@ from mypy.modulefinder import mypy_path
from mypy.nodes import MypyFile, TypeInfo
from mypy.options import Options
from mypy.plugin import (
AttributeContext, ClassDefContext, DynamicClassDefContext, FunctionContext, MethodContext, Plugin,
AttributeContext,
ClassDefContext,
DynamicClassDefContext,
FunctionContext,
MethodContext,
Plugin,
)
from mypy.types import Type as MypyType
import mypy_django_plugin.transformers.orm_lookups
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.transformers import (
fields, forms, init_create, meta, querysets, request, settings,
)
from mypy_django_plugin.transformers.managers import (
create_new_manager_class_from_from_queryset_method,
)
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
from mypy_django_plugin.transformers.managers import create_new_manager_class_from_from_queryset_method
from mypy_django_plugin.transformers.models import process_model_class
def transform_model_class(ctx: ClassDefContext,
django_context: DjangoContext) -> None:
def transform_model_class(ctx: ClassDefContext, django_context: DjangoContext) -> None:
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.MODEL_CLASS_FULLNAME)
if sym is not None and isinstance(sym.node, TypeInfo):
helpers.get_django_metadata(sym.node)['model_bases'][ctx.cls.fullname] = 1
helpers.get_django_metadata(sym.node)["model_bases"][ctx.cls.fullname] = 1
else:
if not ctx.api.final_iteration:
ctx.api.defer()
@@ -41,7 +41,7 @@ def transform_model_class(ctx: ClassDefContext,
def transform_form_class(ctx: ClassDefContext) -> None:
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.BASEFORM_CLASS_FULLNAME)
if sym is not None and isinstance(sym.node, TypeInfo):
helpers.get_django_metadata(sym.node)['baseform_bases'][ctx.cls.fullname] = 1
helpers.get_django_metadata(sym.node)["baseform_bases"][ctx.cls.fullname] = 1
forms.make_meta_nested_class_inherit_from_any(ctx)
@@ -49,11 +49,10 @@ def transform_form_class(ctx: ClassDefContext) -> None:
def add_new_manager_base(ctx: ClassDefContext) -> None:
sym = ctx.api.lookup_fully_qualified_or_none(fullnames.MANAGER_CLASS_FULLNAME)
if sym is not None and isinstance(sym.node, TypeInfo):
helpers.get_django_metadata(sym.node)['manager_bases'][ctx.cls.fullname] = 1
helpers.get_django_metadata(sym.node)["manager_bases"][ctx.cls.fullname] = 1
def extract_django_settings_module(config_file_path: Optional[str]) -> str:
def exit(error_type: int) -> NoReturn:
"""Using mypy's argument parser, raise `SystemExit` to fail hard if validation fails.
@@ -69,24 +68,28 @@ def extract_django_settings_module(config_file_path: Optional[str]) -> str:
[mypy.plugins.django_stubs]
django_settings_module: str (required)
...
""".replace("\n" + 8 * " ", "\n")
handler = CapturableArgumentParser(prog='(django-stubs) mypy', usage=usage)
messages = {1: 'mypy config file is not specified or found',
2: 'no section [mypy.plugins.django-stubs]',
3: 'the setting is not provided'}
""".replace(
"\n" + 8 * " ", "\n"
)
handler = CapturableArgumentParser(prog="(django-stubs) mypy", usage=usage)
messages = {
1: "mypy config file is not specified or found",
2: "no section [mypy.plugins.django-stubs]",
3: "the setting is not provided",
}
handler.error("'django_settings_module' is not set: " + messages[error_type])
parser = configparser.ConfigParser()
try:
parser.read_file(open(cast(str, config_file_path), 'r'), source=config_file_path)
parser.read_file(open(cast(str, config_file_path)), source=config_file_path)
except (IsADirectoryError, OSError):
exit(1)
section = 'mypy.plugins.django-stubs'
section = "mypy.plugins.django-stubs"
if not parser.has_section(section):
exit(2)
settings = parser.get(section, 'django_settings_module', fallback=None) or exit(3)
return cast(str, settings).strip('\'"')
settings = parser.get(section, "django_settings_module", fallback=None) or exit(3)
return cast(str, settings).strip("'\"")
class NewSemanalDjangoPlugin(Plugin):
@@ -102,34 +105,41 @@ class NewSemanalDjangoPlugin(Plugin):
def _get_current_queryset_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(fullnames.QUERYSET_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return (helpers.get_django_metadata(model_sym.node)
.setdefault('queryset_bases', {fullnames.QUERYSET_CLASS_FULLNAME: 1}))
return helpers.get_django_metadata(model_sym.node).setdefault(
"queryset_bases", {fullnames.QUERYSET_CLASS_FULLNAME: 1}
)
else:
return {}
def _get_current_manager_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(fullnames.MANAGER_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return (helpers.get_django_metadata(model_sym.node)
.setdefault('manager_bases', {fullnames.MANAGER_CLASS_FULLNAME: 1}))
return helpers.get_django_metadata(model_sym.node).setdefault(
"manager_bases", {fullnames.MANAGER_CLASS_FULLNAME: 1}
)
else:
return {}
def _get_current_model_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(fullnames.MODEL_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return helpers.get_django_metadata(model_sym.node).setdefault('model_bases',
{fullnames.MODEL_CLASS_FULLNAME: 1})
return helpers.get_django_metadata(model_sym.node).setdefault(
"model_bases", {fullnames.MODEL_CLASS_FULLNAME: 1}
)
else:
return {}
def _get_current_form_bases(self) -> Dict[str, int]:
model_sym = self.lookup_fully_qualified(fullnames.BASEFORM_CLASS_FULLNAME)
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
return (helpers.get_django_metadata(model_sym.node)
.setdefault('baseform_bases', {fullnames.BASEFORM_CLASS_FULLNAME: 1,
return helpers.get_django_metadata(model_sym.node).setdefault(
"baseform_bases",
{
fullnames.BASEFORM_CLASS_FULLNAME: 1,
fullnames.FORM_CLASS_FULLNAME: 1,
fullnames.MODELFORM_CLASS_FULLNAME: 1}))
fullnames.MODELFORM_CLASS_FULLNAME: 1,
},
)
else:
return {}
@@ -144,17 +154,16 @@ class NewSemanalDjangoPlugin(Plugin):
def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
# for settings
if file.fullname == 'django.conf' and self.django_context.django_settings_module:
if file.fullname == "django.conf" and self.django_context.django_settings_module:
return [self._new_dependency(self.django_context.django_settings_module)]
# for values / values_list
if file.fullname == 'django.db.models':
return [self._new_dependency('mypy_extensions'), self._new_dependency('typing')]
if file.fullname == "django.db.models":
return [self._new_dependency("mypy_extensions"), self._new_dependency("typing")]
# for `get_user_model()`
if self.django_context.settings:
if (file.fullname == 'django.contrib.auth'
or file.fullname in {'django.http', 'django.http.request'}):
if file.fullname == "django.contrib.auth" or file.fullname in {"django.http", "django.http.request"}:
auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL
try:
auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__
@@ -186,9 +195,8 @@ class NewSemanalDjangoPlugin(Plugin):
deps.add(self._new_dependency(related_model_module))
return list(deps)
def get_function_hook(self, fullname: str
) -> Optional[Callable[[FunctionContext], MypyType]]:
if fullname == 'django.contrib.auth.get_user_model':
def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], MypyType]]:
if fullname == "django.contrib.auth.get_user_model":
return partial(settings.get_user_model_hook, django_context=self.django_context)
manager_bases = self._get_current_manager_bases()
@@ -204,46 +212,48 @@ class NewSemanalDjangoPlugin(Plugin):
return partial(init_create.redefine_and_typecheck_model_init, django_context=self.django_context)
return None
def get_method_hook(self, fullname: str
) -> Optional[Callable[[MethodContext], MypyType]]:
class_fullname, _, method_name = fullname.rpartition('.')
if method_name == 'get_form_class':
def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], MypyType]]:
class_fullname, _, method_name = fullname.rpartition(".")
if method_name == "get_form_class":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
return forms.extract_proper_type_for_get_form_class
if method_name == 'get_form':
if method_name == "get_form":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
return forms.extract_proper_type_for_get_form
if method_name == 'values':
if method_name == "values":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
return partial(querysets.extract_proper_type_queryset_values, django_context=self.django_context)
if method_name == 'values_list':
if method_name == "values_list":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
return partial(querysets.extract_proper_type_queryset_values_list, django_context=self.django_context)
if method_name == 'get_field':
if method_name == "get_field":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.OPTIONS_CLASS_FULLNAME):
return partial(meta.return_proper_field_type_from_get_field, django_context=self.django_context)
manager_classes = self._get_current_manager_bases()
if class_fullname in manager_classes and method_name == 'create':
if class_fullname in manager_classes and method_name == "create":
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
if class_fullname in manager_classes and method_name in {'filter', 'get', 'exclude'}:
return partial(mypy_django_plugin.transformers.orm_lookups.typecheck_queryset_filter,
django_context=self.django_context)
if class_fullname in manager_classes and method_name in {"filter", "get", "exclude"}:
return partial(
mypy_django_plugin.transformers.orm_lookups.typecheck_queryset_filter,
django_context=self.django_context,
)
return None
def get_base_class_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
if (fullname in self.django_context.all_registered_model_class_fullnames
or fullname in self._get_current_model_bases()):
def get_base_class_hook(self, fullname: str) -> Optional[Callable[[ClassDefContext], None]]:
if (
fullname in self.django_context.all_registered_model_class_fullnames
or fullname in self._get_current_model_bases()
):
return partial(transform_model_class, django_context=self.django_context)
if fullname in self._get_current_manager_bases():
@@ -253,22 +263,19 @@ class NewSemanalDjangoPlugin(Plugin):
return transform_form_class
return None
def get_attribute_hook(self, fullname: str
) -> Optional[Callable[[AttributeContext], MypyType]]:
class_name, _, attr_name = fullname.rpartition('.')
def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeContext], MypyType]]:
class_name, _, attr_name = fullname.rpartition(".")
if class_name == fullnames.DUMMY_SETTINGS_BASE_CLASS:
return partial(settings.get_type_of_settings_attribute,
django_context=self.django_context)
return partial(settings.get_type_of_settings_attribute, django_context=self.django_context)
info = self._get_typeinfo_or_none(class_name)
if info and info.has_base(fullnames.HTTPREQUEST_CLASS_FULLNAME) and attr_name == 'user':
if info and info.has_base(fullnames.HTTPREQUEST_CLASS_FULLNAME) and attr_name == "user":
return partial(request.set_auth_user_model_as_type_for_request_user, django_context=self.django_context)
return None
def get_dynamic_class_hook(self, fullname: str
) -> Optional[Callable[[DynamicClassDefContext], None]]:
if fullname.endswith('from_queryset'):
class_name, _, _ = fullname.rpartition('.')
def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
if fullname.endswith("from_queryset"):
class_name, _, _ = fullname.rpartition(".")
info = self._get_typeinfo_or_none(class_name)
if info and info.has_base(fullnames.BASE_MANAGER_CLASS_FULLNAME):
return create_new_manager_class_from_from_queryset_method

View File

@@ -14,8 +14,7 @@ from mypy_django_plugin.lib import fullnames, helpers
def _get_current_field_from_assignment(ctx: FunctionContext, django_context: DjangoContext) -> Optional[Field]:
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
if (outer_model_info is None
or not helpers.is_model_subclass_info(outer_model_info, django_context)):
if outer_model_info is None or not helpers.is_model_subclass_info(outer_model_info, django_context):
return None
field_name = None
@@ -60,8 +59,7 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
# self reference with abstract=True on the model where ForeignKey is defined
current_model_cls = current_field.model
if (current_model_cls._meta.abstract
and current_model_cls == related_model_cls):
if current_model_cls._meta.abstract and current_model_cls == related_model_cls:
# for all derived non-abstract classes, set variable with this name to
# __get__/__set__ of ForeignKey of derived model
for model_cls in django_context.all_registered_model_classes:
@@ -69,11 +67,10 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
derived_model_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), model_cls)
if derived_model_info is not None:
fk_ref_type = Instance(derived_model_info, [])
derived_fk_type = reparametrize_related_field_type(default_related_field_type,
set_type=fk_ref_type, get_type=fk_ref_type)
helpers.add_new_sym_for_info(derived_model_info,
name=current_field.name,
sym_type=derived_fk_type)
derived_fk_type = reparametrize_related_field_type(
default_related_field_type, set_type=fk_ref_type, get_type=fk_ref_type
)
helpers.add_new_sym_for_info(derived_model_info, name=current_field.name, sym_type=derived_fk_type)
related_model = related_model_cls
related_model_to_set = related_model_cls
@@ -97,16 +94,14 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
related_model_to_set_type = Instance(related_model_to_set_info, []) # type: ignore
# replace Any with referred_to_type
return reparametrize_related_field_type(default_related_field_type,
set_type=related_model_to_set_type,
get_type=related_model_type)
return reparametrize_related_field_type(
default_related_field_type, set_type=related_model_to_set_type, get_type=related_model_type
)
def get_field_descriptor_types(field_info: TypeInfo, is_nullable: bool) -> Tuple[MypyType, MypyType]:
set_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_set_type',
is_nullable=is_nullable)
get_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type',
is_nullable=is_nullable)
set_type = helpers.get_private_descriptor_type(field_info, "_pyi_private_set_type", is_nullable=is_nullable)
get_type = helpers.get_private_descriptor_type(field_info, "_pyi_private_get_type", is_nullable=is_nullable)
return set_type, get_type
@@ -114,7 +109,7 @@ def set_descriptor_types_for_field(ctx: FunctionContext) -> Instance:
default_return_type = cast(Instance, ctx.default_return_type)
is_nullable = False
null_expr = helpers.get_call_argument_by_name(ctx, 'null')
null_expr = helpers.get_call_argument_by_name(ctx, "null")
if null_expr is not None:
is_nullable = helpers.parse_bool(null_expr) or False
@@ -125,7 +120,7 @@ def set_descriptor_types_for_field(ctx: FunctionContext) -> Instance:
def determine_type_of_array_field(ctx: FunctionContext, django_context: DjangoContext) -> MypyType:
default_return_type = set_descriptor_types_for_field(ctx)
base_field_arg_type = helpers.get_call_argument_type_by_name(ctx, 'base_field')
base_field_arg_type = helpers.get_call_argument_type_by_name(ctx, "base_field")
if not base_field_arg_type or not isinstance(base_field_arg_type, Instance):
return default_return_type
@@ -142,8 +137,7 @@ def transform_into_proper_return_type(ctx: FunctionContext, django_context: Djan
assert isinstance(default_return_type, Instance)
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
if (outer_model_info is None
or not helpers.is_model_subclass_info(outer_model_info, django_context)):
if outer_model_info is None or not helpers.is_model_subclass_info(outer_model_info, django_context):
return ctx.default_return_type
assert isinstance(outer_model_info, TypeInfo)

View File

@@ -18,7 +18,7 @@ def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None:
def get_specified_form_class(object_type: Instance) -> Optional[TypeType]:
form_class_sym = object_type.type.get('form_class')
form_class_sym = object_type.type.get("form_class")
if form_class_sym and isinstance(form_class_sym.type, CallableType):
return TypeType(form_class_sym.type.ret_type)
return None
@@ -28,7 +28,7 @@ def extract_proper_type_for_get_form(ctx: MethodContext) -> MypyType:
object_type = ctx.type
assert isinstance(object_type, Instance)
form_class_type = helpers.get_call_argument_type_by_name(ctx, 'form_class')
form_class_type = helpers.get_call_argument_type_by_name(ctx, "form_class")
if form_class_type is None or isinstance(form_class_type, NoneTyp):
form_class_type = get_specified_form_class(object_type)

View File

@@ -9,13 +9,14 @@ from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import helpers
def get_actual_types(ctx: Union[MethodContext, FunctionContext],
expected_keys: List[str]) -> List[Tuple[str, MypyType]]:
def get_actual_types(
ctx: Union[MethodContext, FunctionContext], expected_keys: List[str]
) -> List[Tuple[str, MypyType]]:
actual_types = []
# positionals
for pos, (actual_name, actual_type) in enumerate(zip(ctx.arg_names[0], ctx.arg_types[0])):
if actual_name is None:
if ctx.callee_arg_names[0] == 'kwargs':
if ctx.callee_arg_names[0] == "kwargs":
# unpacked dict as kwargs is not supported
continue
actual_name = expected_keys[pos]
@@ -30,23 +31,23 @@ def get_actual_types(ctx: Union[MethodContext, FunctionContext],
return actual_types
def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext,
model_cls: Type[Model], method: str) -> MypyType:
def typecheck_model_method(
ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext, model_cls: Type[Model], method: str
) -> MypyType:
typechecker_api = helpers.get_typechecker_api(ctx)
expected_types = django_context.get_expected_types(typechecker_api, model_cls, method=method)
expected_keys = [key for key in expected_types.keys() if key != 'pk']
expected_keys = [key for key in expected_types.keys() if key != "pk"]
for actual_name, actual_type in get_actual_types(ctx, expected_keys):
if actual_name not in expected_types:
ctx.api.fail('Unexpected attribute "{}" for model "{}"'.format(actual_name,
model_cls.__name__),
ctx.context)
ctx.api.fail(f'Unexpected attribute "{actual_name}" for model "{model_cls.__name__}"', ctx.context)
continue
helpers.check_types_compatible(ctx,
helpers.check_types_compatible(
ctx,
expected_type=expected_types[actual_name],
actual_type=actual_type,
error_message='Incompatible type for "{}" of "{}"'.format(actual_name,
model_cls.__name__))
error_message=f'Incompatible type for "{actual_name}" of "{model_cls.__name__}"',
)
return ctx.default_return_type
@@ -59,7 +60,7 @@ def redefine_and_typecheck_model_init(ctx: FunctionContext, django_context: Djan
if model_cls is None:
return ctx.default_return_type
return typecheck_model_method(ctx, django_context, model_cls, '__init__')
return typecheck_model_method(ctx, django_context, model_cls, "__init__")
def redefine_and_typecheck_model_create(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
@@ -72,4 +73,4 @@ def redefine_and_typecheck_model_create(ctx: MethodContext, django_context: Djan
if model_cls is None:
return ctx.default_return_type
return typecheck_model_method(ctx, django_context, model_cls, 'create')
return typecheck_model_method(ctx, django_context, model_cls, "create")

View File

@@ -1,6 +1,4 @@
from mypy.nodes import (
GDEF, FuncDef, MemberExpr, NameExpr, RefExpr, StrExpr, SymbolTableNode, TypeInfo,
)
from mypy.nodes import GDEF, FuncDef, MemberExpr, NameExpr, RefExpr, StrExpr, SymbolTableNode, TypeInfo
from mypy.plugin import ClassDefContext, DynamicClassDefContext
from mypy.types import AnyType, Instance, TypeOfAny
@@ -21,16 +19,15 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
return
assert isinstance(base_manager_info, TypeInfo)
new_manager_info = semanal_api.basic_new_typeinfo(ctx.name,
basetype_or_fallback=Instance(base_manager_info,
[AnyType(TypeOfAny.unannotated)]))
new_manager_info = semanal_api.basic_new_typeinfo(
ctx.name, basetype_or_fallback=Instance(base_manager_info, [AnyType(TypeOfAny.unannotated)])
)
new_manager_info.line = ctx.call.line
new_manager_info.defn.line = ctx.call.line
new_manager_info.metaclass_type = new_manager_info.calculate_metaclass_type()
current_module = semanal_api.cur_mod_node
current_module.names[ctx.name] = SymbolTableNode(GDEF, new_manager_info,
plugin_generated=True)
current_module.names[ctx.name] = SymbolTableNode(GDEF, new_manager_info, plugin_generated=True)
passed_queryset = ctx.call.args[0]
assert isinstance(passed_queryset, NameExpr)
@@ -55,15 +52,14 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
assert isinstance(expr, StrExpr)
custom_manager_generated_name = expr.value
else:
custom_manager_generated_name = base_manager_info.name + 'From' + derived_queryset_info.name
custom_manager_generated_name = base_manager_info.name + "From" + derived_queryset_info.name
custom_manager_generated_fullname = '.'.join(['django.db.models.manager', custom_manager_generated_name])
if 'from_queryset_managers' not in base_manager_info.metadata:
base_manager_info.metadata['from_queryset_managers'] = {}
base_manager_info.metadata['from_queryset_managers'][custom_manager_generated_fullname] = new_manager_info.fullname
custom_manager_generated_fullname = ".".join(["django.db.models.manager", custom_manager_generated_name])
if "from_queryset_managers" not in base_manager_info.metadata:
base_manager_info.metadata["from_queryset_managers"] = {}
base_manager_info.metadata["from_queryset_managers"][custom_manager_generated_fullname] = new_manager_info.fullname
class_def_context = ClassDefContext(cls=new_manager_info.defn,
reason=ctx.call, api=semanal_api)
class_def_context = ClassDefContext(cls=new_manager_info.defn, reason=ctx.call, api=semanal_api)
self_type = Instance(new_manager_info, [])
# we need to copy all methods in MRO before django.db.models.query.QuerySet
for class_mro_info in derived_queryset_info.mro:
@@ -71,7 +67,6 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
break
for name, sym in class_mro_info.names.items():
if isinstance(sym.node, FuncDef):
helpers.copy_method_to_another_class(class_def_context,
self_type,
new_method_name=name,
method_node=sym.node)
helpers.copy_method_to_another_class(
class_def_context, self_type, new_method_name=name, method_node=sym.node
)

View File

@@ -9,8 +9,7 @@ from mypy_django_plugin.lib import helpers
def _get_field_instance(ctx: MethodContext, field_fullname: str) -> MypyType:
field_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx),
field_fullname)
field_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), field_fullname)
if field_info is None:
return AnyType(TypeOfAny.unannotated)
return Instance(field_info, [AnyType(TypeOfAny.explicit), AnyType(TypeOfAny.explicit)])
@@ -32,7 +31,7 @@ def return_proper_field_type_from_get_field(ctx: MethodContext, django_context:
if model_cls is None:
return ctx.default_return_type
field_name_expr = helpers.get_call_argument_by_name(ctx, 'field_name')
field_name_expr = helpers.get_call_argument_by_name(ctx, "field_name")
if field_name_expr is None:
return ctx.default_return_type

View File

@@ -3,9 +3,7 @@ from typing import Dict, List, Optional, Type, cast
from django.db.models.base import Model
from django.db.models.fields import DateField, DateTimeField
from django.db.models.fields.related import ForeignKey
from django.db.models.fields.reverse_related import (
ManyToManyRel, ManyToOneRel, OneToOneRel,
)
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
from mypy.nodes import ARG_STAR2, Argument, Context, FuncDef, TypeInfo, Var
from mypy.plugin import ClassDefContext
from mypy.plugins import common
@@ -35,7 +33,7 @@ class ModelClassInitializer:
def lookup_typeinfo_or_incomplete_defn_error(self, fullname: str) -> TypeInfo:
info = self.lookup_typeinfo(fullname)
if info is None:
raise helpers.IncompleteDefnException(f'No {fullname!r} found')
raise helpers.IncompleteDefnException(f"No {fullname!r} found")
return info
def lookup_class_typeinfo_or_incomplete_defn_error(self, klass: type) -> TypeInfo:
@@ -48,20 +46,17 @@ class ModelClassInitializer:
var = Var(name=name, type=typ)
# var.info: type of the object variable is bound to
var.info = self.model_classdef.info
var._fullname = self.model_classdef.info.fullname + '.' + name
var._fullname = self.model_classdef.info.fullname + "." + name
var.is_initialized_in_class = True
var.is_inferred = True
return var
def add_new_node_to_model_class(self, name: str, typ: MypyType) -> None:
helpers.add_new_sym_for_info(self.model_classdef.info,
name=name,
sym_type=typ)
helpers.add_new_sym_for_info(self.model_classdef.info, name=name, sym_type=typ)
def add_new_class_for_current_module(self, name: str, bases: List[Instance]) -> TypeInfo:
current_module = self.api.modules[self.model_classdef.info.module_name]
new_class_info = helpers.add_new_class_for_module(current_module,
name=name, bases=bases)
new_class_info = helpers.add_new_class_for_module(current_module, name=name, bases=bases)
return new_class_info
def run(self) -> None:
@@ -103,8 +98,7 @@ class AddDefaultPrimaryKey(ModelClassInitializer):
auto_field_info = self.lookup_typeinfo_or_incomplete_defn_error(auto_field_fullname)
set_type, get_type = fields.get_field_descriptor_types(auto_field_info, is_nullable=False)
self.add_new_node_to_model_class(auto_field.attname, Instance(auto_field_info,
[set_type, get_type]))
self.add_new_node_to_model_class(auto_field.attname, Instance(auto_field_info, [set_type, get_type]))
class AddRelatedModelsId(ModelClassInitializer):
@@ -117,11 +111,11 @@ class AddRelatedModelsId(ModelClassInitializer):
field_sym = self.ctx.cls.info.get(field.name)
if field_sym is not None and field_sym.node is not None:
error_context = field_sym.node
self.api.fail(f'Cannot find model {field.related_model!r} '
f'referenced in field {field.name!r} ',
ctx=error_context)
self.add_new_node_to_model_class(field.attname,
AnyType(TypeOfAny.explicit))
self.api.fail(
f"Cannot find model {field.related_model!r} " f"referenced in field {field.name!r} ",
ctx=error_context,
)
self.add_new_node_to_model_class(field.attname, AnyType(TypeOfAny.explicit))
continue
if related_model_cls._meta.abstract:
@@ -138,8 +132,7 @@ class AddRelatedModelsId(ModelClassInitializer):
is_nullable = self.django_context.get_field_nullability(field, None)
set_type, get_type = get_field_descriptor_types(field_info, is_nullable)
self.add_new_node_to_model_class(field.attname,
Instance(field_info, [set_type, get_type]))
self.add_new_node_to_model_class(field.attname, Instance(field_info, [set_type, get_type]))
class AddManagers(ModelClassInitializer):
@@ -154,10 +147,9 @@ class AddManagers(ModelClassInitializer):
def get_generated_manager_mappings(self, base_manager_fullname: str) -> Dict[str, str]:
base_manager_info = self.lookup_typeinfo(base_manager_fullname)
if (base_manager_info is None
or 'from_queryset_managers' not in base_manager_info.metadata):
if base_manager_info is None or "from_queryset_managers" not in base_manager_info.metadata:
return {}
return base_manager_info.metadata['from_queryset_managers']
return base_manager_info.metadata["from_queryset_managers"]
def create_new_model_parametrized_manager(self, name: str, base_manager_info: TypeInfo) -> Instance:
bases = []
@@ -166,31 +158,27 @@ class AddManagers(ModelClassInitializer):
if original_base.type is None:
raise helpers.IncompleteDefnException()
original_base = helpers.reparametrize_instance(original_base,
[Instance(self.model_classdef.info, [])])
original_base = helpers.reparametrize_instance(original_base, [Instance(self.model_classdef.info, [])])
bases.append(original_base)
new_manager_info = self.add_new_class_for_current_module(name, bases)
# copy fields to a new manager
new_cls_def_context = ClassDefContext(cls=new_manager_info.defn,
reason=self.ctx.reason,
api=self.api)
new_cls_def_context = ClassDefContext(cls=new_manager_info.defn, reason=self.ctx.reason, api=self.api)
custom_manager_type = Instance(new_manager_info, [Instance(self.model_classdef.info, [])])
for name, sym in base_manager_info.names.items():
# replace self type with new class, if copying method
if isinstance(sym.node, FuncDef):
helpers.copy_method_to_another_class(new_cls_def_context,
self_type=custom_manager_type,
new_method_name=name,
method_node=sym.node)
helpers.copy_method_to_another_class(
new_cls_def_context, self_type=custom_manager_type, new_method_name=name, method_node=sym.node
)
continue
new_sym = sym.copy()
if isinstance(new_sym.node, Var):
new_var = Var(name, type=sym.type)
new_var.info = new_manager_info
new_var._fullname = new_manager_info.fullname + '.' + name
new_var._fullname = new_manager_info.fullname + "." + name
new_sym.node = new_var
new_manager_info.names[name] = new_sym
@@ -215,7 +203,7 @@ class AddManagers(ModelClassInitializer):
manager_info = self.lookup_typeinfo(real_manager_fullname) # type: ignore
if manager_info is None:
continue
manager_class_name = real_manager_fullname.rsplit('.', maxsplit=1)[1]
manager_class_name = real_manager_fullname.rsplit(".", maxsplit=1)[1]
if manager_name not in self.model_classdef.info.names:
manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
@@ -225,10 +213,11 @@ class AddManagers(ModelClassInitializer):
if not self.has_any_parametrized_manager_as_base(manager_info):
continue
custom_model_manager_name = manager.model.__name__ + '_' + manager_class_name
custom_model_manager_name = manager.model.__name__ + "_" + manager_class_name
try:
custom_manager_type = self.create_new_model_parametrized_manager(custom_model_manager_name,
base_manager_info=manager_info)
custom_manager_type = self.create_new_model_parametrized_manager(
custom_model_manager_name, base_manager_info=manager_info
)
except helpers.IncompleteDefnException:
continue
@@ -238,11 +227,11 @@ class AddManagers(ModelClassInitializer):
class AddDefaultManagerAttribute(ModelClassInitializer):
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
# add _default_manager
if '_default_manager' not in self.model_classdef.info.names:
if "_default_manager" not in self.model_classdef.info.names:
default_manager_fullname = helpers.get_class_fullname(model_cls._meta.default_manager.__class__)
default_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(default_manager_fullname)
default_manager = Instance(default_manager_info, [Instance(self.model_classdef.info, [])])
self.add_new_node_to_model_class('_default_manager', default_manager)
self.add_new_node_to_model_class("_default_manager", default_manager)
class AddRelatedManagers(ModelClassInitializer):
@@ -272,8 +261,10 @@ class AddRelatedManagers(ModelClassInitializer):
if isinstance(relation, (ManyToOneRel, ManyToManyRel)):
try:
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS) # noqa: E501
if 'objects' not in related_model_info.names:
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(
fullnames.RELATED_MANAGER_CLASS
) # noqa: E501
if "objects" not in related_model_info.names:
raise helpers.IncompleteDefnException()
except helpers.IncompleteDefnException as exc:
if not self.api.final_iteration:
@@ -282,16 +273,17 @@ class AddRelatedManagers(ModelClassInitializer):
continue
# create new RelatedManager subclass
parametrized_related_manager_type = Instance(related_manager_info,
[Instance(related_model_info, [])])
default_manager_type = related_model_info.names['objects'].type
if (default_manager_type is None
parametrized_related_manager_type = Instance(related_manager_info, [Instance(related_model_info, [])])
default_manager_type = related_model_info.names["objects"].type
if (
default_manager_type is None
or not isinstance(default_manager_type, Instance)
or default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME):
or default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME
):
self.add_new_node_to_model_class(attname, parametrized_related_manager_type)
continue
name = related_model_cls.__name__ + '_' + 'RelatedManager'
name = related_model_cls.__name__ + "_" + "RelatedManager"
bases = [parametrized_related_manager_type, default_manager_type]
new_related_manager_info = self.add_new_class_for_current_module(name, bases)
@@ -303,45 +295,50 @@ class AddExtraFieldMethods(ModelClassInitializer):
# get_FOO_display for choices
for field in self.django_context.get_model_fields(model_cls):
if field.choices:
info = self.lookup_typeinfo_or_incomplete_defn_error('builtins.str')
info = self.lookup_typeinfo_or_incomplete_defn_error("builtins.str")
return_type = Instance(info, [])
common.add_method(self.ctx,
name='get_{}_display'.format(field.attname),
args=[],
return_type=return_type)
common.add_method(self.ctx, name=f"get_{field.attname}_display", args=[], return_type=return_type)
# get_next_by, get_previous_by for Date, DateTime
for field in self.django_context.get_model_fields(model_cls):
if isinstance(field, (DateField, DateTimeField)) and not field.null:
return_type = Instance(self.model_classdef.info, [])
common.add_method(self.ctx,
name='get_next_by_{}'.format(field.attname),
args=[Argument(Var('kwargs', AnyType(TypeOfAny.explicit)),
common.add_method(
self.ctx,
name=f"get_next_by_{field.attname}",
args=[
Argument(
Var("kwargs", AnyType(TypeOfAny.explicit)),
AnyType(TypeOfAny.explicit),
initializer=None,
kind=ARG_STAR2)],
return_type=return_type)
common.add_method(self.ctx,
name='get_previous_by_{}'.format(field.attname),
args=[Argument(Var('kwargs', AnyType(TypeOfAny.explicit)),
kind=ARG_STAR2,
)
],
return_type=return_type,
)
common.add_method(
self.ctx,
name=f"get_previous_by_{field.attname}",
args=[
Argument(
Var("kwargs", AnyType(TypeOfAny.explicit)),
AnyType(TypeOfAny.explicit),
initializer=None,
kind=ARG_STAR2)],
return_type=return_type)
kind=ARG_STAR2,
)
],
return_type=return_type,
)
class AddMetaOptionsAttribute(ModelClassInitializer):
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
if '_meta' not in self.model_classdef.info.names:
if "_meta" not in self.model_classdef.info.names:
options_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.OPTIONS_CLASS_FULLNAME)
self.add_new_node_to_model_class('_meta',
Instance(options_info, [
Instance(self.model_classdef.info, [])
]))
self.add_new_node_to_model_class("_meta", Instance(options_info, [Instance(self.model_classdef.info, [])]))
def process_model_class(ctx: ClassDefContext,
django_context: DjangoContext) -> None:
def process_model_class(ctx: ClassDefContext, django_context: DjangoContext) -> None:
initializers = [
InjectAnyAsBaseForNestedMeta,
AddDefaultPrimaryKey,

View File

@@ -24,21 +24,24 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext)
for lookup_kwarg, provided_type in zip(lookup_kwargs, provided_lookup_types):
if lookup_kwarg is None:
continue
if (isinstance(provided_type, Instance)
and provided_type.type.has_base('django.db.models.expressions.Combinable')):
if isinstance(provided_type, Instance) and provided_type.type.has_base(
"django.db.models.expressions.Combinable"
):
provided_type = resolve_combinable_type(provided_type, django_context)
lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
# Managers as provided_type is not supported yet
if (isinstance(provided_type, Instance)
and helpers.has_any_of_bases(provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME,
fullnames.QUERYSET_CLASS_FULLNAME))):
if isinstance(provided_type, Instance) and helpers.has_any_of_bases(
provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME, fullnames.QUERYSET_CLASS_FULLNAME)
):
return ctx.default_return_type
helpers.check_types_compatible(ctx,
helpers.check_types_compatible(
ctx,
expected_type=lookup_type,
actual_type=provided_type,
error_message=f'Incompatible type for lookup {lookup_kwarg!r}:')
error_message=f"Incompatible type for lookup {lookup_kwarg!r}:",
)
return ctx.default_return_type

View File

@@ -11,17 +11,17 @@ from mypy.types import AnyType, Instance
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny
from mypy_django_plugin.django.context import (
DjangoContext, LookupsAreUnsupported,
)
from mypy_django_plugin.django.context import DjangoContext, LookupsAreUnsupported
from mypy_django_plugin.lib import fullnames, helpers
def _extract_model_type_from_queryset(queryset_type: Instance) -> Optional[Instance]:
for base_type in [queryset_type, *queryset_type.type.bases]:
if (len(base_type.args)
if (
len(base_type.args)
and isinstance(base_type.args[0], Instance)
and base_type.args[0].type.has_base(fullnames.MODEL_CLASS_FULLNAME)):
and base_type.args[0].type.has_base(fullnames.MODEL_CLASS_FULLNAME)
):
return base_type.args[0]
return None
@@ -31,15 +31,15 @@ def determine_proper_manager_type(ctx: FunctionContext) -> MypyType:
assert isinstance(default_return_type, Instance)
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
if (outer_model_info is None
or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME)):
if outer_model_info is None or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME):
return default_return_type
return helpers.reparametrize_instance(default_return_type, [Instance(outer_model_info, [])])
def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model],
*, method: str, lookup: str) -> Optional[MypyType]:
def get_field_type_from_lookup(
ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model], *, method: str, lookup: str
) -> Optional[MypyType]:
try:
lookup_field = django_context.resolve_lookup_into_field(model_cls, lookup)
except FieldError as exc:
@@ -48,20 +48,21 @@ def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext
except LookupsAreUnsupported:
return AnyType(TypeOfAny.explicit)
if ((isinstance(lookup_field, RelatedField) and lookup_field.column == lookup)
or isinstance(lookup_field, ForeignObjectRel)):
if (isinstance(lookup_field, RelatedField) and lookup_field.column == lookup) or isinstance(
lookup_field, ForeignObjectRel
):
related_model_cls = django_context.get_field_related_model_cls(lookup_field)
if related_model_cls is None:
return AnyType(TypeOfAny.from_error)
lookup_field = django_context.get_primary_key_field(related_model_cls)
field_get_type = django_context.get_field_get_type(helpers.get_typechecker_api(ctx),
lookup_field, method=method)
field_get_type = django_context.get_field_get_type(helpers.get_typechecker_api(ctx), lookup_field, method=method)
return field_get_type
def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model],
flat: bool, named: bool) -> MypyType:
def get_values_list_row_type(
ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model], flat: bool, named: bool
) -> MypyType:
field_lookups = resolve_field_lookups(ctx.args[0], django_context)
if field_lookups is None:
return AnyType(TypeOfAny.from_error)
@@ -70,17 +71,17 @@ def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext,
if len(field_lookups) == 0:
if flat:
primary_key_field = django_context.get_primary_key_field(model_cls)
lookup_type = get_field_type_from_lookup(ctx, django_context, model_cls,
lookup=primary_key_field.attname, method='values_list')
lookup_type = get_field_type_from_lookup(
ctx, django_context, model_cls, lookup=primary_key_field.attname, method="values_list"
)
assert lookup_type is not None
return lookup_type
elif named:
column_types: 'OrderedDict[str, MypyType]' = OrderedDict()
column_types: "OrderedDict[str, MypyType]" = OrderedDict()
for field in django_context.get_model_fields(model_cls):
column_type = django_context.get_field_get_type(typechecker_api, field,
method='values_list')
column_type = django_context.get_field_get_type(typechecker_api, field, method="values_list")
column_types[field.attname] = column_type
return helpers.make_oneoff_named_tuple(typechecker_api, 'Row', column_types)
return helpers.make_oneoff_named_tuple(typechecker_api, "Row", column_types)
else:
# flat=False, named=False, all fields
field_lookups = []
@@ -93,8 +94,9 @@ def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext,
column_types = OrderedDict()
for field_lookup in field_lookups:
lookup_field_type = get_field_type_from_lookup(ctx, django_context, model_cls,
lookup=field_lookup, method='values_list')
lookup_field_type = get_field_type_from_lookup(
ctx, django_context, model_cls, lookup=field_lookup, method="values_list"
)
if lookup_field_type is None:
return AnyType(TypeOfAny.from_error)
column_types[field_lookup] = lookup_field_type
@@ -103,7 +105,7 @@ def get_values_list_row_type(ctx: MethodContext, django_context: DjangoContext,
assert len(column_types) == 1
row_type = next(iter(column_types.values()))
elif named:
row_type = helpers.make_oneoff_named_tuple(typechecker_api, 'Row', column_types)
row_type = helpers.make_oneoff_named_tuple(typechecker_api, "Row", column_types)
else:
row_type = helpers.make_tuple(typechecker_api, list(column_types.values()))
@@ -123,13 +125,13 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context:
if model_cls is None:
return ctx.default_return_type
flat_expr = helpers.get_call_argument_by_name(ctx, 'flat')
flat_expr = helpers.get_call_argument_by_name(ctx, "flat")
if flat_expr is not None and isinstance(flat_expr, NameExpr):
flat = helpers.parse_bool(flat_expr)
else:
flat = False
named_expr = helpers.get_call_argument_by_name(ctx, 'named')
named_expr = helpers.get_call_argument_by_name(ctx, "named")
if named_expr is not None and isinstance(named_expr, NameExpr):
named = helpers.parse_bool(named_expr)
else:
@@ -143,8 +145,7 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context:
flat = flat or False
named = named or False
row_type = get_values_list_row_type(ctx, django_context, model_cls,
flat=flat, named=named)
row_type = get_values_list_row_type(ctx, django_context, model_cls, flat=flat, named=named)
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, row_type])
@@ -179,10 +180,11 @@ def extract_proper_type_queryset_values(ctx: MethodContext, django_context: Djan
for field in django_context.get_model_fields(model_cls):
field_lookups.append(field.attname)
column_types: 'OrderedDict[str, MypyType]' = OrderedDict()
column_types: "OrderedDict[str, MypyType]" = OrderedDict()
for field_lookup in field_lookups:
field_lookup_type = get_field_type_from_lookup(ctx, django_context, model_cls,
lookup=field_lookup, method='values')
field_lookup_type = get_field_type_from_lookup(
ctx, django_context, model_cls, lookup=field_lookup, method="values"
)
if field_lookup_type is None:
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, AnyType(TypeOfAny.from_error)])

View File

@@ -13,8 +13,7 @@ def get_user_model_hook(ctx: FunctionContext, django_context: DjangoContext) ->
model_cls = django_context.apps_registry.get_model(auth_user_model)
model_cls_fullname = helpers.get_class_fullname(model_cls)
model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx),
model_cls_fullname)
model_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), model_cls_fullname)
if model_info is None:
return AnyType(TypeOfAny.unannotated)
@@ -32,7 +31,7 @@ def get_type_of_settings_attribute(ctx: AttributeContext, django_context: Django
# first look for the setting in the project settings file, then global settings
settings_module = typechecker_api.modules.get(django_context.django_settings_module)
global_settings_module = typechecker_api.modules.get('django.conf.global_settings')
global_settings_module = typechecker_api.modules.get("django.conf.global_settings")
for module in [settings_module, global_settings_module]:
if module is not None:
sym = module.names.get(setting_name)

View File

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

View File

@@ -1,11 +1,10 @@
[pytest]
testpaths =
./test-plugin
./test-data
./tests
addopts =
--tb=native
-s
-v
--cache-clear
--mypy-ini-file=./test-data/plugins.ini
--mypy-ini-file=./tests/plugins.ini
--mypy-extension-hook=scripts.tests_extension_hook.django_plugin_hook

View File

@@ -1,3 +0,0 @@
#!/usr/local/bin/xonsh
black --line-length=120 django-stubs/

17
release.sh Executable file
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

@@ -1,11 +0,0 @@
#!/usr/local/bin/xonsh
try:
no_uncommitted_changes = bool(!(git diff-index --quiet HEAD --))
if no_uncommitted_changes:
pip install wheel twine
python setup.py sdist bdist_wheel
twine upload dist/*
finally:
rm -rf dist/ build/

View File

@@ -1,28 +0,0 @@
import os
from pathlib import Path
from typing import List
STUBS_ROOT = Path(__file__).parent.parent / 'django-stubs'
def build_package_name(path: str) -> str:
return '.'.join(['django'] + list(Path(path).relative_to(STUBS_ROOT).with_suffix('').parts))
packages: List[str] = []
for dirpath, dirnames, filenames in os.walk(STUBS_ROOT):
if not dirnames:
package = build_package_name(dirpath)
packages.append(package)
for filename in filenames:
if filename != '__init__.pyi':
package = build_package_name(os.path.join(dirpath, filename))
packages.append(package)
test_lines: List[str] = []
for package in packages:
test_lines.append('import ' + package)
test_contents = '\n'.join(sorted(test_lines))
print(test_contents)

View File

@@ -1,88 +0,0 @@
import os
from typing import Optional
import libcst
from libcst import Annotation, BaseExpression, FunctionDef, Name, Subscript
from libcst.metadata import SyntacticPositionProvider
BASE_DIR = 'django-stubs'
fpath = os.path.join(BASE_DIR, 'core', 'checks', 'model_checks.pyi')
with open(fpath, 'r') as f:
contents = f.read()
tree = libcst.parse_module(contents)
class TypeAnnotationsAnalyzer(libcst.CSTVisitor):
METADATA_DEPENDENCIES = (SyntacticPositionProvider,)
def __init__(self, fpath: str):
super().__init__()
self.fpath = fpath
def get_node_location(self, node: FunctionDef) -> str:
start_line = self.get_metadata(SyntacticPositionProvider, node).start.line
return f'{self.fpath}:{start_line}'
def show_error_for_node(self, node: FunctionDef, error_message: str):
print(self.get_node_location(node), error_message)
def check_subscripted_annotation(self, annotation: BaseExpression) -> Optional[str]:
if isinstance(annotation, Subscript):
if isinstance(annotation.value, Name):
error_message = self.check_concrete_class_usage(annotation.value)
if error_message:
return error_message
if annotation.value.value == 'Union':
for slice_param in annotation.slice:
if isinstance(slice_param.slice.value, Name):
error_message = self.check_concrete_class_usage(annotation.value)
if error_message:
return error_message
def check_concrete_class_usage(self, name_node: Name) -> Optional[str]:
if name_node.value == 'List':
return (f'Concrete class {name_node.value!r} used for an iterable annotation. '
f'Use abstract collection (Iterable, Collection, Sequence) instead')
def visit_FunctionDef(self, node: FunctionDef) -> Optional[bool]:
params_node = node.params
for param_node in [*params_node.params, *params_node.default_params]:
param_name = param_node.name.value
annotation_node = param_node.annotation # type: Annotation
if annotation_node is not None:
annotation = annotation_node.annotation
if annotation.value == 'None':
self.show_error_for_node(node, f'"None" type annotation used for parameter {param_name!r}')
continue
error_message = self.check_subscripted_annotation(annotation)
if error_message is not None:
self.show_error_for_node(node, error_message)
continue
if node.returns is not None:
return_annotation = node.returns.annotation
if isinstance(return_annotation, Subscript) and return_annotation.value.value == 'Union':
self.show_error_for_node(node, 'Union is return type annotation')
return False
for dirpath, dirnames, filenames in os.walk(BASE_DIR):
for filename in filenames:
fpath = os.path.join(dirpath, filename)
# skip all other checks for now, low priority
if not fpath.startswith(('django-stubs/db', 'django-stubs/views', 'django-stubs/apps',
'django-stubs/http', 'django-stubs/contrib/postgres')):
continue
with open(fpath, 'r') as f:
contents = f.read()
tree = libcst.MetadataWrapper(libcst.parse_module(contents))
analyzer = TypeAnnotationsAnalyzer(fpath)
tree.visit(analyzer)

View File

@@ -1,233 +1,237 @@
import os
import django
SECRET_KEY = '1'
SECRET_KEY = "1"
SITE_ID = 1
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sites',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.staticfiles',
"django.contrib.contenttypes",
"django.contrib.auth",
"django.contrib.sites",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.admin.apps.SimpleAdminConfig",
"django.contrib.staticfiles",
]
test_modules = [
'absolute_url_overrides',
'admin_autodiscover',
'admin_changelist',
'admin_checks',
'admin_custom_urls',
'admin_default_site',
'admin_docs',
'admin_filters',
'admin_inlines',
'admin_ordering',
'admin_registration',
'admin_scripts',
'admin_utils',
'admin_views',
'admin_widgets',
'aggregation',
'aggregation_regress',
'annotations',
'app_loading',
'apps',
'auth_tests',
'backends',
'base',
'bash_completion',
'basic',
'builtin_server',
'bulk_create',
'cache',
'check_framework',
'conditional_processing',
'constraints',
'contenttypes_tests',
'context_processors',
'csrf_tests',
'custom_columns',
'custom_lookups',
'custom_managers',
'custom_methods',
'custom_migration_operations',
'custom_pk',
'datatypes',
'dates',
'datetimes',
'db_functions',
'db_typecasts',
'db_utils',
'dbshell',
'decorators',
'defer',
'defer_regress',
'delete',
'delete_regress',
'deprecation',
'dispatch',
'distinct_on_fields',
'empty',
'expressions',
'expressions_case',
'expressions_window',
'extra_regress',
'field_deconstruction',
'field_defaults',
'field_subclassing',
'file_storage',
'file_uploads',
'files',
'filtered_relation',
'fixtures',
'fixtures_model_package',
'fixtures_regress',
'flatpages_tests',
'force_insert_update',
'foreign_object',
'forms_tests',
'from_db_value',
'generic_inline_admin',
'generic_relations',
'generic_relations_regress',
'generic_views',
'get_earliest_or_latest',
'get_object_or_404',
'get_or_create',
'gis_tests',
'handlers',
'httpwrappers',
'humanize_tests',
'i18n',
'import_error_package',
'indexes',
'inline_formsets',
'inspectdb',
'introspection',
'invalid_models_tests',
'known_related_objects',
'logging_tests',
'lookup',
'm2m_and_m2o',
'm2m_intermediary',
'm2m_multiple',
'm2m_recursive',
'm2m_regress',
'm2m_signals',
'm2m_through',
'm2m_through_regress',
'm2o_recursive',
'mail',
'managers_regress',
'many_to_many',
'many_to_one',
'many_to_one_null',
'max_lengths',
'messages_tests',
'middleware',
'middleware_exceptions',
'migrate_signals',
'migration_test_data_persistence',
'migrations',
'migrations2',
'model_fields',
'model_forms',
'model_formsets',
'model_formsets_regress',
'model_indexes',
'model_inheritance',
'model_inheritance_regress',
'model_meta',
'model_options',
'model_package',
'model_regress',
'modeladmin',
'multiple_database',
'mutually_referential',
'nested_foreign_keys',
'no_models',
'null_fk',
'null_fk_ordering',
'null_queries',
'one_to_one',
'or_lookups',
'order_with_respect_to',
'ordering',
'pagination',
'postgres_tests',
'prefetch_related',
'project_template',
'properties',
'proxy_model_inheritance',
'proxy_models',
'queries',
'queryset_pickle',
'raw_query',
'redirects_tests',
'requests',
'reserved_names',
'resolve_url',
'responses',
'reverse_lookup',
'save_delete_hooks',
'schema',
'select_for_update',
'select_related',
'select_related_onetoone',
'select_related_regress',
'serializers',
'servers',
'sessions_tests',
'settings_tests',
'shell',
'shortcuts',
'signals',
'signed_cookies_tests',
'signing',
'sitemaps_tests',
'sites_framework',
'sites_tests',
'staticfiles_tests',
'str',
'string_lookup',
'swappable_models',
'syndication_tests',
'template_backends',
'template_loader',
'template_tests',
'test_client',
'test_client_regress',
'test_exceptions',
'test_runner',
'test_runner_apps',
'test_utils',
'timezones',
'transaction_hooks',
'transactions',
'unmanaged_models',
'update',
'update_only_fields',
'urlpatterns',
'urlpatterns_reverse',
'user_commands',
'utils_tests',
'validation',
'validators',
'version',
'view_tests',
'wsgi',
"absolute_url_overrides",
"admin_autodiscover",
"admin_changelist",
"admin_checks",
"admin_custom_urls",
"admin_default_site",
"admin_docs",
"admin_filters",
"admin_inlines",
"admin_ordering",
"admin_registration",
"admin_scripts",
"admin_utils",
"admin_views",
"admin_widgets",
"aggregation",
"aggregation_regress",
"annotations",
"app_loading",
"apps",
"auth_tests",
"backends",
"base",
"bash_completion",
"basic",
"builtin_server",
"bulk_create",
"cache",
"check_framework",
"conditional_processing",
"constraints",
"contenttypes_tests",
"context_processors",
"csrf_tests",
"custom_columns",
"custom_lookups",
"custom_managers",
"custom_methods",
"custom_migration_operations",
"custom_pk",
"datatypes",
"dates",
"datetimes",
"db_functions",
"db_typecasts",
"db_utils",
"dbshell",
"decorators",
"defer",
"defer_regress",
"delete",
"delete_regress",
"deprecation",
"dispatch",
"distinct_on_fields",
"empty",
"expressions",
"expressions_case",
"expressions_window",
"extra_regress",
"field_deconstruction",
"field_defaults",
"field_subclassing",
"file_storage",
"file_uploads",
"files",
"filtered_relation",
"fixtures",
"fixtures_model_package",
"fixtures_regress",
"flatpages_tests",
"force_insert_update",
"foreign_object",
"forms_tests",
"from_db_value",
"generic_inline_admin",
"generic_relations",
"generic_relations_regress",
"generic_views",
"get_earliest_or_latest",
"get_object_or_404",
"get_or_create",
"gis_tests",
"handlers",
"httpwrappers",
"humanize_tests",
"i18n",
"import_error_package",
"indexes",
"inline_formsets",
"inspectdb",
"introspection",
"invalid_models_tests",
"known_related_objects",
"logging_tests",
"lookup",
"m2m_and_m2o",
"m2m_intermediary",
"m2m_multiple",
"m2m_recursive",
"m2m_regress",
"m2m_signals",
"m2m_through",
"m2m_through_regress",
"m2o_recursive",
"mail",
"managers_regress",
"many_to_many",
"many_to_one",
"many_to_one_null",
"max_lengths",
"messages_tests",
"middleware",
"middleware_exceptions",
"migrate_signals",
"migration_test_data_persistence",
"migrations",
"migrations2",
"model_fields",
"model_forms",
"model_formsets",
"model_formsets_regress",
"model_indexes",
"model_inheritance",
"model_inheritance_regress",
"model_meta",
"model_options",
"model_package",
"model_regress",
"modeladmin",
"multiple_database",
"mutually_referential",
"nested_foreign_keys",
"no_models",
"null_fk",
"null_fk_ordering",
"null_queries",
"one_to_one",
"or_lookups",
"order_with_respect_to",
"ordering",
"pagination",
"postgres_tests",
"prefetch_related",
"project_template",
"properties",
"proxy_model_inheritance",
"proxy_models",
"queries",
"queryset_pickle",
"raw_query",
"redirects_tests",
"requests",
"reserved_names",
"resolve_url",
"responses",
"reverse_lookup",
"save_delete_hooks",
"schema",
"select_for_update",
"select_related",
"select_related_onetoone",
"select_related_regress",
"serializers",
"servers",
"sessions_tests",
"settings_tests",
"shell",
"shortcuts",
"signals",
"signed_cookies_tests",
"signing",
"sitemaps_tests",
"sites_framework",
"sites_tests",
"staticfiles_tests",
"str",
"string_lookup",
"swappable_models",
"syndication_tests",
"template_backends",
"template_loader",
"template_tests",
"test_client",
"test_client_regress",
"test_exceptions",
"test_runner",
"test_runner_apps",
"test_utils",
"timezones",
"transaction_hooks",
"transactions",
"unmanaged_models",
"update",
"update_only_fields",
"urlpatterns",
"urlpatterns_reverse",
"user_commands",
"utils_tests",
"validation",
"validators",
"version",
"view_tests",
"wsgi",
]
if django.VERSION[0] == 2:
test_modules += ['choices']
test_modules += ["choices"]
invalid_apps = {
'import_error_package',
"import_error_package",
}
for app in invalid_apps:
test_modules.remove(app)
if os.environ.get("TYPECHECK_TESTS"):
INSTALLED_APPS += test_modules

View File

@@ -2,27 +2,69 @@
# using this constant.
import re
IGNORED_MODULES = {'schema', 'gis_tests', 'admin_widgets', 'admin_filters',
'sitemaps_tests', 'staticfiles_tests', 'modeladmin',
'generic_views', 'forms_tests', 'flatpages_tests',
'admin_ordering', 'admin_changelist', 'admin_views',
'invalid_models_tests', 'i18n', 'model_formsets',
'template_tests', 'template_backends', 'test_runner', 'admin_scripts',
'inline_formsets', 'foreign_object', 'cache'}
IGNORED_MODULES = {
"schema",
"gis_tests",
"admin_widgets",
"admin_filters",
"sitemaps_tests",
"staticfiles_tests",
"modeladmin",
"generic_views",
"forms_tests",
"flatpages_tests",
"admin_ordering",
"admin_changelist",
"admin_views",
"invalid_models_tests",
"i18n",
"model_formsets",
"template_tests",
"template_backends",
"test_runner",
"admin_scripts",
"inline_formsets",
"foreign_object",
"cache",
}
MOCK_OBJECTS = ['MockRequest', 'MockCompiler', 'MockModelAdmin', 'modelz', 'call_count', 'call_args_list',
'call_args', 'MockUser', 'Xtemplate', 'DummyRequest', 'DummyUser', 'MinimalUser', 'DummyNode']
EXTERNAL_MODULES = ['psycopg2', 'PIL', 'selenium', 'oracle', 'mysql', 'sqlparse', 'tblib', 'numpy',
'bcrypt', 'argon2', 'xml.dom']
MOCK_OBJECTS = [
"MockRequest",
"MockCompiler",
"MockModelAdmin",
"modelz",
"call_count",
"call_args_list",
"call_args",
"MockUser",
"Xtemplate",
"DummyRequest",
"DummyUser",
"MinimalUser",
"DummyNode",
]
EXTERNAL_MODULES = [
"psycopg2",
"PIL",
"selenium",
"oracle",
"mysql",
"sqlparse",
"tblib",
"numpy",
"bcrypt",
"argon2",
"xml.dom",
]
IGNORED_ERRORS = {
'__common__': [
"__common__": [
*MOCK_OBJECTS,
*EXTERNAL_MODULES,
'Need type annotation for',
"Need type annotation for",
'has no attribute "getvalue"',
'Cannot assign to a method',
'already defined',
'Cannot assign to a type',
"Cannot assign to a method",
"already defined",
"Cannot assign to a type",
'"HttpResponseBase" has no attribute',
'"object" has no attribute',
re.compile(r'"Callable\[.+, Any\]" has no attribute'),
@@ -30,259 +72,245 @@ IGNORED_ERRORS = {
# private members
re.compile(r'has no attribute ("|\')_[a-zA-Z_]+("|\')'),
"'Settings' object has no attribute",
'**Dict',
"**Dict",
'has incompatible type "object"',
'undefined in superclass',
'Argument after ** must be a mapping',
'note:',
"undefined in superclass",
"Argument after ** must be a mapping",
"note:",
re.compile(r'Item "None" of "[a-zA-Z_ ,\[\]]+" has no attribute'),
'"Callable[..., None]" has no attribute',
'does not return a value',
"does not return a value",
'has no attribute "alternatives"',
'gets multiple values for keyword argument',
"gets multiple values for keyword argument",
'"Handler" has no attribute',
'Module has no attribute',
'namedtuple',
"Module has no attribute",
"namedtuple",
# TODO: see test in managers/test_managers.yml
"Cannot determine type of",
'cache_clear',
'cache_info',
"cache_clear",
"cache_info",
'Incompatible types in assignment (expression has type "None", variable has type Module)',
"Module 'django.contrib.messages.storage.fallback' has no attribute 'CookieStorage'",
# TODO: not supported yet
'GenericRelation',
'RelatedObjectDoesNotExist',
re.compile(r'"Field\[Any, Any\]" has no attribute '
"GenericRelation",
"RelatedObjectDoesNotExist",
re.compile(
r'"Field\[Any, Any\]" has no attribute '
r'"(through|field_name|field|get_related_field|related_model|related_name'
r'|get_accessor_name|empty_strings_allowed|many_to_many)"'),
r'|get_accessor_name|empty_strings_allowed|many_to_many)"'
),
# TODO: multitable inheritance
'ptr',
"ptr",
'Incompatible types in assignment (expression has type "Callable[',
'SimpleLazyObject',
'Test',
"SimpleLazyObject",
"Test",
'Mixin" has no attribute',
'Incompatible types in string interpolation',
"Incompatible types in string interpolation",
'"None" has no attribute',
'has no attribute "assert',
'Unsupported dynamic base class',
"Unsupported dynamic base class",
'error: "HttpResponse" has no attribute "streaming_content"',
'error: "HttpResponse" has no attribute "context_data"',
],
'admin_checks': [
'Argument 1 to "append" of "list" has incompatible type "str"; expected "CheckMessage"'
],
'admin_inlines': [
"admin_checks": ['Argument 1 to "append" of "list" has incompatible type "str"; expected "CheckMessage"'],
"admin_inlines": [
'error: "HttpResponse" has no attribute "rendered_content"',
],
'admin_utils': [
"admin_utils": [
'"Article" has no attribute "non_field"',
],
'aggregation': [
"aggregation": [
re.compile(r'got "Optional\[(Author|Publisher)\]", expected "Union\[(Author|Publisher), Combinable\]"'),
'Argument 2 for "super" not an instance of argument 1',
],
'annotations': [
"annotations": [
'Incompatible type for "store" of "Employee" (got "Optional[Store]", expected "Union[Store, Combinable]")'
],
'apps': [
"apps": [
'Incompatible types in assignment (expression has type "str", target has type "type")',
],
'auth_tests': [
"auth_tests": [
'"PasswordValidator" has no attribute "min_length"',
'AbstractBaseUser',
"AbstractBaseUser",
'Argument "password_validators" to "password_changed" has incompatible type "Tuple[Validator]"; '
+ 'expected "Optional[Sequence[PasswordValidator]]"',
'Unsupported right operand type for in ("object")',
'mock_getpass',
"mock_getpass",
'Unsupported left operand type for + ("Sequence[str]")',
'AuthenticationFormWithInactiveUsersOkay',
"AuthenticationFormWithInactiveUsersOkay",
'Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "QueryDict")',
'No overload variant of "int" matches argument type "AnonymousUser"',
'expression has type "AnonymousUser", variable has type "User"',
],
'basic': [
"basic": [
'Unexpected keyword argument "unknown_kwarg" for "refresh_from_db" of "Model"',
'Unexpected attribute "foo" for model "Article"',
'has no attribute "touched"',
'Incompatible types in assignment (expression has type "Type[CustomQuerySet]"',
'"Manager[Article]" has no attribute "do_something"',
],
'backends': [
'"DatabaseError" has no attribute "pgcode"'
],
'builtin_server': [
"backends": ['"DatabaseError" has no attribute "pgcode"'],
"builtin_server": [
'"ServerHandler" has no attribute',
'Incompatible types in assignment (expression has type "Tuple[BytesIO, BytesIO]"',
],
'bulk_create': [
"bulk_create": [
'has incompatible type "List[Country]"; expected "Iterable[TwoFields]"',
'List item 1 has incompatible type "Country"; expected "ProxyCountry"',
],
'check_framework': [
"check_framework": [
'base class "Model" defined the type as "Callable',
'Value of type "Collection[str]" is not indexable',
'Unsupported target for indexed assignment',
"Unsupported target for indexed assignment",
],
'constraints': [
'Argument "condition" to "UniqueConstraint" has incompatible type "str"; expected "Optional[Q]"'
],
'contenttypes_tests': [
"constraints": ['Argument "condition" to "UniqueConstraint" has incompatible type "str"; expected "Optional[Q]"'],
"contenttypes_tests": [
'"FooWithBrokenAbsoluteUrl" has no attribute "unknown_field"',
'contenttypes_tests.models.Site',
"contenttypes_tests.models.Site",
'Argument 1 to "set" of "RelatedManager" has incompatible type "SiteManager[Site]"',
],
'custom_lookups': [
"custom_lookups": [
'in base class "SQLFuncMixin"',
'has no attribute "name"',
],
'custom_columns': [
"custom_columns": [
"Cannot resolve keyword 'firstname' into field",
],
'custom_pk': [
"custom_pk": [
'"Employee" has no attribute "id"',
],
'custom_managers': [
"custom_managers": [
'Incompatible types in assignment (expression has type "CharField',
'Item "Book" of "Optional[Book]" has no attribute "favorite_avg"',
],
'csrf_tests': [
"csrf_tests": [
'expression has type "property", base class "HttpRequest" defined the type as "QueryDict"',
'expression has type "Dict[<nothing>, <nothing>]", variable has type "SessionBase"',
],
'dates': [
"dates": [
'Too few arguments for "dates" of',
],
'dbshell': [
"dbshell": [
'Incompatible types in assignment (expression has type "None"',
],
'defer': [
'Too many arguments for "refresh_from_db" of "Model"'
],
'delete': [
"defer": ['Too many arguments for "refresh_from_db" of "Model"'],
"delete": [
'Incompatible type for lookup \'pk\': (got "Optional[int]", expected "Union[str, int]")',
],
'dispatch': [
'Item "str" of "Union[ValueError, str]" has no attribute "args"'
],
'deprecation': [
'"Manager" has no attribute "old"',
'"Manager" has no attribute "new"'
],
'db_functions': [
"dispatch": ['Item "str" of "Union[ValueError, str]" has no attribute "args"'],
"deprecation": ['"Manager" has no attribute "old"', '"Manager" has no attribute "new"'],
"db_functions": [
'"FloatModel" has no attribute',
'Incompatible types in assignment (expression has type "Optional[Any]", variable has type "FloatModel")'
'Incompatible types in assignment (expression has type "Optional[Any]", variable has type "FloatModel")',
],
'decorators': [
"decorators": [
'"Type[object]" has no attribute "method"',
'Value of type variable "_T" of function cannot be "descriptor_wrapper"'
'Value of type variable "_T" of function cannot be "descriptor_wrapper"',
],
'expressions_window': [
'has incompatible type "str"'
],
'file_uploads': [
"expressions_window": ['has incompatible type "str"'],
"file_uploads": [
'has no attribute "content_type"',
],
'file_storage': [
"file_storage": [
'Incompatible types in assignment (expression has type "None", variable has type "str")',
'Property "base_url" defined in "FileSystemStorage" is read-only',
],
'files': [
"files": [
'Incompatible types in assignment (expression has type "IOBase", variable has type "File")',
'Argument 1 to "TextIOWrapper" has incompatible type "File"; expected "BinaryIO"',
'Incompatible types in assignment (expression has type "BinaryIO", variable has type "File")',
],
'filtered_relation': [
"filtered_relation": [
'has no attribute "name"',
],
'fixtures': [
"fixtures": [
'Incompatible types in assignment (expression has type "int", target has type "Iterable[str]")',
'Incompatible types in assignment (expression has type "SpyManager[Spy]"',
],
'fixtures_regress': [
"fixtures_regress": [
'Unsupported left operand type for + ("None")',
],
'from_db_value': [
"from_db_value": [
'"Cash" has no attribute',
'"__str__" of "Decimal"',
],
'get_object_or_404': [
"get_object_or_404": [
'Argument 1 to "get_object_or_404" has incompatible type "str"; '
+ 'expected "Union[Type[<nothing>], QuerySet[<nothing>]]"',
'Argument 1 to "get_list_or_404" has incompatible type "List[Type[Article]]"; '
+ 'expected "Union[Type[<nothing>], QuerySet[<nothing>]]"',
'CustomClass',
"CustomClass",
],
'generic_relations': [
"generic_relations": [
"Cannot resolve keyword 'vegetable' into field",
],
'generic_relations_regress': [
"generic_relations_regress": [
'"Link" has no attribute',
],
'httpwrappers': [
"httpwrappers": [
'Argument 2 to "appendlist" of "QueryDict"',
'Incompatible types in assignment (expression has type "int", target has type "Union[str, List[str]]")',
'Argument 1 to "fromkeys" of "QueryDict" has incompatible type "int"',
],
'humanize_tests': [
"humanize_tests": [
'Argument 1 to "append" of "list" has incompatible type "None"; expected "str"',
],
'lookup': [
"lookup": [
'Unexpected keyword argument "headline__startswith" for "in_bulk" of',
'is called with more than one field',
"is called with more than one field",
"Cannot resolve keyword 'pub_date_year' into field",
"Cannot resolve keyword 'blahblah' into field",
],
'logging_tests': [
"logging_tests": [
re.compile(r'Argument [0-9] to "makeRecord" of "Logger"'),
'"LogRecord" has no attribute "request"',
],
'm2m_regress': [
"m2m_regress": [
"Cannot resolve keyword 'porcupine' into field",
'Argument 1 to "set" of "RelatedManager" has incompatible type "int"',
],
'mail': [
"mail": [
'List item 1 has incompatible type "None"; expected "str"',
'Incompatible types in assignment '
"Incompatible types in assignment "
+ '(expression has type "bool", variable has type "Union[SMTP_SSL, SMTP, None]")',
],
'messages_tests': [
"messages_tests": [
'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"',
'Too many arguments',
'CustomRequest',
"Too many arguments",
"CustomRequest",
],
'middleware': [
"middleware": [
re.compile(r'"(HttpRequest|WSGIRequest)" has no attribute'),
'Incompatible types in assignment (expression has type "HttpResponseBase", variable has type "HttpResponse")',
],
'many_to_many': [
"many_to_many": [
'(expression has type "List[Article]", variable has type "Article_RelatedManager2',
'"add" of "RelatedManager" has incompatible type "Article"; expected "Union[Publication, int]"',
],
'many_to_one': [
"many_to_one": [
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")',
'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")',
'expression has type "List[<nothing>]", variable has type "RelatedManager[Article]"',
'"Reporter" has no attribute "cached_query"',
'to "add" of "RelatedManager" has incompatible type "Reporter"; expected "Union[Article, int]"',
],
'middleware_exceptions': [
"middleware_exceptions": [
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"'
],
'migrate_signals': [
"migrate_signals": [
'Value of type "Optional[Any]" is not indexable',
'Argument 1 to "set" has incompatible type "Optional[Any]"; expected "Iterable[Any]"',
],
'migrations': [
'FakeMigration',
'FakeLoader',
"migrations": [
"FakeMigration",
"FakeLoader",
'"Manager[Any]" has no attribute "args"',
'Dict entry 0 has incompatible type "Any"',
'Argument 1 to "append" of "list" has incompatible type',
'base class "Model" defined the type as "BaseManager[Any]"',
'Argument 1 to "RunPython" has incompatible type "str"',
],
'model_fields': [
"model_fields": [
'Item "Field[Any, Any]" of "Union[Field[Any, Any], ForeignObjectRel]" has no attribute',
'Incompatible types in assignment (expression has type "Type[Person',
'Incompatible types in assignment (expression has type "FloatModel", variable has type',
@@ -291,186 +319,168 @@ IGNORED_ERRORS = {
'Incompatible type for "value" of "IntegerModel" (got "object", expected',
'"Child" has no attribute "get_foo_display"',
],
'model_forms': [
"model_forms": [
'"render" of "Widget"',
"Module 'django.core.validators' has no attribute 'ValidationError'",
'Incompatible types in assignment',
'NewForm',
"Incompatible types in assignment",
"NewForm",
'"type" has no attribute "base_fields"',
'Argument "instance" to "InvalidModelForm" has incompatible type "Type[Category]"',
],
'model_indexes': [
'Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"'
],
'model_inheritance': [
"model_indexes": ['Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"'],
"model_inheritance": [
'base class "AbstractBase" defined',
'base class "AbstractModel" defined',
'Definition of "name" in base class "ConcreteParent"',
' Definition of "name" in base class "AbstractParent"',
'referent_references',
"Cannot resolve keyword 'attached_comment_set' into field"
"referent_references",
"Cannot resolve keyword 'attached_comment_set' into field",
],
'model_meta': [
'List item 0 has incompatible type "str"; expected "Union[Field[Any, Any], ForeignObjectRel]"'
],
'model_regress': [
"model_meta": ['List item 0 has incompatible type "str"; expected "Union[Field[Any, Any], ForeignObjectRel]"'],
"model_regress": [
'Incompatible type for "department" of "Worker"',
'"PickledModel" has no attribute',
'"Department" has no attribute "evaluate"',
'Unsupported target for indexed assignment',
"Unsupported target for indexed assignment",
],
'model_formsets_regress': [
"model_formsets_regress": [
'Incompatible types in assignment (expression has type "int", target has type "str")',
],
'model_options': [
"model_options": [
'expression has type "Dict[str, Type[Model]]", target has type "OrderedDict',
],
'model_enums': [
"model_enums": [
"'bool' is not a valid base class",
],
'null_queries': [
"Cannot resolve keyword 'foo' into field"
],
'order_with_respect_to': [
"null_queries": ["Cannot resolve keyword 'foo' into field"],
"order_with_respect_to": [
'"Dimension" has no attribute "set_component_order"',
],
'one_to_one': [
"one_to_one": [
'expression has type "None", variable has type "UndergroundBar"',
'Item "OneToOneField[Union[Place, Combinable], Place]" '
+ 'of "Union[OneToOneField[Union[Place, Combinable], Place], Any]"',
],
'pagination': [
"pagination": [
'"int" not callable',
],
'postgres_tests': [
'DummyArrayField',
'DummyJSONField',
"postgres_tests": [
"DummyArrayField",
"DummyJSONField",
'Incompatible types in assignment (expression has type "Type[Field[Any, Any]]',
'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder";',
'("None" and "SearchQuery")',
],
'properties': [
re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')
],
'prefetch_related': [
"properties": [re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')],
"prefetch_related": [
'"Person" has no attribute "houses_lst"',
'"Book" has no attribute "first_authors"',
'"Book" has no attribute "the_authors"',
'Incompatible types in assignment (expression has type "List[Room]", variable has type "Manager[Room]")',
'Item "Room" of "Optional[Room]" has no attribute "house_attr"',
'Item "Room" of "Optional[Room]" has no attribute "main_room_of_attr"',
'Argument 2 to "Prefetch" has incompatible type "ValuesQuerySet'
'Argument 2 to "Prefetch" has incompatible type "ValuesQuerySet',
],
'proxy_models': [
'Incompatible types in assignment',
'in base class "User"'
],
'queries': [
"proxy_models": ["Incompatible types in assignment", 'in base class "User"'],
"queries": [
'Incompatible types in assignment (expression has type "None", variable has type "str")',
'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"',
'Unsupported operand types for & ("Manager[Author]" and "Manager[Tag]")',
'Unsupported operand types for | ("Manager[Author]" and "Manager[Tag]")',
'ObjectA',
"ObjectA",
"'flat' and 'named' can't be used together",
'"Collection[Any]" has no attribute "explain"',
"Cannot resolve keyword 'unknown_field' into field",
'Incompatible type for lookup \'tag\': (got "str", expected "Union[Tag, int, None]")',
'No overload variant of "__getitem__" of "QuerySet" matches argument type "str"',
],
'requests': [
"requests": [
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
],
'responses': [
"responses": [
'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"',
'"FileLike" has no attribute "closed"',
'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "BinaryIO"',
],
'reverse_lookup': [
"Cannot resolve keyword 'choice' into field"
],
'settings_tests': [
'Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"'
],
'shortcuts': [
"reverse_lookup": ["Cannot resolve keyword 'choice' into field"],
"settings_tests": ['Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"'],
"shortcuts": [
'error: "Context" has no attribute "request"',
],
'signals': [
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]";'
],
'sites_framework': [
"signals": ['Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]";'],
"sites_framework": [
'expression has type "CurrentSiteManager[CustomArticle]", base class "AbstractArticle"',
"Name 'Optional' is not defined",
],
'sites_tests': [
"sites_tests": [
'"RequestSite" of "Union[Site, RequestSite]" has no attribute "id"',
],
'syndication_tests': [
"syndication_tests": [
'Argument 1 to "add_domain" has incompatible type "*Tuple[object, ...]"',
],
'sessions_tests': [
"sessions_tests": [
'Incompatible types in assignment (expression has type "None", variable has type "int")',
'"AbstractBaseSession" has no attribute',
'"None" not callable',
],
'select_related': [
"select_for_update": ['"Thread" has no attribute "isAlive"'],
"select_related": [
'Item "ForeignKey[Union[Genus, Combinable], Genus]" '
+ 'of "Union[ForeignKey[Union[Genus, Combinable], Genus], Any]"'
],
'select_related_onetoone': [
"select_related_onetoone": [
'Incompatible types in assignment (expression has type "Parent2", variable has type "Parent1")',
'"Parent1" has no attribute'
'"Parent1" has no attribute',
],
'servers': [
"servers": [
re.compile('Argument [0-9] to "WSGIRequestHandler"'),
'"HTTPResponse" has no attribute',
'"type" has no attribute',
'"WSGIRequest" has no attribute "makefile"',
'LiveServerAddress',
"LiveServerAddress",
'"Stub" has no attribute "makefile"',
],
'serializers': [
"serializers": [
'"Model" has no attribute "data"',
'"Iterable[Any]" has no attribute "content"',
re.compile(r'Argument 1 to "(serialize|deserialize)" has incompatible type "None"; expected "str"')
re.compile(r'Argument 1 to "(serialize|deserialize)" has incompatible type "None"; expected "str"'),
],
'string_lookup': [
"string_lookup": [
'"Bar" has no attribute "place"',
],
'test_utils': [
"test_utils": [
'"PossessedCar" has no attribute "color"',
'expression has type "None", variable has type "List[str]"',
],
'test_client': [
'(expression has type "HttpResponse", variable has type "StreamingHttpResponse")'
],
'test_client_regress': [
"test_client": ['(expression has type "HttpResponse", variable has type "StreamingHttpResponse")'],
"test_client_regress": [
'(expression has type "Dict[<nothing>, <nothing>]", variable has type "SessionBase")',
'Unsupported left operand type for + ("None")',
'Argument 1 to "len" has incompatible type "Context"; expected "Sized"',
],
'transactions': [
"transactions": [
'Incompatible types in assignment (expression has type "Thread", variable has type "Callable[[], Any]")'
],
'urlpatterns': [
"urlpatterns": [
'"object" not callable',
'"None" not callable',
'Argument 2 to "path" has incompatible type "Callable[[Any], None]"',
'Incompatible return value type (got "None", expected "HttpResponseBase")',
],
'urlpatterns_reverse': [
"urlpatterns_reverse": [
'No overload variant of "path" matches argument types "str", "None"',
'No overload variant of "zip" matches argument types "Any", "object"',
'Argument 1 to "get_callable" has incompatible type "int"'
'Argument 1 to "get_callable" has incompatible type "int"',
],
'utils_tests': [
"utils_tests": [
'Argument 1 to "activate" has incompatible type "None"; expected "Union[tzinfo, str]"',
'Argument 1 to "activate" has incompatible type "Optional[str]"; expected "str"',
'Incompatible types in assignment (expression has type "None", base class "object" defined the type as',
'Class',
"Class",
'has no attribute "cp"',
'Argument "name" to "cached_property" has incompatible type "int"; expected "Optional[str]"',
'has no attribute "sort"',
'Unsupported target for indexed assignment',
"Unsupported target for indexed assignment",
'defined the type as "None"',
'Argument 1 to "Path" has incompatible type "Optional[str]"',
'"None" not callable',
@@ -480,18 +490,18 @@ IGNORED_ERRORS = {
'Argument 1 to "to_path" has incompatible type "int"; expected "Union[Path, str]"',
'Cannot infer type argument 1 of "cached_property"',
],
'view_tests': [
"view_tests": [
"Module 'django.views.debug' has no attribute 'Path'",
'Value of type "Optional[List[str]]" is not indexable',
'ExceptionUser',
'view_tests.tests.test_debug.User',
'Exception must be derived from BaseException',
"ExceptionUser",
"view_tests.tests.test_debug.User",
"Exception must be derived from BaseException",
"No binding for nonlocal 'tb_frames' found",
],
'validation': [
"validation": [
'has no attribute "name"',
],
'wsgi': [
"wsgi": [
'"HttpResponse" has no attribute "block_size"',
],
}
@@ -500,9 +510,9 @@ IGNORED_ERRORS = {
def check_if_custom_ignores_are_covered_by_common() -> None:
from scripts.typecheck_tests import is_pattern_fits
common_ignore_patterns = IGNORED_ERRORS['__common__']
common_ignore_patterns = IGNORED_ERRORS["__common__"]
for module_name, patterns in IGNORED_ERRORS.items():
if module_name == '__common__':
if module_name == "__common__":
continue
for pattern in patterns:
for common_pattern in common_ignore_patterns:

31
scripts/git_helpers.py Normal file
View File

@@ -0,0 +1,31 @@
import shutil
from typing import Optional
from git import RemoteProgress, Repo
from scripts.paths import DJANGO_SOURCE_DIRECTORY
class ProgressPrinter(RemoteProgress):
def line_dropped(self, line: str) -> None:
print(line)
def update(self, op_code, cur_count, max_count=None, message=""):
print(self._cur_line)
def checkout_django_branch(django_version: str, commit_sha: Optional[str]) -> Repo:
branch = f"stable/{django_version}.x"
if DJANGO_SOURCE_DIRECTORY.exists():
shutil.rmtree(DJANGO_SOURCE_DIRECTORY)
DJANGO_SOURCE_DIRECTORY.mkdir(exist_ok=True, parents=False)
repo = Repo.clone_from(
"https://github.com/django/django.git",
DJANGO_SOURCE_DIRECTORY,
progress=ProgressPrinter(),
branch=branch,
depth=100,
)
if commit_sha and repo.head.commit.hexsha != commit_sha:
repo.remote("origin").fetch(branch, progress=ProgressPrinter(), depth=100)
repo.git.checkout(commit_sha)

View File

@@ -1,14 +0,0 @@
[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'

4
scripts/paths.py Normal file
View File

@@ -0,0 +1,4 @@
from pathlib import Path
PROJECT_DIRECTORY = Path(__file__).parent.parent
DJANGO_SOURCE_DIRECTORY = PROJECT_DIRECTORY / "django-source" # type: Path

15
scripts/stubgen-django.py Normal file
View File

@@ -0,0 +1,15 @@
from argparse import ArgumentParser
from mypy.stubgen import generate_stubs, parse_options
from scripts.git_helpers import checkout_django_branch
from scripts.paths import DJANGO_SOURCE_DIRECTORY
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("--django_version", required=True)
parser.add_argument("--commit_sha", required=False)
args = parser.parse_args()
checkout_django_branch(args.django_version, args.commit_sha)
stubgen_options = parse_options([f"{DJANGO_SOURCE_DIRECTORY}", "-o=stubgen"])
generate_stubs(stubgen_options)

View File

@@ -3,28 +3,27 @@ from pytest_mypy_plugins.item import YamlTestItem
def django_plugin_hook(test_item: YamlTestItem) -> None:
custom_settings = test_item.parsed_test_data.get('custom_settings', '')
installed_apps = test_item.parsed_test_data.get('installed_apps', None)
custom_settings = test_item.parsed_test_data.get("custom_settings", "")
installed_apps = test_item.parsed_test_data.get("installed_apps", None)
if installed_apps and custom_settings:
raise ValueError('"installed_apps" and "custom_settings" are not compatible, please use one or the other')
if installed_apps is not None:
# custom_settings is empty, add INSTALLED_APPS
installed_apps += ['django.contrib.contenttypes']
installed_apps_as_str = '(' + ','.join([repr(app) for app in installed_apps]) + ',)'
custom_settings += 'INSTALLED_APPS = ' + installed_apps_as_str
installed_apps += ["django.contrib.contenttypes"]
installed_apps_as_str = "(" + ",".join([repr(app) for app in installed_apps]) + ",)"
custom_settings += "INSTALLED_APPS = " + installed_apps_as_str
if 'SECRET_KEY' not in custom_settings:
if "SECRET_KEY" not in custom_settings:
custom_settings = 'SECRET_KEY = "1"\n' + custom_settings
django_settings_section = "\n[mypy.plugins.django-stubs]\n" \
"django_settings_module = mysettings"
django_settings_section = "\n[mypy.plugins.django-stubs]\n" "django_settings_module = mysettings"
if not test_item.additional_mypy_config:
test_item.additional_mypy_config = django_settings_section
else:
if '[mypy.plugins.django-stubs]' not in test_item.additional_mypy_config:
if "[mypy.plugins.django-stubs]" not in test_item.additional_mypy_config:
test_item.additional_mypy_config += django_settings_section
mysettings_file = File(path='mysettings.py', content=custom_settings)
mysettings_file = File(path="mysettings.py", content=custom_settings)
test_item.files.append(mysettings_file)

View File

@@ -4,30 +4,27 @@ import subprocess
import sys
from argparse import ArgumentParser
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Pattern, Tuple, Union
from distutils import spawn
from typing import Dict, List, Pattern, Union
from git import RemoteProgress, Repo
from scripts.enabled_test_modules import EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS
from scripts.git_helpers import checkout_django_branch
from scripts.paths import DJANGO_SOURCE_DIRECTORY, PROJECT_DIRECTORY
from scripts.enabled_test_modules import (
EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS,
)
DJANGO_COMMIT_REFS: Dict[str, Tuple[str, str]] = {
'2.2': ('stable/2.2.x', '8093aaa8ff9dd7386a069c6eb49fcc1c5980c033'),
'3.0': ('stable/3.0.x', '44da7abda848f05caaed74f6a749038c87dedfda')
DJANGO_COMMIT_REFS: Dict[str, str] = {
"2.2": "8093aaa8ff9dd7386a069c6eb49fcc1c5980c033",
"3.0": "44da7abda848f05caaed74f6a749038c87dedfda",
}
PROJECT_DIRECTORY = Path(__file__).parent.parent
DJANGO_SOURCE_DIRECTORY = PROJECT_DIRECTORY / 'django-sources' # type: Path
def get_unused_ignores(ignored_message_freq: Dict[str, Dict[Union[str, Pattern], int]]) -> List[str]:
unused_ignores = []
for root_key, patterns in IGNORED_ERRORS.items():
for pattern in patterns:
if (ignored_message_freq[root_key][pattern] == 0
and pattern not in itertools.chain(EXTERNAL_MODULES, MOCK_OBJECTS)):
unused_ignores.append(f'{root_key}: {pattern}')
if ignored_message_freq[root_key][pattern] == 0 and pattern not in itertools.chain(
EXTERNAL_MODULES, MOCK_OBJECTS
):
unused_ignores.append(f"{root_key}: {pattern}")
return unused_ignores
@@ -42,7 +39,7 @@ def is_pattern_fits(pattern: Union[Pattern, str], line: str):
def is_ignored(line: str, test_folder_name: str, *, ignored_message_freqs: Dict[str, Dict[str, int]]) -> bool:
if 'runtests' in line:
if "runtests" in line:
return True
if test_folder_name in IGNORED_MODULES:
@@ -53,83 +50,42 @@ def is_ignored(line: str, test_folder_name: str, *, ignored_message_freqs: Dict[
ignored_message_freqs[test_folder_name][pattern] += 1
return True
for pattern in IGNORED_ERRORS['__common__']:
for pattern in IGNORED_ERRORS["__common__"]:
if is_pattern_fits(pattern, line):
ignored_message_freqs['__common__'][pattern] += 1
ignored_message_freqs["__common__"][pattern] += 1
return True
return False
def replace_with_clickable_location(error: str, abs_test_folder: Path) -> str:
raw_path, _, error_line = error.partition(': ')
fname, _, line_number = raw_path.partition(':')
try:
path = abs_test_folder.joinpath(fname).relative_to(PROJECT_DIRECTORY)
except ValueError:
# fail on travis, just show an error
return error
clickable_location = f'./{path}:{line_number or 1}'
return error.replace(raw_path, clickable_location)
class ProgressPrinter(RemoteProgress):
def line_dropped(self, line: str) -> None:
print(line)
def update(self, op_code, cur_count, max_count=None, message=''):
print(self._cur_line)
def get_django_repo_object(branch: str) -> Repo:
if not DJANGO_SOURCE_DIRECTORY.exists():
DJANGO_SOURCE_DIRECTORY.mkdir(exist_ok=True, parents=False)
return Repo.clone_from('https://github.com/django/django.git', DJANGO_SOURCE_DIRECTORY,
progress=ProgressPrinter(),
branch=branch,
depth=100)
else:
repo = Repo(DJANGO_SOURCE_DIRECTORY)
return repo
if __name__ == '__main__':
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument('--django_version', choices=['2.2', '3.0'], required=True)
args = parser.parse_args()
# install proper Django version
subprocess.check_call([sys.executable, '-m', 'pip', 'install', f'Django=={args.django_version}.*'])
branch, commit_sha = DJANGO_COMMIT_REFS[args.django_version]
repo = get_django_repo_object(branch)
if repo.head.commit.hexsha != commit_sha:
repo.remote('origin').fetch(branch, progress=ProgressPrinter(), depth=100)
repo.git.checkout(commit_sha)
mypy_config_file = (PROJECT_DIRECTORY / 'scripts' / 'mypy.ini').absolute()
mypy_cache_dir = Path(__file__).parent / '.mypy_cache'
tests_root = DJANGO_SOURCE_DIRECTORY / 'tests'
parser.add_argument("--django_version", default="3.0")
django_version = parser.parse_args().django_version
subprocess.check_call([sys.executable, "-m", "pip", "install", f"Django=={django_version}.*"])
commit_sha = DJANGO_COMMIT_REFS[django_version]
repo = checkout_django_branch(django_version, commit_sha)
mypy_config_file = (PROJECT_DIRECTORY / "mypy.ini").absolute()
mypy_cache_dir = PROJECT_DIRECTORY / ".mypy_cache"
tests_root = DJANGO_SOURCE_DIRECTORY / "tests"
global_rc = 0
try:
mypy_options = ['--cache-dir', str(mypy_config_file.parent / '.mypy_cache'),
'--config-file', str(mypy_config_file),
'--show-traceback',
'--no-error-summary',
'--hide-error-context'
mypy_options = [
"--cache-dir",
str(mypy_cache_dir),
"--config-file",
str(mypy_config_file),
"--show-traceback",
"--no-error-summary",
"--hide-error-context",
]
mypy_options += [str(tests_root)]
import distutils.spawn
mypy_executable = distutils.spawn.find_executable('mypy')
mypy_executable = spawn.find_executable("mypy")
mypy_argv = [mypy_executable, *mypy_options]
completed = subprocess.run(
mypy_argv,
env={'PYTHONPATH': str(tests_root)},
env={"PYTHONPATH": str(tests_root), "TYPECHECK_TESTS": "1"},
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
@@ -140,20 +96,19 @@ if __name__ == '__main__':
sorted_lines = sorted(output.splitlines())
for line in sorted_lines:
try:
path_to_error = line.split(':')[0]
test_folder_name = path_to_error.split('/')[2]
path_to_error = line.split(":")[0]
test_folder_name = path_to_error.split("/")[2]
except IndexError:
test_folder_name = 'unknown'
test_folder_name = "unknown"
if not is_ignored(line, test_folder_name,
ignored_message_freqs=ignored_message_freqs):
if not is_ignored(line, test_folder_name, ignored_message_freqs=ignored_message_freqs):
global_rc = 1
print(line)
unused_ignores = get_unused_ignores(ignored_message_freqs)
if unused_ignores:
print('UNUSED IGNORES ------------------------------------------------')
print('\n'.join(unused_ignores))
print("UNUSED IGNORES ------------------------------------------------")
print("\n".join(unused_ignores))
sys.exit(global_rc)

View File

@@ -1,19 +1,11 @@
[isort]
skip =
django-sources
django-stubs
test-data
include_trailing_comma = true
multi_line_output = 5
wrap_length = 120
known_first_party = mypy_django_plugin
[flake8]
exclude =
django-sources
django-stubs
test-data
exclude = .*/
select = F401, Y
max_line_length = 120
per-file-ignores =
*__init__.pyi: F401
base_user.pyi: Y003
models.pyi: Y003
[metadata]
license_file = LICENSE.txt

View File

@@ -17,7 +17,7 @@ def find_stub_files(name: str) -> List[str]:
return result
with open("README.md", "r") as f:
with open("README.md") as f:
readme = f.read()
dependencies = [
@@ -42,15 +42,17 @@ setup(
packages=["django-stubs", *find_packages(exclude=["scripts"])],
package_data={"django-stubs": find_stub_files("django-stubs")},
classifiers=[
"Development Status :: 3 - Alpha",
"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",
"Typing :: Typed",
"Framework :: Django :: 3.1",
],
project_urls={
"Release notes": "https://github.com/typeddjango/django-stubs/releases",

View File

@@ -15,42 +15,41 @@ TEMPLATE = """usage: (config)
@pytest.mark.parametrize(
'config_file_contents,message_part',
"config_file_contents,message_part",
[
pytest.param(
None,
'mypy config file is not specified or found',
id='missing-file',
"mypy config file is not specified or found",
id="missing-file",
),
pytest.param(
['[not-really-django-stubs]'],
'no section [mypy.plugins.django-stubs]',
id='missing-section',
["[not-really-django-stubs]"],
"no section [mypy.plugins.django-stubs]",
id="missing-section",
),
pytest.param(
['[mypy.plugins.django-stubs]',
'\tnot_django_not_settings_module = badbadmodule'],
'the setting is not provided',
id='missing-settings-module',
["[mypy.plugins.django-stubs]", "\tnot_django_not_settings_module = badbadmodule"],
"the setting is not provided",
id="missing-settings-module",
),
pytest.param(
['[mypy.plugins.django-stubs]'],
'the setting is not provided',
id='no-settings-given',
["[mypy.plugins.django-stubs]"],
"the setting is not provided",
id="no-settings-given",
),
],
)
def test_misconfiguration_handling(capsys, config_file_contents, message_part):
# type: (typing.Any, typing.List[str], str) -> None
"""Invalid configuration raises `SystemExit` with a precise error message."""
with tempfile.NamedTemporaryFile(mode='w+') as config_file:
with tempfile.NamedTemporaryFile(mode="w+") as config_file:
if not config_file_contents:
config_file.close()
else:
config_file.write('\n'.join(config_file_contents).expandtabs(4))
config_file.write("\n".join(config_file_contents).expandtabs(4))
config_file.seek(0)
with pytest.raises(SystemExit, match='2'):
with pytest.raises(SystemExit, match="2"):
extract_django_settings_module(config_file.name)
error_message = TEMPLATE.format(message_part)
@@ -60,14 +59,14 @@ def test_misconfiguration_handling(capsys, config_file_contents, message_part):
def test_correct_configuration() -> None:
"""Django settings module gets extracted given valid configuration."""
config_file_contents = [
'[mypy.plugins.django-stubs]',
'\tsome_other_setting = setting',
'\tdjango_settings_module = my.module',
"[mypy.plugins.django-stubs]",
"\tsome_other_setting = setting",
"\tdjango_settings_module = my.module",
]
with tempfile.NamedTemporaryFile(mode='w+') as config_file:
config_file.write('\n'.join(config_file_contents).expandtabs(4))
with tempfile.NamedTemporaryFile(mode="w+") as config_file:
config_file.write("\n".join(config_file_contents).expandtabs(4))
config_file.seek(0)
extracted = extract_django_settings_module(config_file.name)
assert extracted == 'my.module'
assert extracted == "my.module"

View File

@@ -3,23 +3,18 @@
import django.apps
import django.apps.config
import django.apps.registry
import django.conf
import django.conf.global_settings
import django.conf.locale
import django.conf.urls
import django.conf.urls.i18n
import django.conf.urls.static
import django.contrib
import django.contrib.admin
import django.contrib.admin.actions
import django.contrib.admin.apps
import django.contrib.admin.checks
import django.contrib.admin.decorators
import django.contrib.admin.exceptions
import django.contrib.admin.filters
import django.contrib.admin.forms
import django.contrib.admin.helpers
import django.contrib.admin.migrations
import django.contrib.admin.models
import django.contrib.admin.options
import django.contrib.admin.sites
@@ -38,12 +33,10 @@
import django.contrib.admin.views.main
import django.contrib.admin.widgets
import django.contrib.admindocs
import django.contrib.admindocs.apps
import django.contrib.admindocs.middleware
import django.contrib.admindocs.urls
import django.contrib.admindocs.utils
import django.contrib.admindocs.views
import django.contrib.auth
import django.contrib.auth.admin
import django.contrib.auth.apps
import django.contrib.auth.backends
@@ -55,12 +48,10 @@
import django.contrib.auth.handlers
import django.contrib.auth.handlers.modwsgi
import django.contrib.auth.hashers
import django.contrib.auth.management
import django.contrib.auth.management.commands
import django.contrib.auth.management.commands.changepassword
import django.contrib.auth.management.commands.createsuperuser
import django.contrib.auth.middleware
import django.contrib.auth.migrations
import django.contrib.auth.mixins
import django.contrib.auth.models
import django.contrib.auth.password_validation
@@ -69,162 +60,28 @@
import django.contrib.auth.urls
import django.contrib.auth.validators
import django.contrib.auth.views
import django.contrib.contenttypes
import django.contrib.contenttypes.admin
import django.contrib.contenttypes.apps
import django.contrib.contenttypes.checks
import django.contrib.contenttypes.fields
import django.contrib.contenttypes.forms
import django.contrib.contenttypes.management
import django.contrib.contenttypes.management.commands
import django.contrib.contenttypes.management.commands.remove_stale_contenttypes
import django.contrib.contenttypes.migrations
import django.contrib.contenttypes.models
import django.contrib.contenttypes.views
import django.contrib.flatpages
import django.contrib.flatpages.admin
import django.contrib.flatpages.apps
import django.contrib.flatpages.forms
import django.contrib.flatpages.middleware
import django.contrib.flatpages.migrations
import django.contrib.flatpages.models
import django.contrib.flatpages.sitemaps
import django.contrib.flatpages.templatetags
import django.contrib.flatpages.templatetags.flatpages
import django.contrib.flatpages.urls
import django.contrib.flatpages.views
import django.contrib.gis
import django.contrib.gis.admin
import django.contrib.gis.admin.options
import django.contrib.gis.admin.widgets
import django.contrib.gis.apps
import django.contrib.gis.db
import django.contrib.gis.db.backends
import django.contrib.gis.db.backends.base
import django.contrib.gis.db.backends.base.adapter
import django.contrib.gis.db.backends.base.features
import django.contrib.gis.db.backends.base.models
import django.contrib.gis.db.backends.base.operations
import django.contrib.gis.db.backends.mysql
import django.contrib.gis.db.backends.mysql.base
import django.contrib.gis.db.backends.mysql.features
import django.contrib.gis.db.backends.mysql.introspection
import django.contrib.gis.db.backends.mysql.operations
import django.contrib.gis.db.backends.mysql.schema
import django.contrib.gis.db.backends.oracle
import django.contrib.gis.db.backends.oracle.adapter
import django.contrib.gis.db.backends.oracle.base
import django.contrib.gis.db.backends.oracle.features
import django.contrib.gis.db.backends.oracle.introspection
import django.contrib.gis.db.backends.oracle.models
import django.contrib.gis.db.backends.oracle.operations
import django.contrib.gis.db.backends.oracle.schema
import django.contrib.gis.db.backends.postgis
import django.contrib.gis.db.backends.postgis.adapter
import django.contrib.gis.db.backends.postgis.base
import django.contrib.gis.db.backends.postgis.const
import django.contrib.gis.db.backends.postgis.features
import django.contrib.gis.db.backends.postgis.introspection
import django.contrib.gis.db.backends.postgis.models
import django.contrib.gis.db.backends.postgis.operations
import django.contrib.gis.db.backends.postgis.pgraster
import django.contrib.gis.db.backends.postgis.schema
import django.contrib.gis.db.backends.spatialite
import django.contrib.gis.db.backends.spatialite.adapter
import django.contrib.gis.db.backends.spatialite.base
import django.contrib.gis.db.backends.spatialite.client
import django.contrib.gis.db.backends.spatialite.features
import django.contrib.gis.db.backends.spatialite.introspection
import django.contrib.gis.db.backends.spatialite.models
import django.contrib.gis.db.backends.spatialite.operations
import django.contrib.gis.db.backends.spatialite.schema
import django.contrib.gis.db.backends.utils
import django.contrib.gis.db.models
import django.contrib.gis.db.models.aggregates
import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.contrib.gis.db.models.lookups
import django.contrib.gis.db.models.proxy
import django.contrib.gis.db.models.sql
import django.contrib.gis.db.models.sql.conversion
import django.contrib.gis.feeds
import django.contrib.gis.forms
import django.contrib.gis.forms.fields
import django.contrib.gis.forms.widgets
import django.contrib.gis.gdal
import django.contrib.gis.gdal.base
import django.contrib.gis.gdal.datasource
import django.contrib.gis.gdal.driver
import django.contrib.gis.gdal.envelope
import django.contrib.gis.gdal.error
import django.contrib.gis.gdal.feature
import django.contrib.gis.gdal.field
import django.contrib.gis.gdal.geometries
import django.contrib.gis.gdal.geomtype
import django.contrib.gis.gdal.layer
import django.contrib.gis.gdal.libgdal
import django.contrib.gis.gdal.prototypes
import django.contrib.gis.gdal.prototypes.ds
import django.contrib.gis.gdal.prototypes.errcheck
import django.contrib.gis.gdal.prototypes.generation
import django.contrib.gis.gdal.prototypes.geom
import django.contrib.gis.gdal.prototypes.raster
import django.contrib.gis.gdal.prototypes.srs
import django.contrib.gis.gdal.raster
import django.contrib.gis.gdal.raster.band
import django.contrib.gis.gdal.raster.base
import django.contrib.gis.gdal.raster.const
import django.contrib.gis.gdal.raster.source
import django.contrib.gis.gdal.srs
import django.contrib.gis.geoip2
import django.contrib.gis.geoip2.base
import django.contrib.gis.geoip2.resources
import django.contrib.gis.geometry
import django.contrib.gis.geos
import django.contrib.gis.geos.base
import django.contrib.gis.geos.collections
import django.contrib.gis.geos.coordseq
import django.contrib.gis.geos.error
import django.contrib.gis.geos.factory
import django.contrib.gis.geos.geometry
import django.contrib.gis.geos.io
import django.contrib.gis.geos.libgeos
import django.contrib.gis.geos.linestring
import django.contrib.gis.geos.mutable_list
import django.contrib.gis.geos.point
import django.contrib.gis.geos.polygon
import django.contrib.gis.geos.prepared
import django.contrib.gis.geos.prototypes
import django.contrib.gis.geos.prototypes.coordseq
import django.contrib.gis.geos.prototypes.errcheck
import django.contrib.gis.geos.prototypes.geom
import django.contrib.gis.geos.prototypes.io
import django.contrib.gis.geos.prototypes.misc
import django.contrib.gis.geos.prototypes.predicates
import django.contrib.gis.geos.prototypes.prepared
import django.contrib.gis.geos.prototypes.threadsafe
import django.contrib.gis.geos.prototypes.topology
import django.contrib.gis.measure
import django.contrib.gis.ptr
import django.contrib.gis.serializers
import django.contrib.gis.serializers.geojson
import django.contrib.gis.shortcuts
import django.contrib.gis.sitemaps
import django.contrib.gis.sitemaps.kml
import django.contrib.gis.sitemaps.views
import django.contrib.gis.utils
import django.contrib.gis.utils.layermapping
import django.contrib.gis.utils.ogrinfo
import django.contrib.gis.utils.ogrinspect
import django.contrib.gis.utils.srs
import django.contrib.gis.views
import django.contrib.humanize
import django.contrib.humanize.apps
import django.contrib.humanize.templatetags
import django.contrib.humanize.templatetags.humanize
import django.contrib.messages
import django.contrib.messages.api
import django.contrib.messages.apps
import django.contrib.messages.constants
import django.contrib.messages.context_processors
import django.contrib.messages.middleware
@@ -235,12 +92,10 @@
import django.contrib.messages.storage.session
import django.contrib.messages.utils
import django.contrib.messages.views
import django.contrib.postgres
import django.contrib.postgres.aggregates
import django.contrib.postgres.aggregates.general
import django.contrib.postgres.aggregates.mixins
import django.contrib.postgres.aggregates.statistics
import django.contrib.postgres.apps
import django.contrib.postgres.constraints
import django.contrib.postgres.fields
import django.contrib.postgres.fields.array
@@ -249,24 +104,16 @@
import django.contrib.postgres.fields.jsonb
import django.contrib.postgres.fields.mixins
import django.contrib.postgres.fields.ranges
import django.contrib.postgres.fields.utils
import django.contrib.postgres.functions
import django.contrib.postgres.indexes
import django.contrib.postgres.lookups
import django.contrib.postgres.operations
import django.contrib.postgres.search
import django.contrib.postgres.serializers
import django.contrib.postgres.signals
import django.contrib.postgres.utils
import django.contrib.postgres.validators
import django.contrib.redirects
import django.contrib.redirects.admin
import django.contrib.redirects.apps
import django.contrib.redirects.middleware
import django.contrib.redirects.migrations
import django.contrib.redirects.models
import django.contrib.sessions
import django.contrib.sessions.apps
import django.contrib.sessions.backends
import django.contrib.sessions.backends.base
import django.contrib.sessions.backends.cache
@@ -276,35 +123,26 @@
import django.contrib.sessions.backends.signed_cookies
import django.contrib.sessions.base_session
import django.contrib.sessions.exceptions
import django.contrib.sessions.management
import django.contrib.sessions.management.commands
import django.contrib.sessions.management.commands.clearsessions
import django.contrib.sessions.middleware
import django.contrib.sessions.migrations
import django.contrib.sessions.models
import django.contrib.sessions.serializers
import django.contrib.sitemaps
import django.contrib.sitemaps.apps
import django.contrib.sitemaps.management
import django.contrib.sitemaps.management.commands
import django.contrib.sitemaps.management.commands.ping_google
import django.contrib.sitemaps.views
import django.contrib.sites
import django.contrib.sites.admin
import django.contrib.sites.apps
import django.contrib.sites.management
import django.contrib.sites.managers
import django.contrib.sites.middleware
import django.contrib.sites.migrations
import django.contrib.sites.models
import django.contrib.sites.requests
import django.contrib.sites.shortcuts
import django.contrib.staticfiles
import django.contrib.staticfiles.apps
import django.contrib.staticfiles.checks
import django.contrib.staticfiles.finders
import django.contrib.staticfiles.handlers
import django.contrib.staticfiles.management
import django.contrib.staticfiles.management.commands
import django.contrib.staticfiles.management.commands.collectstatic
import django.contrib.staticfiles.management.commands.findstatic
@@ -317,11 +155,7 @@
import django.contrib.staticfiles.utils
import django.contrib.staticfiles.views
import django.contrib.syndication
import django.contrib.syndication.apps
import django.contrib.syndication.views
import django.core
import django.core.asgi
import django.core.cache
import django.core.cache.backends
import django.core.cache.backends.base
import django.core.cache.backends.db
@@ -330,10 +164,7 @@
import django.core.cache.backends.locmem
import django.core.cache.backends.memcached
import django.core.cache.utils
import django.core.checks
import django.core.checks.async_checks
import django.core.checks.caches
import django.core.checks.compatibility
import django.core.checks.database
import django.core.checks.messages
import django.core.checks.model_checks
@@ -357,21 +188,13 @@
import django.core.files.uploadhandler
import django.core.files.utils
import django.core.handlers
import django.core.handlers.asgi
import django.core.handlers.base
import django.core.handlers.exception
import django.core.handlers.wsgi
import django.core.mail
import django.core.mail.backends
import django.core.mail.backends.base
import django.core.mail.backends.console
import django.core.mail.backends.dummy
import django.core.mail.backends.filebased
import django.core.mail.backends.locmem
import django.core.mail.backends.smtp
import django.core.mail.message
import django.core.mail.utils
import django.core.management
import django.core.management.base
import django.core.management.color
import django.core.management.commands
@@ -388,16 +211,12 @@
import django.core.serializers.base
import django.core.serializers.json
import django.core.serializers.python
import django.core.serializers.pyyaml
import django.core.serializers.xml_serializer
import django.core.servers
import django.core.servers.basehttp
import django.core.signals
import django.core.signing
import django.core.validators
import django.core.wsgi
import django.db
import django.db.backends
import django.db.backends.base
import django.db.backends.base.base
import django.db.backends.base.client
@@ -410,47 +229,22 @@
import django.db.backends.ddl_references
import django.db.backends.dummy
import django.db.backends.dummy.base
import django.db.backends.dummy.features
import django.db.backends.mysql
import django.db.backends.mysql.base
import django.db.backends.mysql.client
import django.db.backends.mysql.compiler
import django.db.backends.mysql.creation
import django.db.backends.mysql.features
import django.db.backends.mysql.introspection
import django.db.backends.mysql.operations
import django.db.backends.mysql.schema
import django.db.backends.mysql.validation
import django.db.backends.oracle
import django.db.backends.oracle.base
import django.db.backends.oracle.client
import django.db.backends.oracle.creation
import django.db.backends.oracle.features
import django.db.backends.oracle.functions
import django.db.backends.oracle.introspection
import django.db.backends.oracle.operations
import django.db.backends.oracle.schema
import django.db.backends.oracle.utils
import django.db.backends.oracle.validation
import django.db.backends.postgresql
import django.db.backends.postgresql.base
import django.db.backends.postgresql.client
import django.db.backends.postgresql.creation
import django.db.backends.postgresql.features
import django.db.backends.postgresql.introspection
import django.db.backends.postgresql.operations
import django.db.backends.postgresql.schema
import django.db.backends.signals
import django.db.backends.sqlite3
import django.db.backends.sqlite3.base
import django.db.backends.sqlite3.client
import django.db.backends.sqlite3.creation
import django.db.backends.sqlite3.features
import django.db.backends.sqlite3.introspection
import django.db.backends.sqlite3.operations
import django.db.backends.sqlite3.schema
import django.db.backends.utils
import django.db.migrations
import django.db.migrations.autodetector
import django.db.migrations.exceptions
import django.db.migrations.executor
@@ -471,17 +265,14 @@
import django.db.migrations.topological_sort
import django.db.migrations.utils
import django.db.migrations.writer
import django.db.models
import django.db.models.aggregates
import django.db.models.base
import django.db.models.constants
import django.db.models.constraints
import django.db.models.deletion
import django.db.models.enums
import django.db.models.expressions
import django.db.models.enums
import django.db.models.fields
import django.db.models.fields.files
import django.db.models.fields.json
import django.db.models.fields.mixins
import django.db.models.fields.proxy
import django.db.models.fields.related
@@ -538,7 +329,6 @@
import django.middleware.locale
import django.middleware.security
import django.shortcuts
import django.template
import django.template.backends
import django.template.backends.base
import django.template.backends.django
@@ -585,10 +375,8 @@
import django.urls.exceptions
import django.urls.resolvers
import django.urls.utils
import django.utils
import django.utils._os
import django.utils.archive
import django.utils.asyncio
import django.utils.autoreload
import django.utils.baseconv
import django.utils.cache
@@ -633,7 +421,6 @@
import django.utils.tree
import django.utils.version
import django.utils.xmlutils
import django.views
import django.views.csrf
import django.views.debug
import django.views.decorators