Files
django-stubs/scripts/typecheck_tests.py
2019-01-30 22:43:09 +03:00

346 lines
10 KiB
Python

import os
import re
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Pattern
from git import Repo
from mypy import build
from mypy.main import process_options
# Django branch to typecheck against
DJANGO_BRANCH = 'stable/2.1.x'
# Specific commit in the Django repository to check against
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',
'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[',
'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',
re.compile('Cannot determine type of \'(objects|stuff|specimens|normal_manager)\''),
re.compile(r'"Callable\[\[(Any(, )?)+\], Any\]" has no attribute'),
re.compile(r'"HttpResponseBase" has no attribute "[A-Za-z_]+"'),
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]+"'),
]
# Test folders to typecheck
TESTS_DIRS = [
'absolute_url_overrides',
'admin_autodiscover',
'admin_changelist',
'admin_checks',
'admin_custom_urls',
'admin_default_site',
'admin_docs',
# TODO: 'admin_filters',
'admin_inlines',
'admin_ordering',
'admin_registration',
'admin_scripts',
# TODO: 'admin_utils',
# TODO: 'admin_views',
'admin_widgets',
'aggregation',
'aggregation_regress',
'annotations',
'app_loading',
'apps',
# TODO: auth_tests
'base',
'bash_completion',
'basic',
'builtin_server',
'bulk_create',
# TODO: 'cache',
# TODO: 'check_framework',
'choices',
'conditional_processing',
# TODO: 'contenttypes_tests',
'context_processors',
'csrf_tests',
'custom_columns',
# TODO: 'custom_lookups',
# TODO: 'custom_managers',
'custom_methods',
'custom_migration_operations',
'custom_pk',
'datatypes',
'dates',
'datetimes',
'db_functions',
'db_typecasts',
'db_utils',
'dbshell',
# TODO: 'decorators',
'defer',
# TODO: 'defer_regress',
'delete',
'delete_regress',
# TODO: 'deprecation',
# TODO: 'dispatch',
'distinct_on_fields',
'empty',
# TODO: 'expressions',
'expressions_case',
# TODO: 'expressions_window',
# TODO: 'extra_regress',
# TODO: 'field_deconstruction',
'field_defaults',
'field_subclassing',
# TODO: 'file_storage',
# TODO: 'file_uploads',
# TODO: 'files',
'filtered_relation',
# TODO: 'fixtures',
'fixtures_model_package',
# TODO: 'fixtures_regress',
'flatpages_tests',
'force_insert_update',
'foreign_object',
# TODO: 'forms_tests',
'from_db_value',
# TODO: 'generic_inline_admin',
# TODO: 'generic_relations',
'generic_relations_regress',
# TODO: 'generic_views',
'get_earliest_or_latest',
'get_object_or_404',
# TODO: 'get_or_create',
# TODO: 'gis_tests',
'handlers',
# TODO: 'httpwrappers',
'humanize_tests',
# TODO: 'i18n',
'import_error_package',
'indexes',
'inline_formsets',
'inspectdb',
'introspection',
# TODO: 'invalid_models_tests',
'known_related_objects',
# TODO: 'logging_tests',
# TODO: 'lookup',
'm2m_and_m2o',
'm2m_intermediary',
'm2m_multiple',
'm2m_recursive',
'm2m_regress',
'm2m_signals',
'm2m_through',
'm2m_through_regress',
'm2o_recursive',
# TODO: 'mail',
'managers_regress',
'many_to_many',
'many_to_one',
'many_to_one_null',
'max_lengths',
# TODO: 'messages_tests',
# TODO: 'middleware',
# TODO: 'middleware_exceptions',
# SKIPPED (all errors are false positives) 'migrate_signals',
'migration_test_data_persistence',
# TODO: 'migrations',
'migrations2',
# TODO: 'model_fields',
# TODO: 'model_forms',
'model_formsets',
'model_formsets_regress',
'model_indexes',
# TODO: 'model_inheritance',
'model_inheritance_regress',
# SKIPPED (all errors are false positives) 'model_meta',
'model_options',
'model_package',
'model_regress',
# TODO: 'modeladmin',
# TODO: 'multiple_database',
'mutually_referential',
'nested_foreign_keys',
'no_models',
'null_fk',
'null_fk_ordering',
'null_queries',
'one_to_one',
'or_lookups',
'order_with_respect_to',
'ordering',
'pagination',
# TODO: 'postgres_tests',
# TODO: 'prefetch_related',
'project_template',
'properties',
'proxy_model_inheritance',
# TODO: 'proxy_models',
# TODO: 'queries',
'queryset_pickle',
'raw_query',
'redirects_tests',
# TODO: 'requests',
'reserved_names',
'resolve_url',
# TODO: 'responses',
'reverse_lookup',
'save_delete_hooks',
'schema',
# TODO: 'select_for_update',
'select_related',
'select_related_onetoone',
'select_related_regress',
# TODO: 'serializers',
# TODO: 'servers',
'sessions_tests',
'settings_tests'
'shell',
# TODO: 'shortcuts',
# TODO: 'signals',
'signed_cookies_tests',
# TODO: 'signing',
# TODO: 'sitemaps_tests',
'sites_framework',
# TODO: 'sites_tests',
# TODO: 'staticfiles_tests',
'str',
'string_lookup',
'swappable_models',
# TODO: 'syndication_tests',
# TODO: 'template_backends',
'template_loader',
# TODO: 'template_tests',
# TODO: 'test_client',
# TODO: 'test_client_regress',
'test_exceptions',
# TODO: 'test_runner',
'test_runner_apps',
# TODO: 'test_utils',
# TODO: 'timezones',
'transaction_hooks',
# TODO: 'transactions',
'unmanaged_models',
# TODO: 'update',
'update_only_fields',
'urlpatterns',
# TODO: 'urlpatterns_reverse',
'user_commands',
# TODO: 'utils_tests',
# TODO: 'validation',
'validators',
'version',
# TODO: 'view_tests',
# TODO: 'wsgi',
]
@contextmanager
def cd(path):
"""Context manager to temporarily change working directories"""
if not path:
return
prev_cwd = Path.cwd().as_posix()
if isinstance(path, Path):
path = path.as_posix()
os.chdir(str(path))
try:
yield
finally:
os.chdir(prev_cwd)
def is_ignored(line: str) -> bool:
for pattern in IGNORED_ERROR_PATTERNS:
if isinstance(pattern, Pattern):
if pattern.search(line):
return True
else:
if pattern in line:
return True
return False
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):
error_happened = True
print(error_line)
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'
tests_root = repo_directory / 'tests'
global_rc = 0
# clone Django repository, if it does not exist
if not repo_directory.exists():
repo = Repo.clone_from('https://github.com/django/django.git', repo_directory)
else:
repo = Repo(repo_directory)
repo.remotes['origin'].pull(DJANGO_BRANCH)
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()}')
rc = check_with_mypy(abs_path, mypy_config_file)
if rc != 0:
global_rc = 1
sys.exit(rc)