* Fix CI

* Fix CI

* Fix CI

* Fix CI

* APply black

* APply black

* Fix mypy

* Fix mypy errors in django-stubs

* Fix format

* Fix plugin

* Do not patch builtins by default

* Fix mypy

* Only run mypy on 3.10 for now

* Only run mypy on 3.10 for now

* WHAT THE HELL

* Enable strict mode in mypy

* Enable strict mode in mypy

* Fix tests

* Fix tests

* Debug

* Debug

* Fix tests

* Fix tests

* Add TYPE_CHECKING debug

* Caching maybe?

* Caching maybe?

* Try explicit `${{ matrix.python-version }}`

* Remove debug

* Fix typing

* Finally
This commit is contained in:
Nikita Sobolev
2022-08-26 13:22:55 +03:00
committed by GitHub
parent d2bfd3710b
commit 0bb1182c42
80 changed files with 223 additions and 582 deletions

View File

@@ -1,78 +0,0 @@
# Please, keep it in sync with `release-django-stubs-ext.yml`
name: Release django-stubs-ext
on:
push:
tags:
- 'django-stubs-ext@*'
jobs:
build:
environment:
name: django-stubs-ext-pypi
url: https://pypi.org/project/django-stubs-ext
if: github.repository == 'typeddjango/django-stubs'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
cd ./django_stubs_ext
python setup.py --version
python setup.py sdist bdist_wheel
ls dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.DJANGO_STUBS_EXT_PYPI_API_TOKEN }}
packages_dir: django_stubs_ext/dist
print_hash: true
- name: Create release
uses: actions/github-script@v6
with:
script: |
const tagName = context.ref.replace(/^refs\/tags\//, '');
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `Release ${tagName}`,
generate_release_notes: false,
});
if (release.status < 200 || release.status >= 300) {
core.setFailed(`Could not create release for tag '${tagName}'`);
return;
}
# https://github.community/t/run-github-actions-job-only-if-previous-job-has-failed/174786/2
create-issue-on-failure:
name: Create an issue if release failed
runs-on: ubuntu-latest
needs: [build]
if: ${{ github.repository == 'typeddjango/django-stubs' && always() && needs.build.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v6
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Release of django-stubs-ext failure on ${new Date().toDateString()}`,
body: `Details: https://github.com/typeddjango/django-stubs/actions/workflows/release-django-stubs-ext.yml`,
})

View File

@@ -1,76 +0,0 @@
# Please, keep it in sync with `release-django-stubs-ext.yml`
name: Release django-stubs
on:
push:
tags:
- 'django-stubs@*'
jobs:
build:
environment:
name: django-stubs-pypi
url: https://pypi.org/project/django-stubs
if: github.repository == 'typeddjango/django-stubs'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist bdist_wheel
ls dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.DJANGO_STUBS_PYPI_API_TOKEN }}
print_hash: true
- name: Create release
uses: actions/github-script@v6
with:
script: |
const tagName = context.ref.replace(/^refs\/tags\//, '');
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `Release ${tagName}`,
generate_release_notes: true,
});
if (release.status < 200 || release.status >= 300) {
core.setFailed(`Could not create release for tag '${tagName}'`);
return;
}
# https://github.community/t/run-github-actions-job-only-if-previous-job-has-failed/174786/2
create-issue-on-failure:
name: Create an issue if release failed
runs-on: ubuntu-latest
needs: [build]
if: ${{ github.repository == 'typeddjango/django-stubs' && always() && needs.build.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v6
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Release of django-stubs failure on ${new Date().toDateString()}`,
body: `Details: https://github.com/typeddjango/django-stubs/actions/workflows/release-django-stubs.yml`,
})

View File

@@ -27,11 +27,40 @@ jobs:
- name: Run pre-commit
run: pre-commit install && pre-commit run --all-files
mypy-self-check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -U pip setuptools wheel
pip install -r ./requirements.txt
- name: Run mypy on plugin code
run: mypy --strict mypy_django_plugin
- name: Run mypy on ext code
run: mypy --strict django_stubs_ext
- name: Run mypy on scripts and utils
run: mypy --strict scripts
# TODO: run this check on versions, not only 3.10
- name: Run mypy on stubs
if: ${{ matrix.python-version }} == '3.10'
run: |
mypy --cache-dir=/dev/null --no-incremental django-stubs
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8']
python-version: ['3.9']
steps:
- uses: actions/checkout@v3
- name: Setup system dependencies
@@ -46,7 +75,7 @@ jobs:
pip install -r ./requirements.txt
- name: Run tests
run: pytest --mypy-ini-file=mypy.ini
run: PYTHONPATH='.' pytest
typecheck:
runs-on: ubuntu-latest

View File

@@ -16,17 +16,15 @@ repos:
rev: v2.37.3
hooks:
- id: pyupgrade
args: ["--py36-plus"]
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
args: ["--py37-plus"]
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"]
- id: isort
name: isort (pyi)
types: [pyi]
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
@@ -35,19 +33,3 @@ repos:
rev: 3.9.2
hooks:
- id: flake8
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
types: [ python ]
exclude: "scripts/|django_stubs_ext/"
args: [ "--config=mypy.ini", "--cache-dir=/dev/null", "--no-incremental" ]
- id: mypy
name: mypy (django_stubs_ext)
entry: mypy
language: system
types: [ python ]
files: "django_stubs_ext/|django_stubs_ext/tests/"
args: [ "--config=mypy.ini", "--cache-dir=/dev/null", "--no-incremental", "--strict" ]

View File

@@ -116,6 +116,8 @@ This happens because these Django classes do not support [`__class_getitem__`](h
Note: This monkey patching approach will only work when using Python 3.7 and higher, when the `__class_getitem__` magic method was introduced.
You can add extra types to patch with `django_stubs_ext.monkeypatch(extra_classes=[YourDesiredType])`
2. You can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel]'`, this way it will work as a type for `mypy` and as a regular `str` in runtime.
### How can I create a HttpRequest that's guaranteed to have an authenticated user?

View File

@@ -1,4 +1,3 @@
import sys
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple, Union
from django import forms
@@ -10,11 +9,7 @@ from django.forms.models import ModelForm
from django.forms.utils import ErrorDict, ErrorList
from django.forms.widgets import Media, Widget
from django.utils.safestring import SafeString
if sys.version_info < (3, 8):
from typing_extensions import TypedDict
else:
from typing import TypedDict
from typing_extensions import TypedDict
ACTION_CHECKBOX_NAME: str

View File

@@ -1,4 +1,3 @@
import sys
from typing import (
Any,
Callable,
@@ -43,11 +42,7 @@ from django.template.response import _TemplateForResponseT
from django.urls.resolvers import URLPattern
from django.utils.datastructures import _ListOrTuple
from django.utils.safestring import SafeString
if sys.version_info < (3, 8):
from typing_extensions import Literal, TypedDict
else:
from typing import Literal, TypedDict
from typing_extensions import Literal, TypedDict
IS_POPUP_VAR: str
TO_FIELD_VAR: str

View File

@@ -1,5 +1,4 @@
import datetime
import sys
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union, overload
from uuid import UUID
@@ -15,11 +14,7 @@ from django.forms.forms import BaseForm
from django.forms.formsets import BaseFormSet
from django.http.request import HttpRequest
from django.utils.datastructures import _IndexableCollection
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
class FieldIsAForeignKeyColumnName(Exception): ...

View File

@@ -1,4 +1,3 @@
import sys
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union
from django.contrib.admin.filters import ListFilter
@@ -11,11 +10,7 @@ from django.db.models.options import Options
from django.db.models.query import QuerySet
from django.forms.formsets import BaseFormSet
from django.http.request import HttpRequest
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
ALL_VAR: str
ORDER_VAR: str

View File

@@ -1,15 +1,10 @@
import sys
from typing import Any, List, Optional, Tuple, TypeVar, Union, overload
from django.db import models
from django.db.models.base import Model
from django.db.models.expressions import Combinable
from django.db.models.fields import BooleanField
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
_T = TypeVar("_T", bound=Model)

View File

@@ -1,4 +1,3 @@
import sys
from typing import Any, Iterable, Optional, Set, Tuple, Type, TypeVar, Union
from django.contrib.auth.base_user import AbstractBaseUser as AbstractBaseUser
@@ -8,11 +7,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.base import Model
from django.db.models.manager import EmptyManager
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
_AnyUser = Union[Model, "AnonymousUser"]

View File

@@ -1,4 +1,4 @@
from django.core import validators
from django.core.validators import RegexValidator
class ASCIIUsernameValidator(validators.RegexValidator): ...
class UnicodeUsernameValidator(validators.RegexValidator): ...
class ASCIIUsernameValidator(RegexValidator): ...
class UnicodeUsernameValidator(RegexValidator): ...

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional, Tuple, Type
from typing import Any, Dict, List, Tuple, Type
from django.apps.config import AppConfig
from django.apps.registry import Apps

View File

@@ -1,6 +1,6 @@
from typing import Any
from django.contrib import admin as admin
from django.contrib import admin
class FlatPageAdmin(admin.ModelAdmin):
form: Any = ...

View File

@@ -1,7 +1,7 @@
from typing import Any
from django.contrib.gis.db import models as models
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin as SpatialRefSysMixin
from django.contrib.gis.db import models
from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin
class OracleGeometryColumns(models.Model):
table_name: Any = ...

View File

@@ -1,15 +1,10 @@
import sys
from typing import Any, Union
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.db.backends.postgresql.operations import DatabaseOperations
from django.db.models import Func
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
BILATERAL: Literal["bilateral"]

View File

@@ -1,6 +1,6 @@
from typing import Any, Optional
from django import forms as forms
from django import forms
class GeometryField(forms.Field):
widget: Any = ...

View File

@@ -1,14 +1,9 @@
import sys
from typing import Any, Dict, Optional, Type
from django.db import models
from django.db.models.lookups import PostgresOperatorLookup
from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange, Range
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
class RangeBoundary(models.Expression):
lower: str

View File

@@ -1,8 +1,7 @@
from typing import Dict, Literal, Tuple
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations import AddConstraint, AddIndex, RemoveIndex
from django.db.migrations.operations.base import Operation
from typing_extensions import Literal
class CreateExtension(Operation):
reversible: bool = ...

View File

@@ -1,6 +1,6 @@
from typing import Any
from django.contrib import admin as admin
from django.contrib import admin
class RedirectAdmin(admin.ModelAdmin):
list_display: Any = ...

View File

@@ -1,6 +1,6 @@
from typing import Any
from django.contrib import admin as admin
from django.contrib import admin
class SiteAdmin(admin.ModelAdmin):
list_display: Any = ...

View File

@@ -1,13 +1,8 @@
import sys
from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Type, overload
from django.core.checks.messages import CheckMessage
from django.core.files.storage import FileSystemStorage, Storage
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
searched_locations: Any

View File

@@ -1,13 +1,8 @@
import sys
from typing import Any, Callable, List, Optional, Sequence, Set, TypeVar, Union
from django.apps.config import AppConfig
from django.core.checks.messages import CheckMessage
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
class Tags:
admin: str = ...

View File

@@ -1,10 +1,6 @@
import sys
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
class FieldDoesNotExist(Exception): ...
class AppRegistryNotReady(Exception): ...

View File

@@ -1,4 +1,3 @@
import sys
from argparse import ArgumentParser, HelpFormatter, Namespace
from io import TextIOBase
from typing import Any, Callable, Iterable, List, Optional, Sequence, Set, TextIO, Tuple, Union
@@ -6,11 +5,7 @@ from typing import Any, Callable, Iterable, List, Optional, Sequence, Set, TextI
from django.apps.config import AppConfig
from django.core.management.color import Style
from django.utils.datastructures import _ListOrTuple
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
class CommandError(Exception):
def __init__(self, *args: Any, returncode: int = ..., **kwargs: Any) -> None: ...

View File

@@ -2,7 +2,7 @@ from typing import Any
from django.utils.connection import ConnectionProxy
from . import migrations
from . import migrations as migrations
from .backends.base.base import BaseDatabaseWrapper
from .utils import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS # Not exported in __all__
from .utils import DJANGO_VERSION_PICKLE_KEY as DJANGO_VERSION_PICKLE_KEY

View File

@@ -1,10 +1,6 @@
import sys
from typing import Any, Dict, List, Sequence
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
class _QuoteCallable(Protocol):
"""Get rid of `cannot assign to method`"""

View File

@@ -1,7 +1,7 @@
import sys
from typing import Any, Dict, Optional, Type
from django.db.backends.base.base import BaseDatabaseWrapper as BaseDatabaseWrapper
from typing_extensions import Literal
from .client import DatabaseClient
from .creation import DatabaseCreation
@@ -10,11 +10,6 @@ from .introspection import DatabaseIntrospection
from .operations import DatabaseOperations
from .validation import DatabaseValidation
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
version: Any
django_conversions: Any
server_version_re: Any

View File

@@ -9,7 +9,8 @@ class SQLCompiler(compiler.SQLCompiler):
class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): ...
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
def as_sql(self) -> _AsSqlType: ...
# https://github.com/django/django/blob/242499f2dc2bf24a9a5c855690a2e13d3303581a/django/db/backends/mysql/compiler.py#L26
def as_sql(self) -> _AsSqlType: ... # type: ignore[override]
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): ...
class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): ...

View File

@@ -1,5 +1,4 @@
import datetime
import sys
import types
from contextlib import contextmanager
from decimal import Decimal
@@ -21,10 +20,7 @@ from typing import (
)
from uuid import UUID
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
logger: Logger

View File

@@ -1,18 +1,13 @@
import sys
from typing import List, Optional, Sequence, Set, Tuple, Union
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.migrations.migration import Migration
from typing_extensions import Protocol
from .loader import MigrationLoader
from .recorder import MigrationRecorder
from .state import ProjectState
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
class _ProgressCallbackT(Protocol):
def __call__(self, __action: str, __migration: Optional[Migration] = ..., __fake: Optional[bool] = ...) -> None: ...

View File

@@ -1,17 +1,12 @@
import sys
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple, Union
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.utils.datastructures import _ListOrTuple
from typing_extensions import Literal, Protocol
from .base import Operation
if sys.version_info < (3, 8):
from typing_extensions import Literal, Protocol
else:
from typing import Literal, Protocol
class SeparateDatabaseAndState(Operation):
database_operations: Sequence[Operation] = ...
state_operations: Sequence[Operation] = ...

View File

@@ -1,14 +1,9 @@
import sys
from collections import namedtuple
from typing import Iterator, Optional, Tuple, Union
from django.db.migrations.state import ModelState, ProjectState
from django.db.models import Field
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
def resolve_relation(model, app_label: Optional[str] = ..., model_name: Optional[str] = ...) -> Tuple[str, str]: ...

View File

@@ -1,5 +1,4 @@
import datetime
import sys
from decimal import Decimal
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union
@@ -10,11 +9,7 @@ from django.db.models.lookups import Lookup, Transform
from django.db.models.query import QuerySet
from django.db.models.sql.compiler import SQLCompiler, _AsSqlType
from django.db.models.sql.query import Query
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
class SQLiteNumericMixin:
def as_sqlite(self, compiler: SQLCompiler, connection: BaseDatabaseWrapper, **extra_context: Any) -> _AsSqlType: ...

View File

@@ -1,5 +1,4 @@
import decimal
import sys
import uuid
from datetime import date
from datetime import datetime as real_datetime
@@ -32,11 +31,7 @@ from django.forms import Field as FormField
from django.forms import Widget
from django.utils.datastructures import DictWrapper
from django.utils.functional import _Getter
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
class Empty: ...
class NOT_PROVIDED: ...

View File

@@ -1,5 +1,3 @@
import sys
from pathlib import Path
from typing import Any, Callable, Iterable, Optional, Type, TypeVar, Union, overload
from django.core import validators # due to weird mypy.stubtest error
@@ -10,11 +8,7 @@ from django.db.models.base import Model
from django.db.models.fields import Field, _ErrorMessagesT, _FieldChoices
from django.db.models.query_utils import DeferredAttribute
from django.utils._os import _PathCompatible
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
class FieldFile(File):
instance: Model = ...

View File

@@ -1,12 +1,6 @@
import sys
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Type, TypeVar, Union, overload
from uuid import UUID
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from django.core import validators # due to weird mypy.stubtest error
from django.db import models
from django.db.models.base import Model
@@ -26,6 +20,7 @@ from django.db.models.fields.reverse_related import ManyToOneRel as ManyToOneRel
from django.db.models.fields.reverse_related import OneToOneRel as OneToOneRel
from django.db.models.manager import RelatedManager
from django.db.models.query_utils import FilteredRelation, PathInfo, Q
from typing_extensions import Literal
_T = TypeVar("_T", bound=models.Model)
_F = TypeVar("_F", bound=models.Field)

View File

@@ -1,21 +1,15 @@
import sys
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
from typing import Any, Callable, List, Optional, Sequence, Tuple, Type, Union
from django.db.models import Q
from django.db.models.base import Model
from django.db.models.fields import AutoField, Field, _AllLimitChoicesTo, _ChoicesList, _LimitChoicesTo
from django.db.models.fields.related import ForeignKey, ForeignObject, ManyToManyField, OneToOneField
from django.db.models.lookups import BuiltinLookup, Lookup, StartsWith
from django.db.models.lookups import Lookup, StartsWith
from django.db.models.query_utils import FilteredRelation, PathInfo
from django.db.models.sql.where import WhereNode
from typing_extensions import Literal
from .mixins import FieldCacheMixin
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
# Common note: `model` and `through` can be of type `str` when passed to `__init__`.
# When parent's `contribute_to_class` is called (during startup),
# strings are resolved to real model classes.

View File

@@ -1,16 +1,11 @@
import sys
from typing import Any, Generic, Iterable, List, Mapping, Optional, Tuple, Type, TypeVar, Union
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.models.expressions import Expression, Func
from django.db.models.query_utils import RegisterLookupMixin
from django.db.models.sql.compiler import SQLCompiler, _AsSqlType, _ParamT
from django.utils.datastructures import OrderedSet
from typing_extensions import Literal
_L = TypeVar("_L", bound="Lookup")
_T = TypeVar("_T")

View File

@@ -1,4 +1,3 @@
import sys
from typing import Any, Dict, Generic, Iterable, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, overload
from django.apps.config import AppConfig
@@ -13,11 +12,7 @@ from django.db.models.fields.reverse_related import ForeignObjectRel
from django.db.models.manager import Manager
from django.db.models.query_utils import PathInfo
from django.utils.datastructures import ImmutableList, _ListOrTuple
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
PROXY_PARENTS: object
EMPTY_RELATION_TREE: Any

View File

@@ -1,4 +1,3 @@
import sys
from collections import namedtuple
from typing import (
Any,
@@ -17,23 +16,17 @@ from typing import (
Union,
)
from django.db.models.sql.compiler import _AsSqlType
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.models.base import Model
from django.db.models.expressions import BaseExpression
from django.db.models.fields import Field
from django.db.models.fields.mixins import FieldCacheMixin
from django.db.models.lookups import Lookup, Transform
from django.db.models.sql.compiler import SQLCompiler
from django.db.models.sql.compiler import SQLCompiler, _AsSqlType
from django.db.models.sql.query import Query
from django.db.models.sql.where import WhereNode
from django.utils import tree
from typing_extensions import Literal
PathInfo = namedtuple(
"PathInfo", ["from_opts", "to_opts", "target_fields", "join_field", "m2m", "direct", "filtered_relation"]

View File

@@ -1,21 +1,15 @@
import sys
from datetime import date, datetime
from decimal import Decimal
from itertools import chain
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union, overload
from uuid import UUID
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.utils import CursorWrapper
from django.db.models.base import Model
from django.db.models.expressions import BaseExpression, Expression
from django.db.models.sql.query import Query
from django.db.models.sql.subqueries import AggregateQuery, DeleteQuery, InsertQuery, UpdateQuery
from typing_extensions import Literal
_ParamT = Union[str, int]
_ParamsT = List[_ParamT]

View File

@@ -1,10 +1,6 @@
import sys
from typing import Dict, Final, Pattern, Tuple
from typing import Dict, Pattern, Tuple
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Final, Literal
GET_ITERATOR_CHUNK_SIZE: Final[int] = ...

View File

@@ -1,13 +1,7 @@
import collections
import sys
from collections import namedtuple
from typing import Any, Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.utils import CursorWrapper
from django.db.models import Field, FilteredRelation, Model, Q, QuerySet
@@ -18,6 +12,7 @@ from django.db.models.query_utils import PathInfo, RegisterLookupMixin
from django.db.models.sql.compiler import SQLCompiler
from django.db.models.sql.datastructures import BaseTable, Join
from django.db.models.sql.where import WhereNode
from typing_extensions import Literal
JoinInfo = namedtuple("JoinInfo", ("final_field", "targets", "opts", "joins", "path", "transform_function"))

View File

@@ -1,4 +1,3 @@
import sys
from typing import (
Any,
Callable,
@@ -34,11 +33,7 @@ from django.forms.renderers import BaseRenderer
from django.forms.utils import ErrorList, _DataT, _FilesT
from django.forms.widgets import ChoiceWidget, Input, Widget
from django.utils.datastructures import _IndexableCollection, _ListOrTuple, _PropertyDescriptor
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
ALL_FIELDS: Literal["__all__"]

View File

@@ -1,5 +1,4 @@
import datetime
import sys
from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple, Type, Union
from django.core.files.base import File
@@ -9,12 +8,7 @@ from django.forms.utils import _DataT, _FilesT
from django.utils.datastructures import _ListOrTuple
from django.utils.functional import _Getter
from django.utils.safestring import SafeString
from typing_extensions import Literal
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Literal, Protocol
_OptAttrs = Dict[str, Any]

View File

@@ -1,20 +1,15 @@
import sys
from typing import IO, Any, Dict, Iterator, List, Mapping, Optional, Tuple, Union
from django.http.request import QueryDict
from django.utils.datastructures import ImmutableList, MultiValueDict
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
class MultiPartParserError(Exception): ...
class InputStreamExhausted(Exception): ...
RAW: Literal["raw"] = "raw"
FILE: Literal["file"] = "file"
FIELD: Literal["field"] = "field"
RAW: Literal["raw"]
FILE: Literal["file"]
FIELD: Literal["field"]
class MultiPartParser:
def __init__(

View File

@@ -1,4 +1,3 @@
import sys
from io import BytesIO
from typing import (
Any,
@@ -25,11 +24,7 @@ from django.contrib.sites.models import Site
from django.core.files import uploadedfile, uploadhandler
from django.urls import ResolverMatch
from django.utils.datastructures import CaseInsensitiveMapping, ImmutableList, MultiValueDict
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
RAISE_ERROR: object = ...
host_validation_re: Pattern[str] = ...

View File

@@ -1,4 +1,3 @@
import sys
from typing import Any, Callable, List, Mapping, Optional, Protocol, Sequence, Type, TypeVar, Union, overload
from django.db.models import Manager, QuerySet
@@ -7,11 +6,7 @@ from django.http import HttpRequest
from django.http.response import HttpResponse as HttpResponse
from django.http.response import HttpResponsePermanentRedirect as HttpResponsePermanentRedirect
from django.http.response import HttpResponseRedirect as HttpResponseRedirect
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
def render(
request: HttpRequest,

View File

@@ -1,12 +1,7 @@
import sys
from typing import Any, Callable, Dict, Optional, Sequence, Union
from django.urls.resolvers import ResolverMatch
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
def resolve(path: str, urlconf: Optional[str] = ...) -> ResolverMatch: ...
def reverse(

View File

@@ -1,4 +1,3 @@
import sys
from typing import (
Any,
Collection,
@@ -16,10 +15,7 @@ from typing import (
overload,
)
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
_K = TypeVar("_K")
_V = TypeVar("_V")

View File

@@ -1,15 +1,10 @@
import sys
from datetime import date
from datetime import datetime as builtin_datetime
from datetime import time as builtin_time
from typing import Any, Optional, Pattern, Union
from django.utils.timezone import _TzInfoT
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
re_formatchars: Pattern[str]
re_escaped: Pattern[str]

View File

@@ -1,15 +1,9 @@
import datetime
import sys
from decimal import Decimal
from typing import Any, TypeVar, Union, overload
if sys.version_info < (3, 10):
from typing_extensions import TypeGuard
else:
from typing import TypeGuard
from django.utils.functional import Promise
from typing_extensions import Literal
from typing_extensions import Literal, TypeGuard
class DjangoUnicodeDecodeError(UnicodeDecodeError):
obj: bytes = ...

View File

@@ -1,13 +1,8 @@
import sys
from functools import wraps as wraps # noqa: F401
from typing import Any, Callable, Generic, List, Optional, Tuple, Type, TypeVar, Union, overload
from django.db.models.base import Model
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
_T = TypeVar("_T")

View File

@@ -1,4 +1,3 @@
import sys
import types
from contextlib import ContextDecorator
from datetime import date
@@ -9,18 +8,9 @@ from datetime import timezone
from datetime import tzinfo as tzinfo
from typing import Any, Optional, Type, Union, overload
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
if sys.version_info < (3, 10):
from typing_extensions import TypeGuard
else:
from typing import TypeGuard
import pytz
from pytz import BaseTzInfo
from typing_extensions import Literal, TypeGuard
_PytzTzInfoT = Union[pytz.tzinfo.BaseTzInfo, pytz._FixedOffset]
_TzInfoT = Union[_PytzTzInfoT, tzinfo]

View File

@@ -1,14 +1,9 @@
import gettext as gettext_module
import sys
from gettext import NullTranslations
from typing import Any, Callable, Dict, Iterator, List, Optional, Pattern, Protocol, Tuple, TypeVar, Union
from typing import Any, Dict, Iterator, List, Optional, Pattern, Protocol, Tuple, TypeVar, Union
from django.http.request import HttpRequest
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
CONTEXT_SEPARATOR: Literal["\x04"]
accept_language_re: Pattern[str]

View File

@@ -1,4 +1,3 @@
import sys
from typing import Any, Dict, Generic, Optional, Type, TypeVar, Union
from django.db import models
@@ -8,11 +7,7 @@ from django.http import HttpRequest, HttpResponse
from django.utils.datastructures import _ListOrTuple
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
from django.views.generic.detail import BaseDetailView, SingleObjectMixin, SingleObjectTemplateResponseMixin
if sys.version_info < (3, 8):
from typing_extensions import Literal
else:
from typing import Literal
from typing_extensions import Literal
_FormT = TypeVar("_FormT", bound=BaseForm)
_ModelFormT = TypeVar("_ModelFormT", bound=BaseModelForm)

View File

@@ -1,3 +1,4 @@
import builtins
from typing import Any, Generic, Iterable, List, Optional, Tuple, Type, TypeVar
from django import VERSION as VERSION
@@ -17,6 +18,8 @@ from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import DeletionMixin, FormMixin
from django.views.generic.list import MultipleObjectMixin
__all__ = ["monkeypatch"]
_T = TypeVar("_T")
_VersionSpec = Tuple[int, int]
@@ -67,7 +70,7 @@ _need_generic: List[MPGeneric[Any]] = [
]
def monkeypatch(extra_classes: Optional[Iterable[type]] = None) -> None:
def monkeypatch(extra_classes: Optional[Iterable[type]] = None, include_builtins: bool = True) -> None:
"""Monkey patch django as necessary to work properly with mypy."""
# Add the __class_getitem__ dunder.
@@ -81,5 +84,7 @@ def monkeypatch(extra_classes: Optional[Iterable[type]] = None) -> None:
for cls in extra_classes:
cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls) # type: ignore[attr-defined]
__all__ = ["monkeypatch"]
# Add `reveal_type` and `reveal_locals` helpers if needed:
if include_builtins:
builtins.reveal_type = lambda _: None
builtins.reveal_locals = lambda: None

View File

@@ -1,10 +1,7 @@
import sys
from typing import Any
if sys.version_info < (3, 8):
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import Protocol
# Used internally by mypy_django_plugin.
class AnyAttrAllowed(Protocol):

View File

@@ -12,7 +12,7 @@ dependencies = [
setup(
name="django-stubs-ext",
version="0.5.0",
version="0.6.0",
description="Monkey-patching and extensions for django-stubs",
long_description=readme,
long_description_content_type="text/markdown",
@@ -23,7 +23,7 @@ setup(
maintainer="Nikita Sobolev",
maintainer_email="mail@sobolevn.me",
py_modules=[],
python_requires=">=3.6",
python_requires=">=3.7",
install_requires=dependencies,
packages=["django_stubs_ext", *find_packages(exclude=["scripts"])],
package_data={"django_stubs_ext": ["py.typed"]},
@@ -33,6 +33,7 @@ setup(
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Typing :: Typed",
"Framework :: Django",
"Framework :: Django :: 2.2",

View File

@@ -1,13 +1,12 @@
import builtins
from contextlib import suppress
from typing import Iterable, Optional
from typing import Iterable, List, Optional
import pytest
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
from django.db.models import Model
from django.forms.models import ModelForm
from django.views import View
from typing_extensions import Protocol
import django_stubs_ext
@@ -19,7 +18,10 @@ class _MakeGenericClasses(Protocol):
"""Used to represent a type of ``make_generic_classes`` fixture."""
def __call__(
self, django_version: Optional[_VersionSpec] = None, extra_classes: Optional[Iterable[type]] = None
self,
django_version: Optional[_VersionSpec] = None,
extra_classes: Optional[Iterable[type]] = None,
include_builtins: bool = True,
) -> None:
...
@@ -29,7 +31,7 @@ def make_generic_classes(
request: FixtureRequest,
monkeypatch: MonkeyPatch,
) -> _MakeGenericClasses:
_extra_classes: list[type] = []
_extra_classes: List[type] = []
def fin() -> None:
for el in _need_generic:
@@ -38,13 +40,22 @@ def make_generic_classes(
for cls in _extra_classes:
with suppress(AttributeError):
delattr(cls, "__class_getitem__")
_extra_classes.clear()
with suppress(AttributeError):
del builtins.reveal_type
with suppress(AttributeError):
del builtins.reveal_locals
def factory(django_version: Optional[_VersionSpec] = None, extra_classes: Optional[Iterable[type]] = None) -> None:
def factory(
django_version: Optional[_VersionSpec] = None,
extra_classes: Optional[Iterable[type]] = None,
include_builtins: bool = True,
) -> None:
if extra_classes:
_extra_classes.extend(extra_classes)
if django_version is not None:
monkeypatch.setattr(patch, "VERSION", django_version)
django_stubs_ext.monkeypatch(extra_classes)
django_stubs_ext.monkeypatch(extra_classes=extra_classes, include_builtins=include_builtins)
request.addfinalizer(fin)
return factory
@@ -64,14 +75,17 @@ def test_patched_generics(make_generic_classes: _MakeGenericClasses) -> None:
def test_patched_extra_classes_generics(make_generic_classes: _MakeGenericClasses) -> None:
"""Test that the generics actually get patched for extra classes."""
extra_classes = [View]
class _NotGeneric:
pass
extra_classes = [_NotGeneric]
make_generic_classes(django_version=None, extra_classes=extra_classes)
for cls in extra_classes:
assert cls[type] is cls # type: ignore[misc]
class TestView(View[Model]): # type: ignore[type-arg]
class _TestGeneric(_NotGeneric[Model]): # type: ignore
pass
@@ -106,7 +120,17 @@ def test_mypy_builtins_not_patched_globally(
This should only happend during `django.setup()`
(https://github.com/typeddjango/django-stubs/issues/609).
"""
make_generic_classes()
make_generic_classes(include_builtins=False)
assert not hasattr(builtins, "reveal_type")
assert not hasattr(builtins, "reveal_locals")
def test_mypy_builtins_patched(
make_generic_classes: _MakeGenericClasses,
) -> None:
"""Ensures that builtins are patched by default."""
make_generic_classes()
assert hasattr(builtins, "reveal_type")
assert hasattr(builtins, "reveal_locals")

View File

@@ -3,6 +3,7 @@
# `main` is empty on purpose, because we test the `django.setup()` step.
# It executes the `myapp/models.py` file and should not produce anything,
# because `reveal_type` and `reveal_locals` are patched.
monkeypatch: true
installed_apps:
- myapp
files:

View File

@@ -1,10 +1,5 @@
# Regular configuration file (can be used as base in other projects, runs in CI)
# NOTE: this config file is not used by pytest locally.
# See comment in mypy.ini.dev for explanation.
# WARNING: when changing this file, consider doing the same with mypy.ini.dev
[mypy]
allow_redefinition = True
check_untyped_defs = True

View File

@@ -1,30 +0,0 @@
# Configuration file to use during development
# Changes of plugin code are not detected by mypy, so
# incremental mode can be harmful during development
# (mypy cache is not always invalidated).
# However, incremental mode has to be supported by django-stubs,
# thus another config (mypy.ini) is used in CI.
# Incremental mode is recommended for regular usage.
# WARNING: when changing this file, consider doing the same with mypy.ini
[mypy]
allow_redefinition = True
check_untyped_defs = True
ignore_missing_imports = True
# Avoid caching between test runs
incremental = False
strict_optional = True
show_traceback = True
warn_no_return = False
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unreachable = True
plugins =
mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = scripts.django_tests_settings

View File

@@ -1,9 +1,8 @@
import builtins
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, Any, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union
from django.core.exceptions import FieldError
from django.db import models
@@ -39,7 +38,7 @@ if TYPE_CHECKING:
@contextmanager
def temp_environ():
def temp_environ() -> Iterator[None]:
"""Allow the ability to set os.environ temporarily"""
environ = dict(os.environ)
try:
@@ -56,19 +55,6 @@ def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
# add current directory to sys.path
sys.path.append(os.getcwd())
def noop_class_getitem(cls, key):
return cls
from django.db import models
models.QuerySet.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore
models.Manager.__class_getitem__ = classmethod(noop_class_getitem) # type: ignore
# Define mypy builtins, to not cause NameError during setting up Django.
# TODO: temporary/unpatch
builtins.reveal_type = lambda _: None
builtins.reveal_locals = lambda: None
from django.apps import apps
from django.conf import settings
@@ -80,8 +66,8 @@ def initialize_django(settings_module: str) -> Tuple["Apps", "LazySettings"]:
apps.populate(settings.INSTALLED_APPS)
assert apps.apps_ready
assert settings.configured
assert apps.apps_ready, "Apps are not ready"
assert settings.configured, "Settings are not configured"
return apps, settings
@@ -127,7 +113,7 @@ class DjangoContext:
return model_cls
return None
def get_model_fields(self, model_cls: Type[Model]) -> Iterator[Field]:
def get_model_fields(self, model_cls: Type[Model]) -> Iterator["Field[Any, Any]"]:
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
yield field
@@ -137,7 +123,9 @@ class DjangoContext:
if isinstance(field, ForeignObjectRel):
yield field
def get_field_lookup_exact_type(self, api: TypeChecker, field: Union[Field, ForeignObjectRel]) -> MypyType:
def get_field_lookup_exact_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel]
) -> MypyType:
if isinstance(field, (RelatedField, ForeignObjectRel)):
related_model_cls = field.related_model
primary_key_field = self.get_primary_key_field(related_model_cls)
@@ -155,7 +143,7 @@ class DjangoContext:
return AnyType(TypeOfAny.explicit)
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:
def get_primary_key_field(self, model_cls: Type[Model]) -> "Field[Any, Any]":
for field in model_cls._meta.get_fields():
if isinstance(field, Field):
if field.primary_key:
@@ -258,11 +246,11 @@ class DjangoContext:
def all_registered_model_class_fullnames(self) -> Set[str]:
return {helpers.get_class_fullname(cls) for cls in self.all_registered_model_classes}
def get_attname(self, field: Field) -> str:
def get_attname(self, field: "Field[Any, Any]") -> str:
attname = field.attname
return attname
def get_field_nullability(self, field: Union[Field, ForeignObjectRel], method: Optional[str]) -> bool:
def get_field_nullability(self, field: Union["Field[Any, Any]", ForeignObjectRel], method: Optional[str]) -> bool:
if method in ("values", "values_list"):
return field.null
@@ -279,7 +267,9 @@ class DjangoContext:
return True
return nullable
def get_field_set_type(self, api: TypeChecker, field: Union[Field, ForeignObjectRel], *, method: str) -> MypyType:
def get_field_set_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str
) -> MypyType:
"""Get a type of __set__ for this specific Django field."""
target_field = field
if isinstance(field, ForeignKey):
@@ -297,7 +287,9 @@ class DjangoContext:
field_set_type = helpers.convert_any_to_type(field_set_type, argument_field_type)
return field_set_type
def get_field_get_type(self, api: TypeChecker, field: Union[Field, ForeignObjectRel], *, method: str) -> MypyType:
def get_field_get_type(
self, api: TypeChecker, field: Union["Field[Any, Any]", ForeignObjectRel], *, method: str
) -> MypyType:
"""Get a type of __get__ for this specific Django field."""
field_info = helpers.lookup_class_typeinfo(api, field.__class__)
if field_info is None:
@@ -321,14 +313,16 @@ class DjangoContext:
else:
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]]:
def get_field_related_model_cls(
self, field: Union["RelatedField[Any, Any]", ForeignObjectRel]
) -> Optional[Type[Model]]:
if isinstance(field, RelatedField):
related_model_cls = field.remote_field.model
else:
related_model_cls = field.field.model
if isinstance(related_model_cls, str):
if related_model_cls == "self": # type: ignore[unreachable]
if related_model_cls == "self": # type: ignore
# same model
related_model_cls = field.model
elif "." not in related_model_cls:
@@ -342,9 +336,9 @@ class DjangoContext:
def _resolve_field_from_parts(
self, field_parts: Iterable[str], model_cls: Type[Model]
) -> Union[Field, ForeignObjectRel]:
) -> Union["Field[Any, Any]", ForeignObjectRel]:
currently_observed_model = model_cls
field: Union[Field, ForeignObjectRel, GenericForeignKey, None] = None
field: Union["Field[Any, Any]", ForeignObjectRel, GenericForeignKey, None] = None
for field_part in field_parts:
if field_part == "pk":
field = self.get_primary_key_field(currently_observed_model)
@@ -364,7 +358,9 @@ class DjangoContext:
assert isinstance(field, (Field, ForeignObjectRel))
return field
def resolve_lookup_into_field(self, model_cls: Type[Model], lookup: str) -> Union[Field, ForeignObjectRel]:
def resolve_lookup_into_field(
self, model_cls: Type[Model], lookup: str
) -> Union["Field[Any, Any]", ForeignObjectRel]:
query = Query(model_cls)
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)

View File

@@ -95,7 +95,10 @@ def lookup_fully_qualified_typeinfo(api: Union[TypeChecker, SemanticAnalyzer], f
return node
def lookup_class_typeinfo(api: TypeChecker, klass: type) -> Optional[TypeInfo]:
def lookup_class_typeinfo(api: TypeChecker, klass: Optional[type]) -> Optional[TypeInfo]:
if klass is None:
return None
fullname = get_class_fullname(klass)
field_info = lookup_fully_qualified_typeinfo(api, fullname)
return field_info
@@ -183,7 +186,7 @@ def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is
return AnyType(TypeOfAny.explicit)
def get_field_lookup_exact_type(api: TypeChecker, field: Field) -> MypyType:
def get_field_lookup_exact_type(api: TypeChecker, field: "Field[Any, Any]") -> MypyType:
if isinstance(field, (RelatedField, ForeignObjectRel)):
lookup_type_class = field.related_model
rel_model_info = lookup_class_typeinfo(api, lookup_type_class)
@@ -318,7 +321,7 @@ def resolve_string_attribute_value(attr_expr: Expression, django_context: "Djang
member_name = attr_expr.name
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 getattr(django_context.settings, member_name) # type: ignore
return None

View File

@@ -1,6 +1,6 @@
import sys
from functools import partial
from typing import Callable, Dict, List, Optional, Tuple
from typing import Callable, Dict, List, Optional, Tuple, Type
from django.db.models.fields.related import RelatedField
from mypy.modulefinder import mypy_path
@@ -72,7 +72,7 @@ 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(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"queryset_bases", {fullnames.QUERYSET_CLASS_FULLNAME: 1}
)
else:
@@ -81,7 +81,7 @@ class NewSemanalDjangoPlugin(Plugin):
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(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"manager_bases", {fullnames.MANAGER_CLASS_FULLNAME: 1}
)
else:
@@ -90,7 +90,7 @@ class NewSemanalDjangoPlugin(Plugin):
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(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"model_bases", {fullnames.MODEL_CLASS_FULLNAME: 1}
)
else:
@@ -99,7 +99,7 @@ class NewSemanalDjangoPlugin(Plugin):
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(
return helpers.get_django_metadata(model_sym.node).setdefault( # type: ignore[no-any-return]
"baseform_bases",
{
fullnames.BASEFORM_CLASS_FULLNAME: 1,
@@ -305,5 +305,5 @@ class NewSemanalDjangoPlugin(Plugin):
return None
def plugin(version):
def plugin(version: str) -> Type[NewSemanalDjangoPlugin]:
return NewSemanalDjangoPlugin

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
from django.db.models.fields import AutoField, Field
from django.db.models.fields.related import RelatedField
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
def _get_current_field_from_assignment(
ctx: FunctionContext, django_context: DjangoContext
) -> Optional[Union[Field, ForeignObjectRel, "GenericForeignKey"]]:
) -> Optional[Union["Field[Any, Any]", ForeignObjectRel, "GenericForeignKey"]]:
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):
return None
@@ -42,7 +42,7 @@ def _get_current_field_from_assignment(
return current_field
def reparametrize_related_field_type(related_field_type: Instance, set_type, get_type) -> Instance:
def reparametrize_related_field_type(related_field_type: Instance, set_type: MypyType, get_type: MypyType) -> Instance:
args = [
helpers.convert_any_to_type(related_field_type.args[0], set_type),
helpers.convert_any_to_type(related_field_type.args[1], get_type),

View File

@@ -1,6 +1,6 @@
from typing import Optional, Union
from mypy.checker import TypeChecker, fill_typevars
from mypy.checker import TypeChecker
from mypy.nodes import (
GDEF,
CallExpr,
@@ -19,6 +19,7 @@ from mypy.plugin import AttributeContext, DynamicClassDefContext, SemanticAnalyz
from mypy.types import AnyType, CallableType, Instance, ProperType
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny
from mypy.typevars import fill_typevars
from typing_extensions import Final
from mypy_django_plugin.lib import fullnames, helpers

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Type, Union, cast
from typing import Any, Dict, List, Optional, Type, Union, cast
from django.db.models import Manager, Model
from django.db.models.fields import DateField, DateTimeField, Field
@@ -166,7 +166,7 @@ class ModelClassInitializer:
return queryset_info
def run_with_model_cls(self, model_cls):
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
raise NotImplementedError("Implement this in subclasses")
@@ -202,7 +202,7 @@ class AddDefaultPrimaryKey(ModelClassInitializer):
def create_autofield(
self,
auto_field: Field,
auto_field: "Field[Any, Any]",
dest_name: str,
existing_field: bool,
) -> None:
@@ -394,7 +394,7 @@ class AddManagers(ModelClassInitializer):
return None
def get_dynamic_manager(self, fullname: str, manager: Manager) -> Optional[TypeInfo]:
def get_dynamic_manager(self, fullname: str, manager: "Manager[Any]") -> Optional[TypeInfo]:
"""
Try to get a dynamically defined manager
"""

View File

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

View File

@@ -7,5 +7,5 @@ addopts =
-s
-v
--cache-clear
--mypy-ini-file=./mypy.ini.dev
--mypy-ini-file=mypy.ini
--mypy-extension-hook=scripts.tests_extension_hook.django_plugin_hook

View File

@@ -3,10 +3,10 @@ requests==2.28.1
gitpython==3.1.27
pre-commit==2.20.0
pytest==7.1.2
pytest-mypy-plugins==1.9.3
pytest-mypy-plugins==1.10.0
psycopg2-binary
-e ./django_stubs_ext
-e .[compatible-mypy]
# Overrides:
mypy==0.961
mypy==0.971

View File

@@ -1,6 +1,7 @@
# Some errors occur for the test suite itself, and cannot be addressed via django-stubs. They should be ignored
# using this constant.
import re
from typing import Any, Dict, List
IGNORED_MODULES = {
"schema",
@@ -56,7 +57,7 @@ EXTERNAL_MODULES = [
"argon2",
"xml.dom",
]
IGNORED_ERRORS = {
IGNORED_ERRORS: Dict[str, List[Any]] = {
"__common__": [
*MOCK_OBJECTS,
*EXTERNAL_MODULES,

View File

@@ -1,7 +1,8 @@
import shutil
from typing import Optional
from typing import Optional, Union
from git import RemoteProgress, Repo
from git.remote import RemoteProgress
from git.repo import Repo
from scripts.paths import DJANGO_SOURCE_DIRECTORY
@@ -10,7 +11,9 @@ class ProgressPrinter(RemoteProgress):
def line_dropped(self, line: str) -> None:
print(line)
def update(self, op_code, cur_count, max_count=None, message=""):
def update(
self, op_code: int, cur_count: Union[str, float], max_count: Union[str, float, None] = None, message: str = ""
) -> None:
print(self._cur_line)
@@ -22,7 +25,7 @@ def checkout_django_branch(django_version: str, commit_sha: Optional[str]) -> Re
repo = Repo.clone_from(
"https://github.com/django/django.git",
DJANGO_SOURCE_DIRECTORY,
progress=ProgressPrinter(),
progress=ProgressPrinter(), # type: ignore
branch=branch,
depth=100,
)

View File

@@ -5,6 +5,7 @@ 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)
monkeypatch = test_item.parsed_test_data.get("monkeypatch", False)
if installed_apps and custom_settings:
raise ValueError('"installed_apps" and "custom_settings" are not compatible, please use one or the other')
@@ -18,6 +19,9 @@ def django_plugin_hook(test_item: YamlTestItem) -> None:
if "SECRET_KEY" not in custom_settings:
custom_settings = 'SECRET_KEY = "1"\n' + custom_settings
if monkeypatch:
custom_settings = "import django_stubs_ext\ndjango_stubs_ext.monkeypatch()\n" + custom_settings
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

View File

@@ -5,7 +5,7 @@ import sys
from argparse import ArgumentParser
from collections import defaultdict
from distutils import spawn
from typing import Dict, List, Pattern, Union
from typing import DefaultDict, List, Pattern, Union
from scripts.enabled_test_modules import EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS
from scripts.git_helpers import checkout_django_branch
@@ -18,8 +18,10 @@ DJANGO_COMMIT_REFS = {
}
DEFAULT_DJANGO_VERSION = "3.2"
_DictToSearch = DefaultDict[str, DefaultDict[Union[str, Pattern[str]], int]]
def get_unused_ignores(ignored_message_freq: Dict[str, Dict[Union[str, Pattern], int]]) -> List[str]:
def get_unused_ignores(ignored_message_freq: _DictToSearch) -> List[str]:
unused_ignores = []
for root_key, patterns in IGNORED_ERRORS.items():
for pattern in patterns:
@@ -30,7 +32,7 @@ def get_unused_ignores(ignored_message_freq: Dict[str, Dict[Union[str, Pattern],
return unused_ignores
def does_pattern_fit(pattern: Union[Pattern, str], line: str):
def does_pattern_fit(pattern: Union[Pattern[str], str], line: str) -> bool:
if isinstance(pattern, Pattern):
if pattern.search(line):
return True
@@ -40,7 +42,7 @@ def does_pattern_fit(pattern: Union[Pattern, str], line: str):
return False
def is_ignored(line: str, test_folder_name: str, *, ignored_message_freqs: Dict[str, Dict[str, int]]) -> bool:
def is_ignored(line: str, test_folder_name: str, *, ignored_message_freqs: _DictToSearch) -> bool:
if "runtests" in line:
return True
@@ -86,14 +88,14 @@ if __name__ == "__main__":
mypy_executable = spawn.find_executable("mypy")
mypy_argv = [mypy_executable, *mypy_options]
completed = subprocess.run(
mypy_argv,
mypy_argv, # type: ignore
env={"PYTHONPATH": str(tests_root), "TYPECHECK_TESTS": "1"},
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
output = completed.stdout.decode()
ignored_message_freqs = defaultdict(lambda: defaultdict(int))
ignored_message_freqs: _DictToSearch = defaultdict(lambda: defaultdict(int))
sorted_lines = sorted(output.splitlines())
for line in sorted_lines:

View File

@@ -22,7 +22,7 @@ with open("README.md") as f:
dependencies = [
"mypy>=0.930",
"django",
"django-stubs-ext>=0.4.0",
"django-stubs-ext>=0.6.0",
"tomli",
# Types:
"typing-extensions",
@@ -31,7 +31,7 @@ dependencies = [
]
extras_require = {
"compatible-mypy": ["mypy>=0.930,<0.970"],
"compatible-mypy": ["mypy>=0.930,<0.980"],
}
setup(
@@ -61,6 +61,7 @@ setup(
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Typing :: Typed",
"Framework :: Django",
"Framework :: Django :: 2.2",

View File

@@ -2,11 +2,7 @@
- case: annotated_should_not_iterfere
main: |
from dataclasses import dataclass
import sys
if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated
from typing_extensions import Annotated
class IntegerType:
def __init__(self, min_value: int, max_value: int) -> None:

View File

@@ -58,8 +58,6 @@
mypy_config: |
[mypy.plugins.django-stubs]
django_settings_module = mysettings
env:
- MYPYPATH=./extras
files:
- path: extras/extra_module.py
content: |