Add Django 3.0 testing to CI (#246)

* add Django 3.0 testing to CI

* remove importlib_metadata usage

* conditionally load choices module for tests
This commit is contained in:
Maksim Kurnikov
2019-12-06 23:36:24 +03:00
committed by GitHub
parent cadd6c963b
commit 4ac43c6ed6
18 changed files with 125 additions and 34 deletions

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "django-sources"]
path = django-sources
url = https://github.com/django/django.git
branch = stable/2.2.x

View File

@@ -8,13 +8,23 @@ jobs:
python: 3.7 python: 3.7
script: 'pytest' script: 'pytest'
- name: Typecheck Django test suite with python 3.7 - name: Typecheck Django 3.0 test suite with python 3.7
python: 3.7 python: 3.7
script: 'python ./scripts/typecheck_tests.py' script: |
pip install Django==3.0.*
python ./scripts/typecheck_tests.py --django_version=3.0
- name: Typecheck Django test suite with python 3.6 - name: Typecheck Django 3.0 test suite with python 3.6
python: 3.6 python: 3.6
script: 'python ./scripts/typecheck_tests.py' script: |
pip install Django==3.0.*
python ./scripts/typecheck_tests.py --django_version=3.0
- name: Typecheck Django 2.2 test suite with python 3.7
python: 3.7
script: |
pip install Django==2.2.*
python ./scripts/typecheck_tests.py --django_version=2.2
- name: Mypy for plugin code - name: Mypy for plugin code
python: 3.7 python: 3.7

View File

@@ -4,4 +4,5 @@ psycopg2
flake8==3.7.8 flake8==3.7.8
flake8-pyi==19.3.0 flake8-pyi==19.3.0
isort==4.3.21 isort==4.3.21
gitpython==3.0.5
-e . -e .

Submodule django-sources deleted from 9a17ae50c6

View File

@@ -129,3 +129,5 @@ from .constraints import (
CheckConstraint as CheckConstraint, CheckConstraint as CheckConstraint,
UniqueConstraint as UniqueConstraint, UniqueConstraint as UniqueConstraint,
) )
from .enums import Choices as Choices, IntegerChoices as IntegerChoices, TextChoices as TextChoices

View File

@@ -0,0 +1,30 @@
import enum
from typing import Any, List, Tuple
class ChoicesMeta(enum.EnumMeta):
names: List[str] = ...
choices: List[Tuple[Any, str]] = ...
labels: List[str] = ...
values: List[Any] = ...
def __contains__(self, item: Any) -> bool: ...
class Choices(enum.Enum, metaclass=ChoicesMeta):
def __str__(self): ...
# fake
class _IntegerChoicesMeta(ChoicesMeta):
names: List[str] = ...
choices: List[Tuple[int, str]] = ...
labels: List[str] = ...
values: List[int] = ...
class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta): ...
# fake
class _TextChoicesMeta(ChoicesMeta):
names: List[str] = ...
choices: List[Tuple[str, str]] = ...
labels: List[str] = ...
values: List[str] = ...
class TextChoices(str, Choices, metaclass=_TextChoicesMeta): ...

View File

@@ -108,7 +108,8 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
def db_parameters(self, connection: Any) -> Dict[str, str]: ... def db_parameters(self, connection: Any) -> Dict[str, str]: ...
def get_prep_value(self, value: Any) -> Any: ... def get_prep_value(self, value: Any) -> Any: ...
def get_internal_type(self) -> str: ... def get_internal_type(self) -> str: ...
def formfield(self, **kwargs) -> FormField: ... # TODO: plugin support
def formfield(self, **kwargs) -> Any: ...
def save_form_data(self, instance: Model, data: Any) -> None: ... def save_form_data(self, instance: Model, data: Any) -> None: ...
def contribute_to_class(self, cls: Type[Model], name: str, private_only: bool = ...) -> None: ... def contribute_to_class(self, cls: Type[Model], name: str, private_only: bool = ...) -> None: ...
def to_python(self, value: Any) -> Any: ... def to_python(self, value: Any) -> Any: ...
@@ -361,20 +362,20 @@ class UUIDField(Field[_ST, _GT]):
_pyi_private_get_type: uuid.UUID _pyi_private_get_type: uuid.UUID
class FilePathField(Field[_ST, _GT]): class FilePathField(Field[_ST, _GT]):
path: str = ... path: Any = ...
match: Optional[Any] = ... match: Optional[str] = ...
recursive: bool = ... recursive: bool = ...
allow_files: bool = ... allow_files: bool = ...
allow_folders: bool = ... allow_folders: bool = ...
def __init__( def __init__(
self, self,
verbose_name: Optional[Union[str, bytes]] = ..., path: Union[str, Callable[..., str]] = ...,
name: Optional[str] = ..., match: Optional[str] = ...,
path: str = ...,
match: Optional[Any] = ...,
recursive: bool = ..., recursive: bool = ...,
allow_files: bool = ..., allow_files: bool = ...,
allow_folders: bool = ..., allow_folders: bool = ...,
verbose_name: Optional[str] = ...,
name: Optional[str] = ...,
primary_key: bool = ..., primary_key: bool = ...,
max_length: int = ..., max_length: int = ...,
unique: bool = ..., unique: bool = ...,

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union, overload from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union, overload
from django.core.files.base import File from django.core.files.base import File
@@ -39,11 +40,10 @@ class FileField(Field):
upload_to: Union[str, Callable] = ... upload_to: Union[str, Callable] = ...
def __init__( def __init__(
self, self,
upload_to: Union[str, Callable, Path] = ...,
storage: Optional[Storage] = ...,
verbose_name: Optional[Union[str, bytes]] = ..., verbose_name: Optional[Union[str, bytes]] = ...,
name: Optional[str] = ..., name: Optional[str] = ...,
upload_to: Union[str, Callable] = ...,
storage: Optional[Storage] = ...,
primary_key: bool = ...,
max_length: Optional[int] = ..., max_length: Optional[int] = ...,
unique: bool = ..., unique: bool = ...,
blank: bool = ..., blank: bool = ...,

View File

@@ -108,6 +108,8 @@ class Options(Generic[_M]):
def managers(self) -> List[Manager]: ... def managers(self) -> List[Manager]: ...
@property @property
def managers_map(self) -> Dict[str, Manager]: ... def managers_map(self) -> Dict[str, Manager]: ...
@property
def db_returning_fields(self) -> List[Field]: ...
def get_field(self, field_name: Union[Callable, str]) -> Field: ... def get_field(self, field_name: Union[Callable, str]) -> Field: ...
def get_base_chain(self, model: Type[Model]) -> List[Type[Model]]: ... def get_base_chain(self, model: Type[Model]) -> List[Type[Model]]: ...
def get_parent_list(self) -> List[Type[Model]]: ... def get_parent_list(self) -> List[Type[Model]]: ...

View File

@@ -42,6 +42,7 @@ class Q(tree.Node):
class DeferredAttribute: class DeferredAttribute:
field_name: str = ... field_name: str = ...
field: Field
def __init__(self, field_name: str) -> None: ... def __init__(self, field_name: str) -> None: ...
class RegisterLookupMixin: class RegisterLookupMixin:

View File

@@ -1,6 +1,6 @@
import collections import collections
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union, Iterable
from django.db.models.lookups import Lookup, Transform from django.db.models.lookups import Lookup, Transform
from django.db.models.query_utils import PathInfo, RegisterLookupMixin from django.db.models.query_utils import PathInfo, RegisterLookupMixin
@@ -155,19 +155,19 @@ class Query:
def add_ordering(self, *ordering: Any) -> None: ... def add_ordering(self, *ordering: Any) -> None: ...
def clear_ordering(self, force_empty: bool) -> None: ... def clear_ordering(self, force_empty: bool) -> None: ...
def set_group_by(self) -> None: ... def set_group_by(self) -> None: ...
def add_select_related(self, fields: Tuple[str]) -> None: ... def add_select_related(self, fields: Iterable[str]) -> None: ...
def add_extra( def add_extra(
self, self,
select: Optional[Union[Dict[str, int], Dict[str, str], OrderedDict]], select: Optional[Dict[str, Any]],
select_params: Optional[Union[List[int], List[str], Tuple[int]]], select_params: Optional[Iterable[Any]],
where: Optional[List[str]], where: Optional[Sequence[str]],
params: Optional[List[str]], params: Optional[Sequence[str]],
tables: Optional[List[str]], tables: Optional[Sequence[str]],
order_by: Optional[Union[List[str], Tuple[str]]], order_by: Optional[Sequence[str]],
) -> None: ... ) -> None: ...
def clear_deferred_loading(self) -> None: ... def clear_deferred_loading(self) -> None: ...
def add_deferred_loading(self, field_names: Tuple[str]) -> None: ... def add_deferred_loading(self, field_names: Iterable[str]) -> None: ...
def add_immediate_loading(self, field_names: Tuple[str]) -> None: ... def add_immediate_loading(self, field_names: Iterable[str]) -> None: ...
def get_loaded_field_names(self) -> Dict[Type[Model], Set[str]]: ... def get_loaded_field_names(self) -> Dict[Type[Model], Set[str]]: ...
def get_loaded_field_names_cb( def get_loaded_field_names_cb(
self, target: Dict[Type[Model], Set[str]], model: Type[Model], fields: Set[Field] self, target: Dict[Type[Model], Set[str]], model: Type[Model], fields: Set[Field]

View File

@@ -1,4 +1,5 @@
from os.path import abspath from os.path import abspath
from pathlib import Path
from typing import Any, Union from typing import Any, Union
abspathu = abspath abspathu = abspath
@@ -7,3 +8,4 @@ def upath(path: Any): ...
def npath(path: Any): ... def npath(path: Any): ...
def safe_join(base: Union[bytes, str], *paths: Any) -> str: ... def safe_join(base: Union[bytes, str], *paths: Any) -> str: ...
def symlinks_supported() -> Any: ... def symlinks_supported() -> Any: ...
def to_path(value: Union[Path, str]) -> Path: ...

View File

@@ -5,6 +5,7 @@ from django.http.response import HttpResponse
class RemovedInDjango30Warning(PendingDeprecationWarning): ... class RemovedInDjango30Warning(PendingDeprecationWarning): ...
class RemovedInDjango31Warning(PendingDeprecationWarning): ... class RemovedInDjango31Warning(PendingDeprecationWarning): ...
class RemovedInDjango40Warning(PendingDeprecationWarning): ...
class RemovedInNextVersionWarning(DeprecationWarning): ... class RemovedInNextVersionWarning(DeprecationWarning): ...
class warn_about_renamed_method: class warn_about_renamed_method:

View File

@@ -25,6 +25,9 @@ def urlsafe_base64_decode(s: Union[bytes, str]) -> bytes: ...
def parse_etags(etag_str: str) -> List[str]: ... def parse_etags(etag_str: str) -> List[str]: ...
def quote_etag(etag_str: str) -> str: ... def quote_etag(etag_str: str) -> str: ...
def is_same_domain(host: str, pattern: str) -> bool: ... def is_same_domain(host: str, pattern: str) -> bool: ...
def url_has_allowed_host_and_scheme(
url: Optional[str], allowed_hosts: Optional[Union[str, Iterable[str]]], require_https: bool = ...
) -> bool: ...
def is_safe_url( def is_safe_url(
url: Optional[str], allowed_hosts: Optional[Union[str, Iterable[str]]], require_https: bool = ... url: Optional[str], allowed_hosts: Optional[Union[str, Iterable[str]]], require_https: bool = ...
) -> bool: ... ) -> bool: ...

View File

@@ -63,4 +63,4 @@ class ExceptionReporter:
): ... ): ...
def technical_404_response(request: HttpRequest, exception: Http404) -> HttpResponse: ... def technical_404_response(request: HttpRequest, exception: Http404) -> HttpResponse: ...
def default_urlconf(request: HttpRequest) -> HttpResponse: ... def default_urlconf(request: Optional[HttpResponse]) -> HttpResponse: ...

View File

@@ -1,3 +1,5 @@
import django
SECRET_KEY = '1' SECRET_KEY = '1'
SITE_ID = 1 SITE_ID = 1
@@ -41,7 +43,6 @@ test_modules = [
'bulk_create', 'bulk_create',
'cache', 'cache',
'check_framework', 'check_framework',
'choices',
'conditional_processing', 'conditional_processing',
'constraints', 'constraints',
'contenttypes_tests', 'contenttypes_tests',
@@ -219,6 +220,9 @@ test_modules = [
'wsgi', 'wsgi',
] ]
if django.VERSION[0] == 2:
test_modules += ['choices']
invalid_apps = { invalid_apps = {
'import_error_package', 'import_error_package',
} }

View File

@@ -264,6 +264,8 @@ IGNORED_ERRORS = {
'Incompatible types in assignment (expression has type "Type[Person', 'Incompatible types in assignment (expression has type "Type[Person',
'Incompatible types in assignment (expression has type "FloatModel", variable has type', 'Incompatible types in assignment (expression has type "FloatModel", variable has type',
'"ImageFile" has no attribute "was_opened"', '"ImageFile" has no attribute "was_opened"',
'Incompatible type for "size" of "FloatModel" (got "object", expected "Union[float, int, str, Combinable]")',
'Incompatible type for "value" of "IntegerModel" (got "object", expected',
], ],
'model_indexes': [ 'model_indexes': [
'Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"' 'Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"'
@@ -291,6 +293,9 @@ IGNORED_ERRORS = {
'model_options': [ 'model_options': [
'expression has type "Dict[str, Type[Model]]", target has type "OrderedDict', 'expression has type "Dict[str, Type[Model]]", target has type "OrderedDict',
], ],
'model_enums': [
"'bool' is not a valid base class",
],
'multiple_database': [ 'multiple_database': [
'Unexpected attribute "extra_arg" for model "Book"' 'Unexpected attribute "extra_arg" for model "Book"'
], ],
@@ -341,12 +346,14 @@ IGNORED_ERRORS = {
'"Collection[Any]" has no attribute "explain"', '"Collection[Any]" has no attribute "explain"',
"Cannot resolve keyword 'unknown_field' into field", "Cannot resolve keyword 'unknown_field' into field",
'Incompatible type for lookup \'tag\': (got "str", expected "Union[Tag, int, None]")', '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")' '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]"' 'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"',
'"FileLike" has no attribute "closed"',
], ],
'reverse_lookup': [ 'reverse_lookup': [
"Cannot resolve keyword 'choice' into field" "Cannot resolve keyword 'choice' into field"
@@ -424,6 +431,7 @@ IGNORED_ERRORS = {
'"WSGIRequest" has no attribute "process_response_content"', '"WSGIRequest" has no attribute "process_response_content"',
'No overload variant of "join" matches argument types "str", "None"', 'No overload variant of "join" matches argument types "str", "None"',
'Argument 1 to "Archive" has incompatible type "None"; expected "str"', 'Argument 1 to "Archive" has incompatible type "None"; expected "str"',
'Argument 1 to "to_path" has incompatible type "int"; expected "Union[Path, str]"',
], ],
'view_tests': [ 'view_tests': [
@@ -431,10 +439,15 @@ IGNORED_ERRORS = {
'Value of type "Optional[List[str]]" is not indexable', 'Value of type "Optional[List[str]]" is not indexable',
'ExceptionUser', 'ExceptionUser',
'view_tests.tests.test_debug.User', '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"', 'has no attribute "name"',
], ],
'wsgi': [
'"HttpResponse" has no attribute "block_size"',
],
} }

View File

@@ -2,15 +2,23 @@ import itertools
import shutil import shutil
import subprocess import subprocess
import sys import sys
from argparse import ArgumentParser
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Dict, List, Pattern, Union from typing import Dict, List, Pattern, Union
from git import Repo
from scripts.enabled_test_modules import ( from scripts.enabled_test_modules import (
EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS, EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS,
) )
DJANGO_COMMIT_REFS = {
'2.2': 'e8b0903976077b951795938b260211214ed7fe41',
'3.0': '7ec5962638144cbf4c2e47ea7d8dc02d1ce44394'
}
PROJECT_DIRECTORY = Path(__file__).parent.parent 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]: def get_unused_ignores(ignored_message_freq: Dict[str, Dict[Union[str, Pattern], int]]) -> List[str]:
@@ -67,11 +75,29 @@ def replace_with_clickable_location(error: str, abs_test_folder: Path) -> str:
return error.replace(raw_path, clickable_location) return error.replace(raw_path, clickable_location)
def get_django_repo_object() -> 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)
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()
commit_sha = DJANGO_COMMIT_REFS[args.django_version]
repo = get_django_repo_object()
if repo.head.commit.hexsha != commit_sha:
repo.git.fetch('origin')
repo.git.checkout(commit_sha)
mypy_config_file = (PROJECT_DIRECTORY / 'scripts' / 'mypy.ini').absolute() mypy_config_file = (PROJECT_DIRECTORY / 'scripts' / 'mypy.ini').absolute()
repo_directory = PROJECT_DIRECTORY / 'django-sources'
mypy_cache_dir = Path(__file__).parent / '.mypy_cache' mypy_cache_dir = Path(__file__).parent / '.mypy_cache'
tests_root = repo_directory / 'tests' tests_root = DJANGO_SOURCE_DIRECTORY / 'tests'
global_rc = 0 global_rc = 0
try: try: