diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ .cache .pytest_cache .tox/ +.mypy_cache/ debian/ package-lock.json yarn-error.log diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include requirements-test.txt include tox.ini include version.txt +recursive-include swh py.typed recursive-include swh/web/assets * recursive-include swh/web/static * recursive-include swh/web/templates * diff --git a/Makefile.local b/Makefile.local --- a/Makefile.local +++ b/Makefile.local @@ -87,3 +87,11 @@ test-frontend-full-ui: export CYPRESS_SKIP_SLOW_TESTS=0 test-frontend-full-ui: test-frontend-ui-cmd + + +# Override default rule to make sure DJANGO env var is properly set. It +# *should* work without any override thanks to the mypy django-stubs plugin, +# but it currently doesn't; see +# https://github.com/typeddjango/django-stubs/issues/166 +typecheck: + DJANGO_SETTINGS_MODULE=swh.web.settings.development $(MYPY) $(MYPYFLAGS) swh diff --git a/mypy.ini b/mypy.ini new file mode 100644 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,49 @@ +[mypy] +namespace_packages = True +warn_unused_ignores = True +# support for django magic: https://github.com/typeddjango/django-stubs +plugins = mypy_django_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = swh.web.settings.development + +# 3rd party libraries without stubs (yet) + +[mypy-bs4.*] +ignore_missing_imports = True + +[mypy-django_js_reverse.*] +ignore_missing_imports = True + +[mypy-htmlmin.*] +ignore_missing_imports = True + +[mypy-magic.*] +ignore_missing_imports = True + +[mypy-pkg_resources.*] +ignore_missing_imports = True + +[mypy-pygments.*] +ignore_missing_imports = True + +[mypy-pypandoc.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True + +[mypy-rest_framework.*] +ignore_missing_imports = True + +[mypy-requests_mock.*] +ignore_missing_imports = True + +[mypy-sphinx.*] +ignore_missing_imports = True + +[mypy-sphinxcontrib.*] +ignore_missing_imports = True + +[mypy-swh.docs.*] +ignore_missing_imports = True diff --git a/requirements-test.txt b/requirements-test.txt --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,6 +2,7 @@ pytest pytest-django pytest-mock +django-stubs requests-mock swh.core[http] >= 0.0.61 swh.loader.git >= 0.0.47 diff --git a/swh/__init__.py b/swh/__init__.py --- a/swh/__init__.py +++ b/swh/__init__.py @@ -1 +1,4 @@ -__path__ = __import__('pkgutil').extend_path(__path__, __name__) +from pkgutil import extend_path +from typing import Iterable + +__path__ = extend_path(__path__, __name__) # type: Iterable[str] diff --git a/swh/web/admin/deposit.py b/swh/web/admin/deposit.py --- a/swh/web/admin/deposit.py +++ b/swh/web/admin/deposit.py @@ -22,14 +22,14 @@ @admin_route(r'deposit/', view_name='admin-deposit') -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save(request): return render(request, 'admin/deposit.html') @admin_route(r'deposit/list/', view_name='admin-deposit-list') -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_deposit_list(request): table_data = {} table_data['draw'] = int(request.GET['draw']) diff --git a/swh/web/admin/origin_save.py b/swh/web/admin/origin_save.py --- a/swh/web/admin/origin_save.py +++ b/swh/web/admin/origin_save.py @@ -26,7 +26,7 @@ @admin_route(r'origin/save/', view_name='admin-origin-save') -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save(request): return render(request, 'admin/origin-save.html') @@ -68,7 +68,7 @@ @admin_route(r'origin/save/authorized_urls/add/(?P.+)/', view_name='admin-origin-save-add-authorized-url') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_add_authorized_url(request, origin_url): try: SaveAuthorizedOrigin.objects.get(url=origin_url) @@ -91,7 +91,7 @@ @admin_route(r'origin/save/authorized_urls/remove/(?P.+)/', view_name='admin-origin-save-remove-authorized-url') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_remove_authorized_url(request, origin_url): try: entry = SaveAuthorizedOrigin.objects.get(url=origin_url) @@ -105,7 +105,7 @@ @admin_route(r'origin/save/unauthorized_urls/list/', view_name='admin-origin-save-unauthorized-urls-list') -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_unauthorized_urls_list(request): unauthorized_urls = SaveUnauthorizedOrigin.objects.all() return _datatables_origin_urls_response(request, unauthorized_urls) @@ -114,7 +114,7 @@ @admin_route(r'origin/save/unauthorized_urls/add/(?P.+)/', view_name='admin-origin-save-add-unauthorized-url') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_add_unauthorized_url(request, origin_url): try: SaveUnauthorizedOrigin.objects.get(url=origin_url) @@ -137,7 +137,7 @@ @admin_route(r'origin/save/unauthorized_urls/remove/(?P.+)/', view_name='admin-origin-save-remove-unauthorized-url') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_remove_unauthorized_url(request, origin_url): try: entry = SaveUnauthorizedOrigin.objects.get(url=origin_url) @@ -152,7 +152,7 @@ @admin_route(r'origin/save/request/accept/(?P.+)/url/(?P.+)/', # noqa view_name='admin-origin-save-request-accept') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_request_accept(request, visit_type, origin_url): try: SaveAuthorizedOrigin.objects.get(url=origin_url) @@ -165,7 +165,7 @@ @admin_route(r'origin/save/request/reject/(?P.+)/url/(?P.+)/', # noqa view_name='admin-origin-save-request-reject') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_request_reject(request, visit_type, origin_url): try: SaveUnauthorizedOrigin.objects.get(url=origin_url) @@ -182,7 +182,7 @@ @admin_route(r'origin/save/request/remove/(?P.+)/', view_name='admin-origin-save-request-remove') @require_POST -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _admin_origin_save_request_remove(request, sor_id): try: entry = SaveOriginRequest.objects.get(id=sor_id) @@ -196,7 +196,7 @@ @admin_route(r'origin/save/task/info/(?P.+)/', view_name='admin-origin-save-task-info') -@staff_member_required(login_url=settings.LOGIN_URL) +@staff_member_required(view_func=None, login_url=settings.LOGIN_URL) def _save_origin_task_info(request, save_request_id): request_info = get_save_origin_task_info(save_request_id) for date_field in ('scheduled', 'started', 'ended'): diff --git a/swh/web/api/apiurls.py b/swh/web/api/apiurls.py --- a/swh/web/api/apiurls.py +++ b/swh/web/api/apiurls.py @@ -5,6 +5,8 @@ import functools +from typing import Dict + from rest_framework.decorators import api_view from swh.web.common.urlsindex import UrlsIndex @@ -20,8 +22,7 @@ generating related urls in API documentation """ - _apidoc_routes = {} - _method_endpoints = {} + _apidoc_routes = {} # type: Dict[str, Dict[str, str]] scope = 'api' @classmethod diff --git a/swh/web/common/highlightjs.py b/swh/web/common/highlightjs.py --- a/swh/web/common/highlightjs.py +++ b/swh/web/common/highlightjs.py @@ -1,10 +1,12 @@ -# Copyright (C) 2017-2018 The Software Heritage developers +# Copyright (C) 2017-2019 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import functools +from typing import Dict + from pygments.lexers import ( get_all_lexers, get_lexer_for_filename @@ -239,7 +241,7 @@ } # dictionary mapping pygment lexers to hljs languages -_pygments_lexer_to_hljs_language = {} +_pygments_lexer_to_hljs_language = {} # type: Dict[str, str] # dictionary mapping mime types to hljs languages diff --git a/swh/web/common/migrations/0001_initial.py b/swh/web/common/migrations/0001_initial.py --- a/swh/web/common/migrations/0001_initial.py +++ b/swh/web/common/migrations/0001_initial.py @@ -32,9 +32,6 @@ initial = True - dependencies = [ - ] - operations = [ migrations.CreateModel( name='SaveAuthorizedOrigin', diff --git a/swh/web/common/urlsindex.py b/swh/web/common/urlsindex.py --- a/swh/web/common/urlsindex.py +++ b/swh/web/common/urlsindex.py @@ -3,6 +3,10 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +from typing import Dict, List + +import django.urls + from django.conf.urls import url from django.shortcuts import redirect @@ -16,7 +20,7 @@ all declared patterns will be grouped under the default one. """ - _urlpatterns = {} + _urlpatterns = {} # type: Dict[str, List[django.urls.URLPattern]] scope = 'default' @classmethod diff --git a/swh/web/config.py b/swh/web/config.py --- a/swh/web/config.py +++ b/swh/web/config.py @@ -5,6 +5,8 @@ import os +from typing import Any, Dict + from swh.core import config from swh.indexer.storage import get_indexer_storage from swh.scheduler import get_scheduler @@ -97,7 +99,7 @@ 'history_counters_url': ('string', 'https://stats.export.softwareheritage.org/history_counters.json'), # noqa } -swhweb_config = {} +swhweb_config = {} # type: Dict[str, Any] def get_config(config_file='web/web'): diff --git a/swh/web/py.typed b/swh/web/py.typed new file mode 100644 --- /dev/null +++ b/swh/web/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. diff --git a/swh/web/settings/tests.py b/swh/web/settings/tests.py --- a/swh/web/settings/tests.py +++ b/swh/web/settings/tests.py @@ -100,4 +100,4 @@ ALLOWED_HOSTS += ['testserver'] # Silent DEBUG output when running unit tests - LOGGING['handlers']['console']['level'] = 'INFO' + LOGGING['handlers']['console']['level'] = 'INFO' # type: ignore diff --git a/swh/web/tests/data.py b/swh/web/tests/data.py --- a/swh/web/tests/data.py +++ b/swh/web/tests/data.py @@ -3,10 +3,11 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information -from copy import deepcopy import os import random +from copy import deepcopy +from typing import Dict from rest_framework.decorators import api_view from rest_framework.response import Response @@ -326,9 +327,9 @@ # Implement some special endpoints used to provide input tests data # when executing end to end tests with cypress -_content_code_data_exts = {} -_content_code_data_filenames = {} -_content_other_data_exts = {} +_content_code_data_exts = {} # type: Dict[str, Dict[str, str]] +_content_code_data_filenames = {} # type: Dict[str, Dict[str, str]] +_content_other_data_exts = {} # type: Dict[str, Dict[str, str]] def _init_content_tests_data(data_path, data_dict, ext_key): diff --git a/tox.ini b/tox.ini --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=flake8,py3 +envlist=flake8,mypy,py3 [testenv:py3] deps = @@ -24,3 +24,12 @@ commands = {envpython} -m flake8 \ --exclude=.tox,.git,__pycache__,.eggs,*.egg,node_modules + +[testenv:mypy] +setenv = DJANGO_SETTINGS_MODULE = swh.web.settings.development +skip_install = true +deps = + mypy + .[testing] +commands = + mypy swh