Skip to content

Commit 4ac43c6

Browse files
authored
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
1 parent cadd6c9 commit 4ac43c6

18 files changed

+125
-34
lines changed

.gitmodules

Lines changed: 0 additions & 4 deletions
This file was deleted.

.travis.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,23 @@ jobs:
88
python: 3.7
99
script: 'pytest'
1010

11-
- name: Typecheck Django test suite with python 3.7
11+
- name: Typecheck Django 3.0 test suite with python 3.7
1212
python: 3.7
13-
script: 'python ./scripts/typecheck_tests.py'
13+
script: |
14+
pip install Django==3.0.*
15+
python ./scripts/typecheck_tests.py --django_version=3.0
1416
15-
- name: Typecheck Django test suite with python 3.6
17+
- name: Typecheck Django 3.0 test suite with python 3.6
1618
python: 3.6
17-
script: 'python ./scripts/typecheck_tests.py'
19+
script: |
20+
pip install Django==3.0.*
21+
python ./scripts/typecheck_tests.py --django_version=3.0
22+
23+
- name: Typecheck Django 2.2 test suite with python 3.7
24+
python: 3.7
25+
script: |
26+
pip install Django==2.2.*
27+
python ./scripts/typecheck_tests.py --django_version=2.2
1828
1929
- name: Mypy for plugin code
2030
python: 3.7

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ psycopg2
44
flake8==3.7.8
55
flake8-pyi==19.3.0
66
isort==4.3.21
7+
gitpython==3.0.5
78
-e .

django-sources

Lines changed: 0 additions & 1 deletion
This file was deleted.

django-stubs/db/models/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,5 @@ from .constraints import (
129129
CheckConstraint as CheckConstraint,
130130
UniqueConstraint as UniqueConstraint,
131131
)
132+
133+
from .enums import Choices as Choices, IntegerChoices as IntegerChoices, TextChoices as TextChoices

django-stubs/db/models/enums.pyi

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import enum
2+
from typing import Any, List, Tuple
3+
4+
class ChoicesMeta(enum.EnumMeta):
5+
names: List[str] = ...
6+
choices: List[Tuple[Any, str]] = ...
7+
labels: List[str] = ...
8+
values: List[Any] = ...
9+
def __contains__(self, item: Any) -> bool: ...
10+
11+
class Choices(enum.Enum, metaclass=ChoicesMeta):
12+
def __str__(self): ...
13+
14+
# fake
15+
class _IntegerChoicesMeta(ChoicesMeta):
16+
names: List[str] = ...
17+
choices: List[Tuple[int, str]] = ...
18+
labels: List[str] = ...
19+
values: List[int] = ...
20+
21+
class IntegerChoices(int, Choices, metaclass=_IntegerChoicesMeta): ...
22+
23+
# fake
24+
class _TextChoicesMeta(ChoicesMeta):
25+
names: List[str] = ...
26+
choices: List[Tuple[str, str]] = ...
27+
labels: List[str] = ...
28+
values: List[str] = ...
29+
30+
class TextChoices(str, Choices, metaclass=_TextChoicesMeta): ...

django-stubs/db/models/fields/__init__.pyi

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
108108
def db_parameters(self, connection: Any) -> Dict[str, str]: ...
109109
def get_prep_value(self, value: Any) -> Any: ...
110110
def get_internal_type(self) -> str: ...
111-
def formfield(self, **kwargs) -> FormField: ...
111+
# TODO: plugin support
112+
def formfield(self, **kwargs) -> Any: ...
112113
def save_form_data(self, instance: Model, data: Any) -> None: ...
113114
def contribute_to_class(self, cls: Type[Model], name: str, private_only: bool = ...) -> None: ...
114115
def to_python(self, value: Any) -> Any: ...
@@ -361,20 +362,20 @@ class UUIDField(Field[_ST, _GT]):
361362
_pyi_private_get_type: uuid.UUID
362363

363364
class FilePathField(Field[_ST, _GT]):
364-
path: str = ...
365-
match: Optional[Any] = ...
365+
path: Any = ...
366+
match: Optional[str] = ...
366367
recursive: bool = ...
367368
allow_files: bool = ...
368369
allow_folders: bool = ...
369370
def __init__(
370371
self,
371-
verbose_name: Optional[Union[str, bytes]] = ...,
372-
name: Optional[str] = ...,
373-
path: str = ...,
374-
match: Optional[Any] = ...,
372+
path: Union[str, Callable[..., str]] = ...,
373+
match: Optional[str] = ...,
375374
recursive: bool = ...,
376375
allow_files: bool = ...,
377376
allow_folders: bool = ...,
377+
verbose_name: Optional[str] = ...,
378+
name: Optional[str] = ...,
378379
primary_key: bool = ...,
379380
max_length: int = ...,
380381
unique: bool = ...,

django-stubs/db/models/fields/files.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
from typing import Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union, overload
23

34
from django.core.files.base import File
@@ -39,11 +40,10 @@ class FileField(Field):
3940
upload_to: Union[str, Callable] = ...
4041
def __init__(
4142
self,
43+
upload_to: Union[str, Callable, Path] = ...,
44+
storage: Optional[Storage] = ...,
4245
verbose_name: Optional[Union[str, bytes]] = ...,
4346
name: Optional[str] = ...,
44-
upload_to: Union[str, Callable] = ...,
45-
storage: Optional[Storage] = ...,
46-
primary_key: bool = ...,
4747
max_length: Optional[int] = ...,
4848
unique: bool = ...,
4949
blank: bool = ...,

django-stubs/db/models/options.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ class Options(Generic[_M]):
108108
def managers(self) -> List[Manager]: ...
109109
@property
110110
def managers_map(self) -> Dict[str, Manager]: ...
111+
@property
112+
def db_returning_fields(self) -> List[Field]: ...
111113
def get_field(self, field_name: Union[Callable, str]) -> Field: ...
112114
def get_base_chain(self, model: Type[Model]) -> List[Type[Model]]: ...
113115
def get_parent_list(self) -> List[Type[Model]]: ...

django-stubs/db/models/query_utils.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Q(tree.Node):
4242

4343
class DeferredAttribute:
4444
field_name: str = ...
45+
field: Field
4546
def __init__(self, field_name: str) -> None: ...
4647

4748
class RegisterLookupMixin:

django-stubs/db/models/sql/query.pyi

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import collections
22
from collections import OrderedDict, namedtuple
3-
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union
3+
from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union, Iterable
44

55
from django.db.models.lookups import Lookup, Transform
66
from django.db.models.query_utils import PathInfo, RegisterLookupMixin
@@ -155,19 +155,19 @@ class Query:
155155
def add_ordering(self, *ordering: Any) -> None: ...
156156
def clear_ordering(self, force_empty: bool) -> None: ...
157157
def set_group_by(self) -> None: ...
158-
def add_select_related(self, fields: Tuple[str]) -> None: ...
158+
def add_select_related(self, fields: Iterable[str]) -> None: ...
159159
def add_extra(
160160
self,
161-
select: Optional[Union[Dict[str, int], Dict[str, str], OrderedDict]],
162-
select_params: Optional[Union[List[int], List[str], Tuple[int]]],
163-
where: Optional[List[str]],
164-
params: Optional[List[str]],
165-
tables: Optional[List[str]],
166-
order_by: Optional[Union[List[str], Tuple[str]]],
161+
select: Optional[Dict[str, Any]],
162+
select_params: Optional[Iterable[Any]],
163+
where: Optional[Sequence[str]],
164+
params: Optional[Sequence[str]],
165+
tables: Optional[Sequence[str]],
166+
order_by: Optional[Sequence[str]],
167167
) -> None: ...
168168
def clear_deferred_loading(self) -> None: ...
169-
def add_deferred_loading(self, field_names: Tuple[str]) -> None: ...
170-
def add_immediate_loading(self, field_names: Tuple[str]) -> None: ...
169+
def add_deferred_loading(self, field_names: Iterable[str]) -> None: ...
170+
def add_immediate_loading(self, field_names: Iterable[str]) -> None: ...
171171
def get_loaded_field_names(self) -> Dict[Type[Model], Set[str]]: ...
172172
def get_loaded_field_names_cb(
173173
self, target: Dict[Type[Model], Set[str]], model: Type[Model], fields: Set[Field]

django-stubs/utils/_os.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from os.path import abspath
2+
from pathlib import Path
23
from typing import Any, Union
34

45
abspathu = abspath
@@ -7,3 +8,4 @@ def upath(path: Any): ...
78
def npath(path: Any): ...
89
def safe_join(base: Union[bytes, str], *paths: Any) -> str: ...
910
def symlinks_supported() -> Any: ...
11+
def to_path(value: Union[Path, str]) -> Path: ...

django-stubs/utils/deprecation.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from django.http.response import HttpResponse
55

66
class RemovedInDjango30Warning(PendingDeprecationWarning): ...
77
class RemovedInDjango31Warning(PendingDeprecationWarning): ...
8+
class RemovedInDjango40Warning(PendingDeprecationWarning): ...
89
class RemovedInNextVersionWarning(DeprecationWarning): ...
910

1011
class warn_about_renamed_method:

django-stubs/utils/http.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ def urlsafe_base64_decode(s: Union[bytes, str]) -> bytes: ...
2525
def parse_etags(etag_str: str) -> List[str]: ...
2626
def quote_etag(etag_str: str) -> str: ...
2727
def is_same_domain(host: str, pattern: str) -> bool: ...
28+
def url_has_allowed_host_and_scheme(
29+
url: Optional[str], allowed_hosts: Optional[Union[str, Iterable[str]]], require_https: bool = ...
30+
) -> bool: ...
2831
def is_safe_url(
2932
url: Optional[str], allowed_hosts: Optional[Union[str, Iterable[str]]], require_https: bool = ...
3033
) -> bool: ...

django-stubs/views/debug.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@ class ExceptionReporter:
6363
): ...
6464

6565
def technical_404_response(request: HttpRequest, exception: Http404) -> HttpResponse: ...
66-
def default_urlconf(request: HttpRequest) -> HttpResponse: ...
66+
def default_urlconf(request: Optional[HttpResponse]) -> HttpResponse: ...

scripts/django_tests_settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import django
2+
13
SECRET_KEY = '1'
24
SITE_ID = 1
35

@@ -41,7 +43,6 @@
4143
'bulk_create',
4244
'cache',
4345
'check_framework',
44-
'choices',
4546
'conditional_processing',
4647
'constraints',
4748
'contenttypes_tests',
@@ -219,6 +220,9 @@
219220
'wsgi',
220221
]
221222

223+
if django.VERSION[0] == 2:
224+
test_modules += ['choices']
225+
222226
invalid_apps = {
223227
'import_error_package',
224228
}

scripts/enabled_test_modules.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@
264264
'Incompatible types in assignment (expression has type "Type[Person',
265265
'Incompatible types in assignment (expression has type "FloatModel", variable has type',
266266
'"ImageFile" has no attribute "was_opened"',
267+
'Incompatible type for "size" of "FloatModel" (got "object", expected "Union[float, int, str, Combinable]")',
268+
'Incompatible type for "value" of "IntegerModel" (got "object", expected',
267269
],
268270
'model_indexes': [
269271
'Argument "condition" to "Index" has incompatible type "str"; expected "Optional[Q]"'
@@ -291,6 +293,9 @@
291293
'model_options': [
292294
'expression has type "Dict[str, Type[Model]]", target has type "OrderedDict',
293295
],
296+
'model_enums': [
297+
"'bool' is not a valid base class",
298+
],
294299
'multiple_database': [
295300
'Unexpected attribute "extra_arg" for model "Book"'
296301
],
@@ -341,12 +346,14 @@
341346
'"Collection[Any]" has no attribute "explain"',
342347
"Cannot resolve keyword 'unknown_field' into field",
343348
'Incompatible type for lookup \'tag\': (got "str", expected "Union[Tag, int, None]")',
349+
'No overload variant of "__getitem__" of "QuerySet" matches argument type "str"',
344350
],
345351
'requests': [
346352
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
347353
],
348354
'responses': [
349-
'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"'
355+
'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"',
356+
'"FileLike" has no attribute "closed"',
350357
],
351358
'reverse_lookup': [
352359
"Cannot resolve keyword 'choice' into field"
@@ -424,17 +431,23 @@
424431
'"WSGIRequest" has no attribute "process_response_content"',
425432
'No overload variant of "join" matches argument types "str", "None"',
426433
'Argument 1 to "Archive" has incompatible type "None"; expected "str"',
434+
'Argument 1 to "to_path" has incompatible type "int"; expected "Union[Path, str]"',
427435

428436
],
429437
'view_tests': [
430438
"Module 'django.views.debug' has no attribute 'Path'",
431439
'Value of type "Optional[List[str]]" is not indexable',
432440
'ExceptionUser',
433441
'view_tests.tests.test_debug.User',
442+
'Exception must be derived from BaseException',
443+
"No binding for nonlocal 'tb_frames' found",
434444
],
435445
'validation': [
436446
'has no attribute "name"',
437447
],
448+
'wsgi': [
449+
'"HttpResponse" has no attribute "block_size"',
450+
],
438451
}
439452

440453

scripts/typecheck_tests.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22
import shutil
33
import subprocess
44
import sys
5+
from argparse import ArgumentParser
56
from collections import defaultdict
67
from pathlib import Path
78
from typing import Dict, List, Pattern, Union
89

10+
from git import Repo
11+
912
from scripts.enabled_test_modules import (
1013
EXTERNAL_MODULES, IGNORED_ERRORS, IGNORED_MODULES, MOCK_OBJECTS,
1114
)
1215

16+
DJANGO_COMMIT_REFS = {
17+
'2.2': 'e8b0903976077b951795938b260211214ed7fe41',
18+
'3.0': '7ec5962638144cbf4c2e47ea7d8dc02d1ce44394'
19+
}
1320
PROJECT_DIRECTORY = Path(__file__).parent.parent
21+
DJANGO_SOURCE_DIRECTORY = PROJECT_DIRECTORY / 'django-sources' # type: Path
1422

1523

1624
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:
6775
return error.replace(raw_path, clickable_location)
6876

6977

78+
def get_django_repo_object() -> Repo:
79+
if not DJANGO_SOURCE_DIRECTORY.exists():
80+
DJANGO_SOURCE_DIRECTORY.mkdir(exist_ok=True, parents=False)
81+
return Repo.clone_from('https://github.com/django/django.git', DJANGO_SOURCE_DIRECTORY)
82+
else:
83+
repo = Repo(DJANGO_SOURCE_DIRECTORY)
84+
return repo
85+
86+
7087
if __name__ == '__main__':
88+
parser = ArgumentParser()
89+
parser.add_argument('--django_version', choices=['2.2', '3.0'], required=True)
90+
args = parser.parse_args()
91+
92+
commit_sha = DJANGO_COMMIT_REFS[args.django_version]
93+
repo = get_django_repo_object()
94+
if repo.head.commit.hexsha != commit_sha:
95+
repo.git.fetch('origin')
96+
repo.git.checkout(commit_sha)
97+
7198
mypy_config_file = (PROJECT_DIRECTORY / 'scripts' / 'mypy.ini').absolute()
72-
repo_directory = PROJECT_DIRECTORY / 'django-sources'
7399
mypy_cache_dir = Path(__file__).parent / '.mypy_cache'
74-
tests_root = repo_directory / 'tests'
100+
tests_root = DJANGO_SOURCE_DIRECTORY / 'tests'
75101
global_rc = 0
76102

77103
try:

0 commit comments

Comments
 (0)