split error suppression for tests typechecking, fix ci, bunch of fixes

This commit is contained in:
Maxim Kurnikov
2019-02-03 19:33:51 +03:00
parent e409dbdb82
commit d03fddd96d
19 changed files with 191 additions and 105 deletions

View File

@@ -1,5 +1,5 @@
from django.contrib.admin.decorators import register as register
from django.contrib.admin.filters import (
from .decorators import register as register
from .filters import (
AllValuesFieldListFilter as AllValuesFieldListFilter,
BooleanFieldListFilter as BooleanFieldListFilter,
ChoicesFieldListFilter as ChoicesFieldListFilter,
@@ -10,14 +10,15 @@ from django.contrib.admin.filters import (
RelatedOnlyFieldListFilter as RelatedOnlyFieldListFilter,
SimpleListFilter as SimpleListFilter,
)
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME as ACTION_CHECKBOX_NAME
from django.contrib.admin.options import (
from .helpers import ACTION_CHECKBOX_NAME as ACTION_CHECKBOX_NAME
from .options import (
HORIZONTAL as HORIZONTAL,
VERTICAL as VERTICAL,
ModelAdmin as ModelAdmin,
StackedInline as StackedInline,
TabularInline as TabularInline,
)
from django.contrib.admin.sites import AdminSite as AdminSite, site as site
from .sites import AdminSite as AdminSite, site as site
from . import checks as checks
def autodiscover() -> None: ...

View File

@@ -11,11 +11,8 @@ def check_dependencies(**kwargs: Any) -> List[_CheckError]: ...
class BaseModelAdminChecks:
def check(self, admin_obj: BaseModelAdmin, **kwargs: Any) -> List[_CheckError]: ...
class ModelAdminChecks(BaseModelAdminChecks):
def check(self, admin_obj: ModelAdmin, **kwargs: Any) -> List[_CheckError]: ...
class InlineModelAdminChecks(BaseModelAdminChecks):
def check(self, inline_obj: InlineModelAdmin, **kwargs: Any) -> List[_CheckError]: ...
class ModelAdminChecks(BaseModelAdminChecks): ...
class InlineModelAdminChecks(BaseModelAdminChecks): ...
def must_be(type: Any, option: Any, obj: Any, id: Any): ...
def must_inherit_from(parent: Any, option: Any, obj: Any, id: Any): ...

View File

@@ -21,6 +21,7 @@ from django.urls.resolvers import URLPattern
from django.utils.safestring import SafeText
from django.db.models.fields import Field
from django.template.response import TemplateResponse
IS_POPUP_VAR: str
TO_FIELD_VAR: str
@@ -229,7 +230,7 @@ class ModelAdmin(BaseModelAdmin):
) -> HttpResponse: ...
def changelist_view(
self, request: WSGIRequest, extra_context: Optional[Dict[str, str]] = ...
) -> HttpResponseBase: ...
) -> TemplateResponse: ...
def get_deleted_objects(
self, objs: QuerySet, request: WSGIRequest
) -> Tuple[List[Any], Dict[Any, Any], Set[Any], List[Any]]: ...

View File

@@ -3,12 +3,14 @@ from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from uuid import UUID
from django.forms.models import ModelChoiceIterator
from django import forms
from django.contrib.admin.sites import AdminSite
from django.db.models.fields.reverse_related import ForeignObjectRel
from django.db.models.fields.reverse_related import ForeignObjectRel, ManyToOneRel, ManyToManyRel
from django.db.models.query_utils import Q
from django.forms.fields import Field
from django.forms.widgets import ChoiceWidget, Media, Widget
from django.forms.widgets import ChoiceWidget, Media, Widget, DateTimeBaseInput
from django.http.request import QueryDict
from django.utils.datastructures import MultiValueDict
@@ -92,7 +94,7 @@ def url_params_from_lookup_dict(
class ForeignKeyRawIdWidget(forms.TextInput):
attrs: Dict[Any, Any]
template_name: str = ...
rel: django.db.models.fields.reverse_related.ManyToOneRel = ...
rel: ManyToOneRel = ...
admin_site: AdminSite = ...
db: None = ...
def __init__(self, rel: ForeignObjectRel, admin_site: AdminSite, attrs: None = ..., using: None = ...) -> None: ...
@@ -107,7 +109,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
admin_site: AdminSite
attrs: Dict[Any, Any]
db: None
rel: django.db.models.fields.reverse_related.ManyToManyRel
rel: ManyToManyRel
template_name: str = ...
def get_context(
self, name: str, value: Optional[List[int]], attrs: Optional[Dict[str, str]]
@@ -122,8 +124,8 @@ class RelatedFieldWidgetWrapper(forms.Widget):
needs_multipart_form: bool = ...
attrs: Dict[Any, Any] = ...
choices: ModelChoiceIterator = ...
widget: django.contrib.admin.widgets.AutocompleteSelect = ...
rel: django.db.models.fields.reverse_related.ManyToOneRel = ...
widget: AutocompleteSelect = ...
rel: ManyToOneRel = ...
can_add_related: bool = ...
can_change_related: bool = ...
can_delete_related: bool = ...

View File

@@ -14,6 +14,9 @@ class SupportsLen(Protocol):
class SupportsCount(Protocol):
def count(self) -> int: ...
class SupportsOrdered(Protocol):
ordered: bool = ...
class Paginator:
object_list: QuerySet = ...
per_page: int = ...
@@ -21,7 +24,7 @@ class Paginator:
allow_empty_first_page: bool = ...
def __init__(
self,
object_list: Union[SupportsLen, SupportsCount],
object_list: Union[SupportsLen, SupportsCount, SupportsOrdered],
per_page: Union[int, str],
orphans: int = ...,
allow_empty_first_page: bool = ...,

View File

@@ -0,0 +1,6 @@
from django.dispatch import Signal
request_started: Signal = ...
request_finished: Signal = ...
got_request_exception: Signal = ...
setting_changed: Signal = ...

View File

@@ -47,6 +47,7 @@ from .fields.related import (
ForeignObject as ForeignObject,
)
from .fields.files import ImageField as ImageField, FileField as FileField
from .fields.proxy import OrderWrt as OrderWrt
from .deletion import (
CASCADE as CASCADE,
@@ -54,6 +55,7 @@ from .deletion import (
SET_NULL as SET_NULL,
DO_NOTHING as DO_NOTHING,
PROTECT as PROTECT,
SET as SET,
)
from .query import (
@@ -97,3 +99,5 @@ from .aggregates import (
)
from .indexes import Index as Index
from . import signals as signals

View File

@@ -1,3 +1,5 @@
from typing import Any, Callable
from django.db import IntegrityError
def CASCADE(collector, field, sub_objs, using): ...
@@ -5,5 +7,6 @@ def SET_NULL(collector, field, sub_objs, using): ...
def SET_DEFAULT(collector, field, sub_objs, using): ...
def DO_NOTHING(collector, field, sub_objs, using): ...
def PROTECT(collector, field, sub_objs, using): ...
def SET(value: Any) -> Callable: ...
class ProtectedError(IntegrityError): ...

View File

@@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any
from django.db.models import fields

View File

@@ -152,8 +152,10 @@ class Prefetch(object):
def get_current_queryset(self, level) -> Optional[QuerySet]: ...
def prefetch_related_objects(model_instances: Iterable[_T], *related_lookups: Union[str, Prefetch]) -> None: ...
def get_prefetcher(instance: _T, through_attr: str, to_attr: str) -> Tuple[Any, Any, bool, bool]: ...
def get_prefetcher(instance: Model, through_attr: str, to_attr: str) -> Tuple[Any, Any, bool, bool]: ...
class ModelIterable(Iterable[_T]):
def __iter__(self) -> Iterator[_T]: ...
class ModelIterable(Iterable[_T]): ...
class InstanceCheckMeta(type): ...
class EmptyQuerySet(metaclass=InstanceCheckMeta): ...

View File

@@ -7,7 +7,7 @@ from django.dispatch import Signal
class_prepared: Any
class ModelSignal(Signal):
def connect(
def connect( # type: ignore
self,
receiver: Callable,
sender: Optional[Union[Type[Model], str]] = ...,
@@ -15,7 +15,7 @@ class ModelSignal(Signal):
dispatch_uid: None = ...,
apps: Optional[Apps] = ...,
) -> None: ...
def disconnect(
def disconnect( # type: ignore
self,
receiver: Callable = ...,
sender: Optional[Union[Type[Model], str]] = ...,

View File

@@ -15,15 +15,15 @@ class Signal:
def __init__(self, providing_args: List[str] = ..., use_caching: bool = ...) -> None: ...
def connect(
self,
receiver: Any,
sender: Optional[Union[Type[Model], AppConfig]] = ...,
receiver: Callable,
sender: Optional[Union[Type[Model], AppConfig, str]] = ...,
weak: bool = ...,
dispatch_uid: Optional[str] = ...,
) -> None: ...
def disconnect(
self,
receiver: Optional[Callable] = ...,
sender: Optional[Union[Type[Model], AppConfig]] = ...,
sender: Optional[Union[Type[Model], AppConfig, str]] = ...,
dispatch_uid: Optional[str] = ...,
) -> bool: ...
def has_listeners(self, sender: Any = ...) -> bool: ...

View File

@@ -34,6 +34,9 @@ from .widgets import (
SelectMultiple as SelectMultiple,
TimeInput as TimeInput,
URLInput as URLInput,
SelectDateWidget as SelectDateWidget,
SplitHiddenDateTimeWidget as SplitHiddenDateTimeWidget,
SplitDateTimeWidget as SplitDateTimeWidget,
)
from .fields import (

View File

@@ -159,6 +159,7 @@ class SelectMultiple(Select):
allow_multiple_selected: bool = ...
class RadioSelect(ChoiceWidget):
can_add_related: bool
option_template_name: str = ...
class CheckboxSelectMultiple(ChoiceWidget):

View File

@@ -12,3 +12,5 @@ from .base import Node as Node, NodeList as NodeList, Origin as Origin, Template
from .context import Context as Context, RequestContext as RequestContext
from .library import Library as Library
from . import defaultfilters as defaultfilters

View File

@@ -1,4 +1,4 @@
from datetime import date, datetime, timedelta
from datetime import _date, datetime, timedelta
from decimal import Decimal
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
@@ -72,14 +72,14 @@ def unordered_list(
value: Union[Iterator[Any], List[Union[List[Union[List[Union[List[str], str]], str]], str]]], autoescape: bool = ...
) -> SafeText: ...
def add(
value: Union[List[int], Tuple[int, int], date, int, str],
value: Union[List[int], Tuple[int, int], _date, int, str],
arg: Union[List[int], Tuple[int, int], timedelta, int, str],
) -> Union[List[int], Tuple[int, int, int, int], date, int, str]: ...
) -> Union[List[int], Tuple[int, int, int, int], _date, int, str]: ...
def get_digit(value: Union[int, str], arg: int) -> Union[int, str]: ...
def date(value: Optional[Union[datetime, str]], arg: Optional[str] = ...) -> str: ...
def date(value: Optional[Union[_date, datetime, str]], arg: Optional[str] = ...) -> str: ...
def time(value: Optional[Union[datetime, str]], arg: Optional[str] = ...) -> str: ...
def timesince_filter(value: Optional[date], arg: Optional[date] = ...) -> str: ...
def timeuntil_filter(value: Optional[date], arg: Optional[date] = ...) -> str: ...
def timesince_filter(value: Optional[_date], arg: Optional[_date] = ...) -> str: ...
def timeuntil_filter(value: Optional[_date], arg: Optional[_date] = ...) -> str: ...
def default(value: Optional[Union[int, str]], arg: Union[int, str]) -> Union[int, str]: ...
def default_if_none(value: Optional[str], arg: Union[int, str]) -> Union[int, str]: ...
def divisibleby(value: int, arg: int) -> bool: ...

View File

@@ -1,4 +1,5 @@
from typing import Any
from django.core.signals import setting_changed as setting_changed
template_rendered: Any
COMPLEX_OVERRIDE_SETTINGS: Any

View File

@@ -1,5 +1,6 @@
import decimal
import warnings
from io import StringIO
from contextlib import contextmanager
from decimal import Decimal
from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Type, Union, IO
@@ -107,13 +108,13 @@ class isolate_apps(TestContextDecorator):
@contextmanager
def extend_sys_path(*paths: str) -> Iterator[None]: ...
@contextmanager
def captured_output(stream_name) -> Iterator[IO[str]]: ...
def captured_output(stream_name) -> Iterator[StringIO]: ...
@contextmanager
def captured_stdin() -> Iterator[IO[str]]: ...
def captured_stdin() -> Iterator[StringIO]: ...
@contextmanager
def captured_stdout() -> Iterator[IO[str]]: ...
def captured_stdout() -> Iterator[StringIO]: ...
@contextmanager
def captured_stderr() -> Iterator[IO[str]]: ...
def captured_stderr() -> Iterator[StringIO]: ...
@contextmanager
def freeze_time(t: float) -> Iterator[None]: ...
def tag(*tags: str): ...

View File

@@ -9,6 +9,8 @@ from git import Repo
from mypy import build
from mypy.main import process_options
PROJECT_DIRECTORY = Path(__file__).parent.parent
# Django branch to typecheck against
DJANGO_BRANCH = 'stable/2.1.x'
@@ -17,64 +19,114 @@ DJANGO_COMMIT_SHA = '03219b5f709dcd5b0bfacd963508625557ec1ef0'
# Some errors occur for the test suite itself, and cannot be addressed via django-stubs. They should be ignored
# using this constant.
IGNORED_ERROR_PATTERNS = [
'Need type annotation for',
'already defined on',
'Cannot assign to a',
'cannot perform relative import',
'broken_app',
'cache_clear',
'call_count',
'call_args_list',
'call_args',
'"password_changed" does not return a value',
'"validate_password" does not return a value',
MOCK_OBJECTS = ['MockRequest', 'MockCompiler', 'modelz']
IGNORED_ERRORS = {
'__common__': [
*MOCK_OBJECTS,
'LazySettings',
'Cannot infer type of lambda',
'"refresh_from_db" of "Model"',
'"as_sql" undefined in superclass',
'Incompatible types in assignment (expression has type "str", target has type "type")',
'Incompatible types in assignment (expression has type "Callable[',
'NullTranslations',
'Need type annotation for',
'Invalid value for a to= parameter',
'Incompatible types in assignment (expression has type "FilteredChildAdmin", variable has type "ChildAdmin")',
'Incompatible types in assignment (expression has type "RelatedFieldWidgetWrapper", variable has type "AdminRadioSelect")',
'has incompatible type "MockRequest"; expected "WSGIRequest"',
'"NullTranslations" has no attribute "_catalog"',
'Definition of "as_sql" in base class',
'expression has type "property"',
'"object" has no attribute "__iter__"',
'Too few arguments for "dates" of "QuerySet"',
'has no attribute "vendor"',
'Argument 1 to "get_list_or_404" has incompatible type "List',
'error: "AdminRadioSelect" has no attribute "can_add_related"',
'MockCompiler',
'SessionTestsMixin',
'Argument 1 to "Paginator" has incompatible type "ObjectList"',
'"Type[Morsel[Any]]" has no attribute "_reserved"',
'Argument 1 to "append" of "list"',
'Argument 1 to "bytes"',
'"full_clean" of "Model" does not return a value',
'"object" not callable',
'Item "GenericForeignKey" of "Union[GenericForeignKey, Model, None]" has no attribute "read_by"',
'Item "Model" of "Union[GenericForeignKey, Model, None]" has no attribute "read_by"',
re.compile('Cannot determine type of \'(objects|stuff|specimens|normal_manager)\''),
'already defined (possibly by an import)',
'Cannot assign to a type',
# forms <-> models plugin support
'"Model" has no attribute',
re.compile(r'Cannot determine type of \'(objects|stuff)\''),
# settings
re.compile(r'Module has no attribute "[A-Z_]+"'),
# attributes assigned to test functions
re.compile(r'"Callable\[\[(Any(, )?)+\], Any\]" has no attribute'),
re.compile(r'"HttpResponseBase" has no attribute "[A-Za-z_]+"'),
# assign empty tuple
re.compile(r'Incompatible types in assignment \(expression has type "Tuple\[\]", '
r'variable has type "Tuple\[[A-Za-z, ]+\]"'),
re.compile(r'"validate" of "[A-Za-z]+" does not return a value'),
re.compile(r'Module has no attribute "[A-Za-z_]+"'),
re.compile(r'"[A-Za-z\[\]]+" has no attribute "getvalue"'),
# TODO: remove when reassignment will be possible (in 0.670? )
re.compile(r'Incompatible types in assignment \(expression has type "(QuerySet|List)\[[A-Za-z, ]+\]", '
r'variable has type "(QuerySet|List)\[[A-Za-z, ]+\]"\)'),
re.compile(r'"(MockRequest|DummyRequest|DummyUser)" has no attribute "[a-zA-Z_]+"'),
# TODO: remove when form <-> model plugin support is added
re.compile(r'"Model" has no attribute "[A-Za-z_]+"'),
re.compile(r'Argument 1 to "get_object_or_404" has incompatible type "(str|Type\[CustomClass\])"'),
re.compile(r'"None" has no attribute "[a-zA-Z_0-9]+"'),
# assign method to a method
'Cannot assign to a method',
'Cannot infer type of lambda',
re.compile(r'Incompatible types in assignment \(expression has type "Callable\[\[(Any(, )?)+\], Any\]", '
r'variable has type "Callable\['),
],
'admin_changelist': [
'Incompatible types in assignment (expression has type "FilteredChildAdmin", variable has type "ChildAdmin")'
],
'admin_scripts': [
'Incompatible types in assignment (expression has type "Callable['
],
'admin_widgets': [
'Incompatible types in assignment (expression has type "RelatedFieldWidgetWrapper", '
'variable has type "AdminRadioSelect")',
'Incompatible types in assignment (expression has type "Widget", variable has type "AutocompleteSelect")'
],
'aggregation': [
'Incompatible types in assignment (expression has type "QuerySet[Any]", variable has type "List[Any]")',
'"as_sql" undefined in superclass'
],
'aggregation_regress': [
'Incompatible types in assignment (expression has type "List[str]", variable has type "QuerySet[Author]")'
],
'basic': [
'Unexpected keyword argument "unknown_kwarg" for "refresh_from_db" of "Model"',
'"refresh_from_db" of "Model" defined here'
],
'builtin_server': [
'has no attribute "getvalue"'
],
'csrf_tests': [
'Incompatible types in assignment (expression has type "property", ' +
'base class "HttpRequest" defined the type as "QueryDict")'
],
'dates': [
'Too few arguments for "dates" of "QuerySet"',
],
'defer': [
'Too many arguments for "refresh_from_db" of "Model"'
],
'db_typecasts': [
'"object" has no attribute "__iter__"; maybe "__str__" or "__dir__"? (not iterable)'
],
'from_db_value': [
'has no attribute "vendor"'
],
'get_object_or_404': [
'Argument 1 to "get_object_or_404" has incompatible type "str"; '
+ 'expected "Union[Type[Model], Manager[Any], QuerySet[Any]]"',
'Argument 1 to "get_object_or_404" has incompatible type "Type[CustomClass]"; '
+ 'expected "Union[Type[Model], Manager[Any], QuerySet[Any]]"',
'Argument 1 to "get_list_or_404" has incompatible type "List[Type[Article]]"; '
+ 'expected "Union[Type[Model], Manager[Any], QuerySet[Any]]"'
],
'model_inheritance_regress': [
'Incompatible types in assignment (expression has type "List[Supplier]", variable has type "QuerySet[Supplier]")'
],
'model_meta': [
'"object" has no attribute "items"',
'"Field" has no attribute "many_to_many"'
],
'migrate_signals': [
'Value of type "None" is not indexable',
],
'queryset_pickle': [
'"None" has no attribute "somefield"'
],
'prefetch_related': [
'Incompatible types in assignment (expression has type "List[Room]", variable has type "QuerySet[Room]")',
'"None" has no attribute "__iter__"',
'has no attribute "read_by"'
],
'urlpatterns': [
'"object" has no attribute "__iter__"; maybe "__str__" or "__dir__"? (not iterable)',
'"object" not callable'
],
'user_commands': [
'Incompatible types in assignment (expression has type "Callable[[Any, KwArg(Any)], Any]", variable has type'
],
'sessions_tests': [
'base class "SessionTestsMixin" defined the type as "None")',
'has no attribute "_reserved"'
],
'select_related_onetoone': [
'"None" has no attribute'
]
}
# Test folders to typecheck
TESTS_DIRS = [
'absolute_url_overrides',
@@ -190,7 +242,7 @@ TESTS_DIRS = [
# TODO: 'messages_tests',
# TODO: 'middleware',
# TODO: 'middleware_exceptions',
# SKIPPED (all errors are false positives) 'migrate_signals',
'migrate_signals',
'migration_test_data_persistence',
# TODO: 'migrations',
'migrations2',
@@ -201,7 +253,7 @@ TESTS_DIRS = [
'model_indexes',
# TODO: 'model_inheritance',
'model_inheritance_regress',
# SKIPPED (all errors are false positives) 'model_meta',
'model_meta',
'model_options',
'model_package',
'model_regress',
@@ -298,8 +350,8 @@ def cd(path):
os.chdir(prev_cwd)
def is_ignored(line: str) -> bool:
for pattern in IGNORED_ERROR_PATTERNS:
def is_ignored(line: str, test_folder_name: str) -> bool:
for pattern in IGNORED_ERRORS['__common__'] + IGNORED_ERRORS.get(test_folder_name, []):
if isinstance(pattern, Pattern):
if pattern.search(line):
return True
@@ -309,22 +361,29 @@ def is_ignored(line: str) -> bool:
return False
def replace_with_clickable_location(error: str, abs_test_folder: Path) -> str:
raw_path, _, error_line = error.partition(': ')
fname, line_number = raw_path.split(':')
path = abs_test_folder.joinpath(fname).relative_to(PROJECT_DIRECTORY)
clickable_location = f'./{path}:{line_number}'
return error.replace(raw_path, clickable_location)
def check_with_mypy(abs_path: Path, config_file_path: Path) -> int:
error_happened = False
with cd(abs_path):
sources, options = process_options(['--config-file', str(config_file_path), str(abs_path)])
res = build.build(sources, options)
for error_line in res.errors:
if not is_ignored(error_line):
if not is_ignored(error_line, abs_path.name):
error_happened = True
print(error_line)
print(replace_with_clickable_location(error_line, abs_test_folder=abs_path))
return int(error_happened)
if __name__ == '__main__':
project_directory = Path(__file__).parent.parent
mypy_config_file = (project_directory / 'scripts' / 'mypy.ini').absolute()
repo_directory = project_directory / 'django-sources'
mypy_config_file = (PROJECT_DIRECTORY / 'scripts' / 'mypy.ini').absolute()
repo_directory = PROJECT_DIRECTORY / 'django-sources'
tests_root = repo_directory / 'tests'
global_rc = 0
@@ -337,8 +396,8 @@ if __name__ == '__main__':
repo.git.checkout(DJANGO_COMMIT_SHA)
for dirname in TESTS_DIRS:
abs_path = (project_directory / tests_root / dirname).absolute()
print(f'Checking {abs_path.as_uri()}')
abs_path = (PROJECT_DIRECTORY / tests_root / dirname).absolute()
print(f'Checking {abs_path}')
rc = check_with_mypy(abs_path, mypy_config_file)
if rc != 0: