From 9d1ebac86e60cf439c37587c3413f95dfe817f48 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Wed, 23 Aug 2017 20:51:06 +0200 Subject: [PATCH] Add typing (#3370) Add typing --- .gitignore | 3 +++ .travis.yml | 3 ++- openslides/assignments/models.py | 4 +++- openslides/motions/models.py | 4 +++- openslides/poll/models.py | 3 ++- openslides/users/models.py | 4 ++-- openslides/utils/auth.py | 12 +++++++++--- openslides/utils/cache.py | 2 +- openslides/utils/collection.py | 4 +++- openslides/utils/dispatch.py | 2 +- openslides/utils/models.py | 3 ++- openslides/utils/projector.py | 4 +++- openslides/utils/rest_api.py | 4 +++- openslides/utils/views.py | 4 +++- requirements.txt | 1 + setup.cfg | 7 +++++++ tests/unit/config/test_api.py | 11 +---------- 17 files changed, 49 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 350707c84..cec005a74 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ tests/file/* # Plugin development openslides_* + +# Mypy cache for typechecking +.mypy_cache diff --git a/.travis.yml b/.travis.yml index 4944960b8..93e6c50c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,8 @@ install: - node_modules/.bin/gulp --production script: - flake8 openslides tests - - isort --check-only --recursive openslides tests + - if [ "`python --version`" \> "Python 3.5.0" ]; then isort --check-only --recursive openslides tests; fi + - python -m mypy openslides/ - node_modules/.bin/gulp jshint - node_modules/.bin/karma start --browsers PhantomJS tests/karma/karma.conf.js diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index 7bdabd27a..8fad3e681 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -399,7 +399,9 @@ class AssignmentOption(RESTModelMixin, BaseOption): return self.poll.assignment -class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, +# TODO: remove the type-ignoring in the next line, after this is solved: +# https://github.com/python/mypy/issues/3855 +class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore PublishPollMixin, BasePoll): option_class = AssignmentOption diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 896b8ab35..fd81d677b 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -994,7 +994,9 @@ class MotionOption(RESTModelMixin, BaseOption): return self.poll.motion -class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): +# TODO: remove the type-ignoring in the next line, after this is solved: +# https://github.com/python/mypy/issues/3855 +class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore """The Class to saves the vote result for a motion poll.""" motion = models.ForeignKey( diff --git a/openslides/poll/models.py b/openslides/poll/models.py index bbcdca7cb..545e3f111 100644 --- a/openslides/poll/models.py +++ b/openslides/poll/models.py @@ -1,4 +1,5 @@ import locale +from typing import Type # noqa from django.core.exceptions import ObjectDoesNotExist from django.db import models @@ -16,7 +17,7 @@ class BaseOption(models.Model): which has to be a subclass of BaseVote. Otherwise you have to override the get_vote_class method. """ - vote_class = None + vote_class = None # type: Type[BaseVote] class Meta: abstract = True diff --git a/openslides/users/models.py b/openslides/users/models.py index d2a39b4b4..35c55fd49 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -2,10 +2,10 @@ from random import choice from django.contrib.auth.hashers import make_password from django.contrib.auth.models import Group as DjangoGroup +from django.contrib.auth.models import GroupManager as _GroupManager from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, - GroupManager, Permission, PermissionsMixin, ) @@ -229,7 +229,7 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): raise RuntimeError('Do not use user.has_perm() but use openslides.utils.auth.has_perm') -class GroupManager(GroupManager): +class GroupManager(_GroupManager): """ Customized manager that supports our get_full_queryset method. """ diff --git a/openslides/utils/auth.py b/openslides/utils/auth.py index ca22c322d..e61c44a86 100644 --- a/openslides/utils/auth.py +++ b/openslides/utils/auth.py @@ -1,10 +1,13 @@ +from typing import Optional, Union + from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser +from django.db.models import Model from .collection import CollectionElement -def has_perm(user, perm): +def has_perm(user: Optional[CollectionElement], perm: str) -> bool: """ Checks that user has a specific permission. @@ -34,7 +37,7 @@ def has_perm(user, perm): return has_perm -def anonymous_is_enabled(): +def anonymous_is_enabled() -> bool: """ Returns True if the anonymous user is enabled in the settings. """ @@ -42,7 +45,10 @@ def anonymous_is_enabled(): .get_full_data()['value']) -def user_to_collection_user(user): +AnyUser = Union[Model, CollectionElement, int, AnonymousUser, None] + + +def user_to_collection_user(user: AnyUser) -> Optional[CollectionElement]: """ Takes an object, that represents a user and converts it to a CollectionElement or to None, if it is an anonymous user. diff --git a/openslides/utils/cache.py b/openslides/utils/cache.py index 7ea2d92f2..be801f8ca 100644 --- a/openslides/utils/cache.py +++ b/openslides/utils/cache.py @@ -261,6 +261,6 @@ def get_redis_connection(): if use_redis_cache(): - websocket_user_cache = RedisWebsocketUserCache() + websocket_user_cache = RedisWebsocketUserCache() # type: BaseWebsocketUserCache else: websocket_user_cache = DjangoCacheWebsocketUserCache() diff --git a/openslides/utils/collection.py b/openslides/utils/collection.py index ab50036cd..43f47a025 100644 --- a/openslides/utils/collection.py +++ b/openslides/utils/collection.py @@ -1,3 +1,5 @@ +from typing import Mapping # noqa + from django.apps import apps from django.core.cache import cache @@ -507,7 +509,7 @@ class Collection: cache.set(self.get_cache_key(), ids) -_models_to_collection_string = {} +_models_to_collection_string = {} # type: Mapping[str, object] def get_model_from_collection_string(collection_string): diff --git a/openslides/utils/dispatch.py b/openslides/utils/dispatch.py index 09bb228ae..14c336fb2 100644 --- a/openslides/utils/dispatch.py +++ b/openslides/utils/dispatch.py @@ -71,7 +71,7 @@ class SignalConnectMetaClass(type): return new_class -@classmethod +@classmethod # type: ignore def get_all(cls, request=None): """ Collects all objects of the class created by the SignalConnectMetaClass diff --git a/openslides/utils/models.py b/openslides/utils/models.py index d145faa6b..e9143fc1d 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -1,5 +1,6 @@ from django.db import models +from .access_permissions import BaseAccessPermissions # noqa from .utils import convert_camel_case_to_pseudo_snake_case @@ -23,7 +24,7 @@ class RESTModelMixin: Mixin for Django models which are used in our REST API. """ - access_permissions = None + access_permissions = None # type: BaseAccessPermissions def get_root_rest_element(self): """ diff --git a/openslides/utils/projector.py b/openslides/utils/projector.py index db6f20216..b24d3ff7e 100644 --- a/openslides/utils/projector.py +++ b/openslides/utils/projector.py @@ -1,3 +1,5 @@ +from typing import Optional # noqa + from django.dispatch import Signal from .collection import CollectionElement @@ -14,7 +16,7 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass): magic. """ signal = Signal() - name = None + name = None # type: Optional[str] def __init__(self, **kwargs): """ diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index f7c94e455..ec02e35b2 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from typing import Optional # noqa from django.http import Http404 from rest_framework import status # noqa @@ -30,6 +31,7 @@ from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa from rest_framework.viewsets import ViewSet as _ViewSet # noqa +from .access_permissions import BaseAccessPermissions # noqa from .auth import user_to_collection_user from .collection import Collection, CollectionElement @@ -102,7 +104,7 @@ class PermissionMixin: Also connects container to handle access permissions for model and viewset. """ - access_permissions = None + access_permissions = None # type: Optional[BaseAccessPermissions] def get_permissions(self): """ diff --git a/openslides/utils/views.py b/openslides/utils/views.py index bb0309dd0..b68c4ff51 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -1,3 +1,5 @@ +from typing import List # noqa + from django.views import generic as django_views from django.views.decorators.csrf import ensure_csrf_cookie from rest_framework.response import Response @@ -22,7 +24,7 @@ class APIView(_APIView): The Django Rest framework APIView with improvements for OpenSlides. """ - http_method_names = [] + http_method_names = [] # type: List[str] """ The allowed actions have to be explicitly defined. diff --git a/requirements.txt b/requirements.txt index a88110c3b..e458bce41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ coverage flake8 isort==4.2.5 +mypy diff --git a/setup.cfg b/setup.cfg index 8ad6eaacc..22617486d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,3 +13,10 @@ max_line_length = 150 [isort] include_trailing_comma = true multi_line_output = 3 + +[mypy] +ignore_missing_imports = true +strict_optional = true + +[mypy-openslides.utils.auth] +disallow_any = unannotated diff --git a/tests/unit/config/test_api.py b/tests/unit/config/test_api.py index d059401bc..53b8c063e 100644 --- a/tests/unit/config/test_api.py +++ b/tests/unit/config/test_api.py @@ -1,8 +1,7 @@ from unittest import TestCase from unittest.mock import patch -from openslides.core.config import ConfigVariable, config -from openslides.core.exceptions import ConfigNotFound +from openslides.core.config import ConfigVariable class TestConfigVariable(TestCase): @@ -23,11 +22,3 @@ class TestConfigVariable(TestCase): 'test_default_value', "The value of config_variable.data['default_value'] should be the same " "as set as second argument of ConfigVariable()") - - -class TestConfigHandler(TestCase): - def test_get_not_found(self): - self.assertRaises( - ConfigNotFound, - config.__getitem__, - 'key_leehah4Sho4ee7aCohbn')