Skip to content

Commit 8c4631d

Browse files
committed
Use django-stubs, pass mypy with it
1 parent d5e5bef commit 8c4631d

25 files changed

+297
-178
lines changed

.ci/run-safety.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# 67599: pip issue, utter nonsense
1717
# 70612: Jinja2 SSTI, as of https://github.com/inducer/relate/pull/1053
1818
# there is no longer a direct Jinja dependency, and no known path to SSTI.
19-
poetry run safety check \
19+
safety check \
2020
-i 38678 \
2121
-i 39253 \
2222
-i 39535 \

.github/workflows/ci.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ jobs:
4242
run: |
4343
poetry run ruff check
4444
- uses: crate-ci/typos@master
45+
- name: "Set up local settings"
46+
run: cp local_settings_example.py local_settings.py
4547
- name: "Mypy"
46-
run: poetry run mypy relate course
48+
run: poetry run ./.ci/run-mypy.sh
4749
- name: "Safety"
48-
run: bash ./.ci/run-safety.sh
50+
run: poetry run ./.ci/run-safety.sh
4951
- name: "Sphinx"
5052
run: |
51-
cp local_settings_example.py local_settings.py
5253
(cd doc; poetry run make html SPHINXOPTS="-W --keep-going -n")
5354
5455
frontend:

.gitlab-ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ ruff:
5656
5757
mypy:
5858
<<: *quality
59-
script: poetry run mypy relate course
59+
variables:
60+
RELATE_LOCAL_TEST_SETTINGS: './local_settings_example.py'
61+
script: poetry run ./.ci/run-mypy.sh
6062

6163
safety:
6264
<<: *quality

accounts/admin.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ def queryset(self, request, queryset):
6565
class UserAdmin(UserAdminBase):
6666
save_on_top = True
6767

68-
list_display = (*tuple(UserAdminBase.list_display),
68+
# Fixing this type-ignore would require type.__getitem__ on Django types,
69+
# which is only available via monkeypatching, ugh.
70+
list_display = (
71+
*UserAdminBase.list_display, # type: ignore[misc]
6972
"name_verified", "status", "institutional_id", "institutional_id_verified")
7073
list_editable = ("first_name", "last_name",
7174
"name_verified",
@@ -77,8 +80,12 @@ class UserAdmin(UserAdminBase):
7780
"status", CourseListFilter) # type: ignore
7881
search_fields = (*tuple(UserAdminBase.search_fields), "institutional_id")
7982

80-
fieldsets = UserAdminBase.fieldsets[:1] + (
81-
(UserAdminBase.fieldsets[1][0], {"fields": (
83+
_fsets = UserAdminBase.fieldsets
84+
assert _fsets is not None
85+
86+
fieldsets = (
87+
*_fsets[:1],
88+
(_fsets[1][0], {"fields": (
8289
"status",
8390
"first_name",
8491
"last_name",
@@ -88,7 +95,8 @@ class UserAdmin(UserAdminBase):
8895
"institutional_id_verified",
8996
"editor_mode",)
9097
}),
91-
) + UserAdminBase.fieldsets[2:]
98+
*_fsets[2:],
99+
)
92100
ordering = ["-date_joined"]
93101

94102
def get_fieldsets(self, request, obj=None):

accounts/models.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from django.contrib.auth.validators import ASCIIUsernameValidator
3333
from django.db import models
3434
from django.utils import timezone
35-
from django.utils.translation import gettext_lazy as _, pgettext_lazy
35+
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
3636

3737
from course.constants import USER_STATUS_CHOICES
3838

@@ -130,20 +130,22 @@ class Meta:
130130
verbose_name = _("user")
131131
verbose_name_plural = _("users")
132132

133-
def get_full_name(self, allow_blank=True, force_verbose_blank=False):
133+
def get_full_name(
134+
self, allow_blank=True, force_verbose_blank=False
135+
) -> str | None:
134136
if (not allow_blank
135137
and (not self.first_name or not self.last_name)):
136138
return None
137139

138-
def verbose_blank(s):
140+
def verbose_blank(s: str) -> str:
139141
if force_verbose_blank:
140142
if not s:
141-
return _("(blank)")
143+
return gettext("(blank)")
142144
else:
143145
return s
144146
return s
145147

146-
def default_fullname(first_name, last_name):
148+
def default_fullname(first_name: str, last_name: str) -> str:
147149
"""
148150
Returns the first_name plus the last_name, with a space in
149151
between.
@@ -164,7 +166,7 @@ def default_fullname(first_name, last_name):
164166

165167
return full_name.strip()
166168

167-
def get_masked_profile(self):
169+
def get_masked_profile(self) -> str:
168170
"""
169171
Returns the masked user profile.
170172
"""
@@ -188,11 +190,11 @@ def default_mask_method(user):
188190
"an empty string.")
189191
return result
190192

191-
def get_short_name(self):
193+
def get_short_name(self) -> str:
192194
"""Returns the short name for the user."""
193195
return self.first_name
194196

195-
def get_email_appellation(self):
197+
def get_email_appellation(self) -> str:
196198
"""Return the appellation of the receiver in email."""
197199

198200
from accounts.utils import relate_user_method_settings
@@ -210,9 +212,9 @@ def get_email_appellation(self):
210212

211213
return appellation
212214

213-
return _("user")
215+
return gettext("user")
214216

215-
def clean(self):
217+
def clean(self) -> None:
216218
super().clean()
217219

218220
# email can be None in Django admin when create new user

course/auth.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
Participation,
7575
ParticipationRole,
7676
)
77-
from course.utils import course_view, render_course_page
77+
from course.utils import CoursePageContext, course_view, render_course_page
7878
from relate.utils import (
7979
HTML5DateTimeInput,
8080
StyledForm,
@@ -1191,21 +1191,30 @@ def find_matching_token(
11911191
token_hash_str: str | None = None,
11921192
now_datetime: datetime.datetime | None = None
11931193
) -> AuthenticationToken | None:
1194+
if token_id is None:
1195+
return None
1196+
11941197
try:
11951198
token = AuthenticationToken.objects.get(
11961199
id=token_id,
11971200
participation__course__identifier=course_identifier)
11981201
except AuthenticationToken.DoesNotExist:
11991202
return None
12001203

1204+
if token.token_hash is None:
1205+
return None
1206+
12011207
from django.contrib.auth.hashers import check_password
12021208
if not check_password(token_hash_str, token.token_hash):
12031209
return None
12041210

12051211
if token.revocation_time is not None:
12061212
return None
1207-
if token.valid_until is not None and now_datetime > token.valid_until:
1208-
return None
1213+
if token.valid_until is not None:
1214+
if now_datetime is None:
1215+
return None
1216+
if now_datetime > token.valid_until:
1217+
return None
12091218

12101219
return token
12111220

@@ -1400,7 +1409,7 @@ def __init__(
14001409
pperm.impersonate_role, prole.identifier)}
14011410
)
14021411

1403-
self.fields["restrict_to_participation_role"].queryset = (
1412+
self.fields["restrict_to_participation_role"].queryset = ( # type:ignore[attr-defined]
14041413
ParticipationRole.objects.filter(
14051414
id__in=list(allowable_role_ids)
14061415
))
@@ -1409,15 +1418,15 @@ def __init__(
14091418

14101419

14111420
@course_view
1412-
def manage_authentication_tokens(pctx: http.HttpRequest) -> http.HttpResponse:
1413-
1421+
def manage_authentication_tokens(pctx: CoursePageContext) -> http.HttpResponse:
14141422
request = pctx.request
14151423

14161424
if not request.user.is_authenticated:
14171425
raise PermissionDenied()
14181426

14191427
if not pctx.has_permission(pperm.view_analytics):
14201428
raise PermissionDenied()
1429+
assert pctx.participation is not None
14211430

14221431
from course.views import get_now_or_fake_time
14231432
now_datetime = get_now_or_fake_time(request)
@@ -1446,7 +1455,7 @@ def manage_authentication_tokens(pctx: http.HttpRequest) -> http.HttpResponse:
14461455

14471456
from django.contrib.auth.hashers import make_password
14481457
auth_token = AuthenticationToken(
1449-
user=pctx.request.user,
1458+
user=request.user,
14501459
participation=pctx.participation,
14511460
restrict_to_participation_role=form.cleaned_data[
14521461
"restrict_to_participation_role"],

course/content.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
# {{{ mypy
5353

54-
from collections.abc import Callable
54+
from collections.abc import Callable, Collection
5555
from typing import (
5656
TYPE_CHECKING,
5757
Any,
@@ -1736,7 +1736,7 @@ def compute_chunk_weight_and_shown(
17361736
chunk: ChunkDesc,
17371737
roles: list[str],
17381738
now_datetime: datetime.datetime,
1739-
facilities: frozenset[str],
1739+
facilities: Collection[str],
17401740
) -> tuple[float, bool]:
17411741
if not hasattr(chunk, "rules"):
17421742
return 0, True
@@ -1794,7 +1794,7 @@ def get_processed_page_chunks(
17941794
page_desc: StaticPageDesc,
17951795
roles: list[str],
17961796
now_datetime: datetime.datetime,
1797-
facilities: frozenset[str],
1797+
facilities: Collection[str],
17981798
) -> list[ChunkDesc]:
17991799
for chunk in page_desc.chunks:
18001800
chunk.weight, chunk.shown = \
@@ -2006,15 +2006,12 @@ def is_commit_sha_valid(repo: Repo_ish, commit_sha: str) -> bool:
20062006
preview_sha = participation.preview_git_commit_sha
20072007

20082008
if repo is not None:
2009-
commit_sha_valid = is_commit_sha_valid(repo, preview_sha)
2009+
preview_sha_valid = is_commit_sha_valid(repo, preview_sha)
20102010
else:
20112011
with get_course_repo(course) as repo:
2012-
commit_sha_valid = is_commit_sha_valid(repo, preview_sha)
2012+
preview_sha_valid = is_commit_sha_valid(repo, preview_sha)
20132013

2014-
if not commit_sha_valid:
2015-
preview_sha = None
2016-
2017-
if preview_sha is not None:
2014+
if preview_sha_valid:
20182015
sha = preview_sha
20192016

20202017
return sha.encode()

0 commit comments

Comments
 (0)