diff --git a/.travis.yml b/.travis.yml index 7d380ed..d0ab33d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ jobs: - name: "Typecheck Django test suite" python: 3.7 script: | - xonsh ./scripts/typecheck_django_tests.xsh + python ./scripts/typecheck_tests.py - name: "Run plugin test suite with python 3.7" python: 3.7 @@ -25,7 +25,8 @@ jobs: before_install: | # Upgrade pip, setuptools, and wheel - pip install -U pip setuptools wheel xonsh + pip install -U pip setuptools wheel install: | pip install -r ./dev-requirements.txt + pip install -r ./scripts/typecheck-tests-requirements.txt diff --git a/scripts/typecheck-tests-requirements.txt b/scripts/typecheck-tests-requirements.txt new file mode 100644 index 0000000..5396f9c --- /dev/null +++ b/scripts/typecheck-tests-requirements.txt @@ -0,0 +1 @@ +gitpython \ No newline at end of file diff --git a/scripts/typecheck_django_tests.xsh b/scripts/typecheck_django_tests.xsh deleted file mode 100644 index d8972a2..0000000 --- a/scripts/typecheck_django_tests.xsh +++ /dev/null @@ -1,68 +0,0 @@ -import os -import sys - -if not os.path.exists('./django-sources'): - git clone -b stable/2.1.x https://github.com/django/django.git django-sources - -IGNORED_ERROR_PATTERNS = [ - 'Need type annotation for', - 'already defined on', - 'Cannot assign to a', - 'cannot perform relative import', - 'broken_app', - 'LazySettings', - 'Cannot infer type of lambda', - 'Incompatible types in assignment (expression has type "Callable[', - '"Callable[[Any], Any]" has no attribute', - 'Invalid value for a to= parameter' -] -TESTS_DIRS = [ - 'absolute_url_overrides', - 'admin_*', - 'aggregation', - 'aggregation_regress', - 'annotations', - 'app_loading', -] - -def check_file_in_the_current_directory(directory, fname): - rc = 0 - cd @(directory) - with ${...}.swap(FNAME=fname): - for line in $(mypy --config-file ../../../scripts/mypy.ini $FNAME).split('\n'): - for pattern in IGNORED_ERROR_PATTERNS: - if pattern in line: - break - else: - if line: - rc = 1 - print(line) - cd - - return rc - -def parse_ls_output_into_fnames(output): - fnames = [] - for line in output.splitlines()[1:]: - fnames.append(line.split()[-1]) - return fnames - -all_tests_dirs = [] -for test_dir in TESTS_DIRS: - with ${...}.swap(TEST_DIR=test_dir): - dirs = g`django-sources/tests/$TEST_DIR` - all_tests_dirs.extend(dirs) - -rc = 0 -for tests_dir in all_tests_dirs: - print('Checking ' + tests_dir) - abs_dir = os.path.join(os.getcwd(), tests_dir) - - with ${...}.swap(ABS_DIR=abs_dir): - ls_output = $(ls -lhv --color=auto -F --group-directories-first $ABS_DIR) - for fname in parse_ls_output_into_fnames(ls_output): - path_to_check = os.path.join(abs_dir, fname) - current_step_rc = check_file_in_the_current_directory(abs_dir, fname) - if current_step_rc != 0: - rc = current_step_rc - -sys.exit(rc) \ No newline at end of file diff --git a/scripts/typecheck_tests.py b/scripts/typecheck_tests.py new file mode 100644 index 0000000..2f78b5a --- /dev/null +++ b/scripts/typecheck_tests.py @@ -0,0 +1,96 @@ +import glob +import os +import sys +from contextlib import contextmanager +from pathlib import Path + +from git import Repo +from mypy import build +from mypy.main import process_options + +DJANGO_BRANCH = 'stable/2.1.x' +DJANGO_COMMIT_SHA = '03219b5f709dcd5b0bfacd963508625557ec1ef0' +IGNORED_ERROR_PATTERNS = [ + 'Need type annotation for', + 'already defined on', + 'Cannot assign to a', + 'cannot perform relative import', + 'broken_app', + 'LazySettings', + 'Cannot infer type of lambda', + 'Incompatible types in assignment (expression has type "Callable[', + '"Callable[[Any], Any]" has no attribute', + '"Callable[[Any, Any], Any]" has no attribute', + 'Invalid value for a to= parameter', + '"HttpResponseBase" has no attribute "user"' +] +TESTS_DIRS = [ + 'absolute_url_overrides', + # 'admin_*' +] + + +@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 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.remote().pull(DJANGO_COMMIT_SHA) + + branch = repo.heads[DJANGO_BRANCH] + branch.checkout() + assert repo.active_branch.name == DJANGO_BRANCH + assert repo.active_branch.commit.hexsha == DJANGO_COMMIT_SHA + + for dirname in TESTS_DIRS: + paths = glob.glob(str(tests_root / dirname)) + for path in paths: + abs_path = (project_directory / path).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)