diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fb58f19882f3d4789444f4595d3eb0a0194a7b34..22d961dbea41e95f17ac2935fa7f684376865b1d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,9 +30,9 @@ Mypy: script: - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-mypy.sh - "cp local_settings.example.py local_settings.py" - - ". ./prepare-and-run-mypy.sh" + - ". ./prepare-and-run-mypy.sh python3.6 mypy==0.521 typed-ast==1.0.4" tags: - - python3.5 + - python3.6 except: - tags diff --git a/course/constants.py b/course/constants.py index 7c50b9f1469457c7144366370680fdf7e1eba3b1..f418746b796327abd60311e4fbd1040f3fcd62bf 100644 --- a/course/constants.py +++ b/course/constants.py @@ -24,6 +24,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +if False: + import typing # noqa from django.utils.translation import pgettext_lazy, ugettext # Allow 10x extra credit at the very most. @@ -286,7 +288,7 @@ FLOW_SESSION_EXPIRATION_MODE_CHOICES = ( def is_expiration_mode_allowed(expmode, permissions): - # type: (str, frozenset[str]) -> bool + # type: (str, typing.FrozenSet[str]) -> bool if expmode == flow_session_expiration_mode.roll_over: if (flow_permission.set_roll_over_expiration_mode in permissions): diff --git a/course/content.py b/course/content.py index 407d1adadbfbb2730847d9917044a40141b3049e..294d871d3b1586b703fba6410a4a59404ce2c2eb 100644 --- a/course/content.py +++ b/course/content.py @@ -62,7 +62,7 @@ else: if False: # for mypy from typing import ( # noqa - Any, List, Tuple, Optional, Callable, Text, Dict) + Any, List, Tuple, Optional, Callable, Text, Dict, FrozenSet) from course.models import Course, Participation # noqa import dulwich # noqa from course.validation import ValidationContext # noqa @@ -300,6 +300,7 @@ def get_repo_blob_data_cached(repo, full_name, commit_sha): except ImproperlyConfigured: cache_key = None + result = None # type: Optional[bytes] if cache_key is None: result = get_repo_blob(repo, full_name, commit_sha, allow_tree=False).data @@ -312,17 +313,18 @@ def get_repo_blob_data_cached(repo, full_name, commit_sha): def_cache = cache.caches["default"] - result = None # Memcache is apparently limited to 250 characters. if len(cache_key) < 240: - result = def_cache.get(cache_key) - if result is not None: - (result,) = result - assert isinstance(result, six.binary_type), cache_key - return result + cached_result = def_cache.get(cache_key) + + if cached_result is not None: + (result,) = cached_result + assert isinstance(result, six.binary_type), cache_key + return result result = get_repo_blob(repo, full_name, commit_sha, allow_tree=False).data + assert result is not None if len(result) <= getattr(settings, "RELATE_CACHE_MAX_BYTES", 0): def_cache.add(cache_key, (result,), None) @@ -561,18 +563,20 @@ def get_raw_yaml_from_repo(repo, full_name, commit_sha): import django.core.cache as cache def_cache = cache.caches["default"] - result = None + + result = None # type: Optional[Any] # Memcache is apparently limited to 250 characters. if len(cache_key) < 240: result = def_cache.get(cache_key) if result is not None: return result - result = load_yaml( - expand_yaml_macros( + yaml_str = expand_yaml_macros( repo, commit_sha, get_repo_blob(repo, full_name, commit_sha, - allow_tree=False).data)) + allow_tree=False).data) + + result = load_yaml(yaml_str) # type: ignore def_cache.add(cache_key, result, None) @@ -619,7 +623,8 @@ def get_yaml_from_repo(repo, full_name, commit_sha, cached=True): expanded = expand_yaml_macros( repo, commit_sha, yaml_bytestream) - result = dict_to_struct(load_yaml(expanded)) + yaml_data = load_yaml(expanded) # type:ignore + result = dict_to_struct(yaml_data) if cached: def_cache.add(cache_key, result, None) @@ -1215,7 +1220,7 @@ def compute_chunk_weight_and_shown( chunk, # type: ChunkDesc roles, # type: List[Text] now_datetime, # type: datetime.datetime - facilities, # type: frozenset[Text] + facilities, # type: FrozenSet[Text] ): # type: (...) -> Tuple[float, bool] if not hasattr(chunk, "rules"): @@ -1274,7 +1279,7 @@ def get_processed_page_chunks( page_desc, # type: StaticPageDesc roles, # type: List[Text] now_datetime, # type: datetime.datetime - facilities, # type: frozenset[Text] + facilities, # type: FrozenSet[Text] ): # type: (...) -> List[ChunkDesc] for chunk in page_desc.chunks: diff --git a/course/enrollment.py b/course/enrollment.py index abad304680fc9c9997af018d9f19ac667c37d82c..cfbc7802c7f891e07d1519509337f6efb173d12a 100644 --- a/course/enrollment.py +++ b/course/enrollment.py @@ -71,7 +71,7 @@ from pytools.lex import RE as REBase # noqa # {{{ for mypy if False: - from typing import Any, Tuple, Text, Optional, List # noqa + from typing import Any, Tuple, Text, Optional, List, FrozenSet # noqa from course.utils import CoursePageContext # noqa # }}} @@ -137,7 +137,7 @@ def get_participation_permissions( course, # type: Course participation, # type: Optional[Participation] ): - # type: (...) -> frozenset[Tuple[Text, Optional[Text]]] + # type: (...) -> FrozenSet[Tuple[Text, Optional[Text]]] if participation is not None: return participation.permissions() @@ -233,6 +233,8 @@ def enroll_view(request, course_identifier): course, user, participation_status.requested, roles, request) + assert participation is not None + with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): from django.template.loader import render_to_string message = render_to_string("course/enrollment-request-email.txt", { diff --git a/course/exam.py b/course/exam.py index 6cd666c7aa36962bc7b22c3f339f2ab3eae197a8..ee6d53919280ceac6e930c3e70c26f6d8f54f53c 100644 --- a/course/exam.py +++ b/course/exam.py @@ -615,7 +615,7 @@ def is_from_exams_only_facility(request): def get_login_exam_ticket(request): - # type: (http.HttpRequest) -> ExamTicket + # type: (http.HttpRequest) -> Optional[ExamTicket] exam_ticket_pk = request.session.get("relate_exam_ticket_pk_used_for_login") if exam_ticket_pk is None: diff --git a/course/flow.py b/course/flow.py index 9122f44691b192006e584bcacea97777645f3e50..23e178e6d08aa10bb38004b78a3ff32bc03125ce 100644 --- a/course/flow.py +++ b/course/flow.py @@ -88,7 +88,7 @@ from relate.utils import retry_transaction_decorator # {{{ mypy if False: - from typing import Any, Optional, Iterable, Tuple, Text, List # noqa + from typing import Any, Optional, Iterable, Sequence, Tuple, Text, List, FrozenSet # noqa import datetime # noqa from course.models import Course # noqa from course.utils import ( # noqa @@ -494,6 +494,7 @@ def get_prev_answer_visits_qset(page_data): def get_first_from_qset(qset): + # type: (Sequence) -> Optional[Any] for item in qset[:1]: return item @@ -1347,7 +1348,7 @@ def recalculate_session_grade(repo, course, session): def lock_down_if_needed( request, # type: http.HttpRequest - permissions, # type: frozenset[Text] + permissions, # type: FrozenSet[Text] flow_session, # type: FlowSession ): # type: (...) -> None @@ -1607,7 +1608,7 @@ def get_and_check_flow_session(pctx, flow_session_id): def will_receive_feedback(permissions): - # type: (frozenset[Text]) -> bool + # type: (FrozenSet[Text]) -> bool return ( flow_permission.see_correctness in permissions @@ -1615,14 +1616,14 @@ def will_receive_feedback(permissions): def may_send_email_about_flow_page(permissions): - # type: (frozenset[Text]) -> bool + # type: (FrozenSet[Text]) -> bool return flow_permission.send_email_about_flow_page in permissions def get_page_behavior( page, # type: PageBase - permissions, # type: frozenset[Text] + permissions, # type: FrozenSet[Text] session_in_progress, # type: bool answer_was_graded, # type: bool generates_grade, # type: bool @@ -1679,7 +1680,7 @@ def get_page_behavior( def add_buttons_to_form(form, fpctx, flow_session, permissions): - # type: (StyledForm, FlowPageContext, FlowSession, frozenset[Text]) -> StyledForm + # type: (StyledForm, FlowPageContext, FlowSession, FrozenSet[Text]) -> StyledForm from crispy_forms.layout import Submit show_save_button = getattr(form, "show_save_button", True) @@ -2137,7 +2138,7 @@ def post_flow_page( flow_session, # type: FlowSession fpctx, # type: FlowPageContext request, # type: http.HttpRequest - permissions, # type: frozenset[Text] + permissions, # type: FrozenSet[Text] generates_grade, # type: bool ): # type: (...) -> Tuple[PageBehavior, List[FlowPageVisit], forms.Form, Optional[AnswerFeedback], Any, bool] # noqa @@ -2832,6 +2833,7 @@ def regrade_flows_view(pctx): class UnsubmitFlowPageForm(forms.Form): def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None self.helper = FormHelper() super(UnsubmitFlowPageForm, self).__init__(*args, **kwargs) diff --git a/course/grading.py b/course/grading.py index 910edc9a7a122892d491b0612817f020c9357369..5691309d0f4123abfffef7988bf730b1c1f82cdd 100644 --- a/course/grading.py +++ b/course/grading.py @@ -58,7 +58,7 @@ from course.constants import ( # {{{ for mypy if False: - from typing import Text, Any, Optional, Dict # noqa + from typing import Text, Any, Optional, Dict, List # noqa from course.models import ( # noqa GradingOpportunity) from course.utils import ( # noqa @@ -320,6 +320,8 @@ def grade_flow_page(pctx, flow_session_id, page_ordinal): else: grading_form = None + grading_form_html = None # type: Optional[Text] + if grading_form is not None: from crispy_forms.layout import Submit grading_form.helper.form_class += " relate-grading-form" @@ -332,9 +334,6 @@ def grade_flow_page(pctx, flow_session_id, page_ordinal): grading_form_html = fpctx.page.grading_form_to_html( pctx.request, fpctx.page_context, grading_form, grade_data) - else: - grading_form_html = None - # }}} # {{{ compute points_awarded diff --git a/course/models.py b/course/models.py index 6a0f84c34ef3bdbb81035fb20721a92b680b6962..f1f2fc873a3cf1a77da530174778b22abb5b2b5f 100644 --- a/course/models.py +++ b/course/models.py @@ -61,7 +61,7 @@ from course.page.base import AnswerFeedback # {{{ mypy if False: - from typing import List, Dict, Any, Optional, Text, Iterable # noqa # noqa + from typing import List, Dict, Any, Optional, Text, Iterable, Tuple, FrozenSet # noqa from course.content import FlowDesc # noqa # }}} @@ -468,11 +468,13 @@ class Participation(models.Model): # {{{ permissions handling + _permissions_cache = None # type: FrozenSet[Tuple[Text, Optional[Text]]] + def permissions(self): - try: + # type: () -> FrozenSet[Tuple[Text, Optional[Text]]] + + if self._permissions_cache is not None: return self._permissions_cache - except AttributeError: - pass perm = ( list( @@ -486,14 +488,15 @@ class Participation(models.Model): participation=self) .values_list("permission", "argument"))) - perm = frozenset( + fset_perm = frozenset( (permission, argument) if argument else (permission, None) for permission, argument in perm) - self._permissions_cache = perm - return perm + self._permissions_cache = fset_perm + return fset_perm def has_permission(self, perm, argument=None): + # type: (Text, Optional[Text]) -> bool return (perm, argument) in self.permissions() # }}} @@ -793,7 +796,7 @@ class FlowSession(models.Model): __str__ = __unicode__ def append_comment(self, s): - # type: (Text) -> None + # type: (Optional[Text]) -> None if s is None: return diff --git a/course/page/base.py b/course/page/base.py index 4bc5c7eb450d9bac9f36cbb6d009a38beff04471..bfb3c81c4f849c737926a79c013cd7ae60468937 100644 --- a/course/page/base.py +++ b/course/page/base.py @@ -44,7 +44,7 @@ from django.conf import settings # {{{ mypy if False: - from typing import Text, Optional, Any, Tuple, Dict, Callable # noqa + from typing import Text, Optional, Any, Tuple, Dict, Callable, FrozenSet # noqa from django import http # noqa from course.models import ( # noqa Course, @@ -216,7 +216,7 @@ class AnswerFeedback(object): @staticmethod def from_json(json, bulk_json): - # type: (Any, Any) -> AnswerFeedback + # type: (Any, Any) -> Optional[AnswerFeedback] if json is None: return json @@ -371,7 +371,7 @@ class PageBase(object): ) def get_modified_permissions_for_page(self, permissions): - # type: (frozenset[Text]) -> frozenset[Text] + # type: (FrozenSet[Text]) -> FrozenSet[Text] rw_permissions = set(permissions) if hasattr(self.page_desc, "access_rules"): @@ -387,9 +387,11 @@ class PageBase(object): return frozenset(rw_permissions) def make_page_data(self): + # type: () -> Dict return {} def initialize_page_data(self, page_context): + # type: (PageContext) -> Dict """Return (possibly randomly generated) data that is used to generate the content on this page. This is passed to methods below as the *page_data* argument. One possible use for this argument would be a random permutation diff --git a/course/sandbox.py b/course/sandbox.py index 3ffd16b6bf1f58e6180db0ef304ffb686ccde9df..23258395ed0638fdadba1757bf4824a8019920d2 100644 --- a/course/sandbox.py +++ b/course/sandbox.py @@ -240,7 +240,9 @@ def view_page_sandbox(pctx): from course.content import expand_yaml_macros new_page_source = expand_yaml_macros( pctx.repo, pctx.course_commit_sha, new_page_source) - page_desc = dict_to_struct(yaml.load(new_page_source)) + + yaml_data = yaml.load(new_page_source) # type: ignore + page_desc = dict_to_struct(yaml_data) if not isinstance(page_desc, Struct): raise ValidationError("Provided page source code is not " @@ -295,7 +297,8 @@ def view_page_sandbox(pctx): have_valid_page = page_source is not None if have_valid_page: - page_desc = cast(FlowPageDesc, dict_to_struct(yaml.load(page_source))) + yaml_data = yaml.load(page_source) # type: ignore + page_desc = cast(FlowPageDesc, dict_to_struct(yaml_data)) from course.content import instantiate_flow_page try: @@ -313,6 +316,8 @@ def view_page_sandbox(pctx): have_valid_page = False if have_valid_page: + page_desc = cast(FlowPageDesc, page_desc) + # Try to recover page_data, answer_data page_data = get_sandbox_data_for_page( pctx, page_desc, PAGE_DATA_SESSION_KEY) diff --git a/course/utils.py b/course/utils.py index 2ff1f3f567f611043a31478cbbfad6430d58c8db..944fbcabbf8cc67e9e640836c522550b2d99f8e4 100644 --- a/course/utils.py +++ b/course/utils.py @@ -24,11 +24,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from typing import cast, Text + import six import datetime # noqa -from typing import cast - from django.shortcuts import ( # noqa render, get_object_or_404) from django import http @@ -58,7 +58,7 @@ from course.page.base import ( # noqa # {{{ mypy if False: - from typing import Tuple, List, Text, Iterable, Any, Optional, Union, Dict # noqa + from typing import Tuple, List, Iterable, Any, Optional, Union, Dict, FrozenSet # noqa from relate.utils import Repo_ish # noqa from course.models import ( # noqa Course, @@ -105,7 +105,7 @@ class FlowSessionStartRule(FlowSessionRuleBase): class FlowSessionAccessRule(FlowSessionRuleBase): def __init__( self, - permissions, # type: frozenset[Text] + permissions, # type: FrozenSet[Text] message=None, # type: Optional[Text] ): # type: (...) -> None @@ -280,7 +280,7 @@ def get_session_start_rule( flow_id, # type: Text flow_desc, # type: FlowDesc now_datetime, # type: datetime.datetime - facilities=None, # type: Optional[frozenset[Text]] + facilities=None, # type: Optional[FrozenSet[Text]] for_rollover=False, # type: bool login_exam_ticket=None, # type: Optional[ExamTicket] ): @@ -373,7 +373,7 @@ def get_session_access_rule( session, # type: FlowSession flow_desc, # type: FlowDesc now_datetime, # type: datetime.datetime - facilities=None, # type: Optional[frozenset[Text]] + facilities=None, # type: Optional[FrozenSet[Text]] login_exam_ticket=None, # type: Optional[ExamTicket] ): # type: (...) -> FlowSessionAccessRule @@ -518,7 +518,7 @@ def get_session_grading_rule( return FlowSessionGradingRule( grade_identifier=grade_identifier, - grade_aggregation_strategy=grade_aggregation_strategy, + grade_aggregation_strategy=cast(Text, grade_aggregation_strategy), due=due, generates_grade=generates_grade, description=getattr(rule, "description", None), @@ -552,7 +552,7 @@ class CoursePageContext(object): self.request = request self.course_identifier = course_identifier - self._permissions_cache = None # type: Optional[frozenset[Tuple[Text, Optional[Text]]]] # noqa + self._permissions_cache = None # type: Optional[FrozenSet[Tuple[Text, Optional[Text]]]] # noqa self._role_identifiers_cache = None # type: Optional[List[Text]] from course.models import Course # noqa @@ -612,7 +612,7 @@ class CoursePageContext(object): return self._role_identifiers_cache def permissions(self): - # type: () -> frozenset[Tuple[Text, Optional[Text]]] + # type: () -> FrozenSet[Tuple[Text, Optional[Text]]] if self.participation is None: if self._permissions_cache is not None: return self._permissions_cache @@ -966,7 +966,7 @@ def get_codemirror_widget( # {{{ facility processing def get_facilities_config(request=None): - # type: (Optional[http.HttpRequest]) -> Dict[Text, Dict[Text, Any]] + # type: (Optional[http.HttpRequest]) -> Optional[Dict[Text, Dict[Text, Any]]] from django.conf import settings # This is called during offline validation, where Django isn't really set up. diff --git a/course/validation.py b/course/validation.py index 27ed619cc17b25ab4918ca7e6fc4965d138a7def..76c1739a77473ff8410476b405d432425e7a2090 100644 --- a/course/validation.py +++ b/course/validation.py @@ -1219,7 +1219,8 @@ def check_attributes_yml(vctx, repo, path, tree, access_kinds): from relate.utils import dict_to_struct from yaml import load as load_yaml - att_yml = dict_to_struct(load_yaml(true_repo[attr_blob_sha].data)) + yaml_data = load_yaml(true_repo[attr_blob_sha].data) # type: ignore + att_yml = dict_to_struct(yaml_data) if path: loc = path + "/" + ATTRIBUTES_FILENAME diff --git a/course/views.py b/course/views.py index 5128d3e2d8da2c05c3cd8f174882576f1e629fcf..40c5727e997db47b7cf1d6ceef21fffc8dd47a2b 100644 --- a/course/views.py +++ b/course/views.py @@ -24,6 +24,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from typing import cast, List, Text + import datetime from django.shortcuts import ( # noqa @@ -83,7 +85,7 @@ from course.utils import ( # noqa # {{{ for mypy if False: - from typing import Tuple, List, Text, Optional, Any, Iterable, Dict # noqa + from typing import Tuple, Any, Iterable, Dict, Optional # noqa from course.content import ( # noqa FlowDesc, @@ -1130,9 +1132,10 @@ def grant_exception_stage_3(pctx, participation_id, flow_id, session_id): flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id, pctx.course_commit_sha) - tags = None + + tags = [] # type: List[Text] if hasattr(flow_desc, "rules"): - tags = getattr(flow_desc.rules, "tags", None) + tags = cast(List[Text], getattr(flow_desc.rules, "tags", [])) # {{{ put together access rule diff --git a/run-mypy.sh b/run-mypy.sh index 6896ff9a5cd1baad80ca2076244343e9ad3fa9f5..e867afdaf9cd9e8cf8a1286a8d941650408a772f 100755 --- a/run-mypy.sh +++ b/run-mypy.sh @@ -1,7 +1,6 @@ #! /bin/bash mypy \ - --fast-parser \ --strict-optional \ --ignore-missing-imports \ --follow-imports=skip \