Skip to content

Commit 3dc28ab

Browse files
committed
build: Fix type annotations for new mypy version
Includes some new Request type annotations in openedx.core.types.http, plus a new meta-utility @type_annotation_only to ensure that we don't accidentally start instantiating those new classes.
1 parent 9262c9a commit 3dc28ab

File tree

8 files changed

+107
-18
lines changed

8 files changed

+107
-18
lines changed

openedx/core/djangoapps/content_libraries/views.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
from openedx.core.lib.api.view_utils import view_auth_classes
123123
from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected
124124
from openedx.core.djangoapps.xblock import api as xblock_api
125+
from openedx.core.types.http import RestRequest
125126

126127
from .models import ContentLibrary, LtiGradedResource, LtiProfile
127128

@@ -667,7 +668,7 @@ class LibraryBlockCollectionsView(APIView):
667668
View to set collections for a component.
668669
"""
669670
@convert_exceptions
670-
def patch(self, request, usage_key_str) -> Response:
671+
def patch(self, request: RestRequest, usage_key_str) -> Response:
671672
"""
672673
Sets Collections for a Component.
673674
@@ -688,7 +689,7 @@ def patch(self, request, usage_key_str) -> Response:
688689
library_key=key.lib_key,
689690
component=component,
690691
collection_keys=collection_keys,
691-
created_by=self.request.user.id,
692+
created_by=request.user.id,
692693
content_library=content_library,
693694
)
694695

openedx/core/djangoapps/content_libraries/views_collections.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ContentLibraryCollectionComponentsUpdateSerializer,
2626
ContentLibraryCollectionUpdateSerializer,
2727
)
28+
from openedx.core.types.http import RestRequest
2829

2930

3031
class LibraryCollectionsView(ModelViewSet):
@@ -89,23 +90,23 @@ def get_object(self) -> Collection:
8990
return collection
9091

9192
@convert_exceptions
92-
def retrieve(self, request, *args, **kwargs) -> Response:
93+
def retrieve(self, request: RestRequest, *args, **kwargs) -> Response:
9394
"""
9495
Retrieve the Content Library Collection
9596
"""
9697
# View declared so we can wrap it in @convert_exceptions
9798
return super().retrieve(request, *args, **kwargs)
9899

99100
@convert_exceptions
100-
def list(self, request, *args, **kwargs) -> Response:
101+
def list(self, request: RestRequest, *args, **kwargs) -> Response:
101102
"""
102103
List Collections that belong to Content Library
103104
"""
104105
# View declared so we can wrap it in @convert_exceptions
105106
return super().list(request, *args, **kwargs)
106107

107108
@convert_exceptions
108-
def create(self, request, *args, **kwargs) -> Response:
109+
def create(self, request: RestRequest, *args, **kwargs) -> Response:
109110
"""
110111
Create a Collection that belongs to a Content Library
111112
"""
@@ -139,7 +140,7 @@ def create(self, request, *args, **kwargs) -> Response:
139140
return Response(serializer.data)
140141

141142
@convert_exceptions
142-
def partial_update(self, request, *args, **kwargs) -> Response:
143+
def partial_update(self, request: RestRequest, *args, **kwargs) -> Response:
143144
"""
144145
Update a Collection that belongs to a Content Library
145146
"""
@@ -161,7 +162,7 @@ def partial_update(self, request, *args, **kwargs) -> Response:
161162
return Response(serializer.data)
162163

163164
@convert_exceptions
164-
def destroy(self, request, *args, **kwargs) -> Response:
165+
def destroy(self, request: RestRequest, *args, **kwargs) -> Response:
165166
"""
166167
Soft-deletes a Collection that belongs to a Content Library
167168
"""
@@ -176,7 +177,7 @@ def destroy(self, request, *args, **kwargs) -> Response:
176177

177178
@convert_exceptions
178179
@action(detail=True, methods=['post'], url_path='restore', url_name='collection-restore')
179-
def restore(self, request, *args, **kwargs) -> Response:
180+
def restore(self, request: RestRequest, *args, **kwargs) -> Response:
180181
"""
181182
Restores a soft-deleted Collection that belongs to a Content Library
182183
"""
@@ -191,7 +192,7 @@ def restore(self, request, *args, **kwargs) -> Response:
191192

192193
@convert_exceptions
193194
@action(detail=True, methods=['delete', 'patch'], url_path='components', url_name='components-update')
194-
def update_components(self, request, *args, **kwargs) -> Response:
195+
def update_components(self, request: RestRequest, *args, **kwargs) -> Response:
195196
"""
196197
Adds (PATCH) or removes (DELETE) Components to/from a Collection.
197198
@@ -209,7 +210,7 @@ def update_components(self, request, *args, **kwargs) -> Response:
209210
content_library=content_library,
210211
collection_key=collection_key,
211212
usage_keys=usage_keys,
212-
created_by=self.request.user.id,
213+
created_by=request.user.id,
213214
remove=(request.method == "DELETE"),
214215
)
215216

openedx/core/djangoapps/content_staging/api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def save_xblock_to_user_clipboard(block: XBlock, user_id: int, version_num: int
6666
olx=block_data.olx_str,
6767
display_name=block_metadata_utils.display_name_with_default(block),
6868
suggested_url_name=usage_key.block_id,
69-
tags=block_data.tags,
69+
tags=block_data.tags or {},
7070
version_num=(version_num or 0),
7171
)
7272
(clipboard, _created) = _UserClipboard.objects.update_or_create(user_id=user_id, defaults={
@@ -209,7 +209,7 @@ def _user_clipboard_model_to_data(clipboard: _UserClipboard) -> UserClipboardDat
209209
status=content.status,
210210
block_type=content.block_type,
211211
display_name=content.display_name,
212-
tags=content.tags,
212+
tags=content.tags or {},
213213
version_num=content.version_num,
214214
),
215215
source_usage_key=clipboard.source_usage_key,

openedx/core/djangoapps/content_staging/models.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ class Meta:
6767
version_num = models.PositiveIntegerField(default=0)
6868

6969
# Tags applied to the original source block(s) will be copied to the new block(s) on paste.
70-
tags = models.JSONField(null=True, help_text=_("Content tags applied to these blocks"))
70+
tags: models.JSONField[dict | None, dict | None] = models.JSONField(
71+
null=True, help_text=_("Content tags applied to these blocks")
72+
)
7173

7274
@property
7375
def olx_filename(self) -> str:

openedx/core/djangoapps/content_tagging/rest_api/v1/views.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from rest_framework import status
1111
from rest_framework.decorators import action
1212
from rest_framework.exceptions import PermissionDenied, ValidationError
13-
from rest_framework.request import Request
1413
from rest_framework.response import Response
1514
from rest_framework.views import APIView
1615
from openedx_events.content_authoring.data import ContentObjectData, ContentObjectChangedData
@@ -19,6 +18,8 @@
1918
CONTENT_OBJECT_TAGS_CHANGED,
2019
)
2120

21+
from openedx.core.types.http import RestRequest
22+
2223
from ...auth import has_view_object_tags_access
2324
from ...api import (
2425
create_taxonomy,
@@ -99,7 +100,7 @@ def perform_create(self, serializer):
99100
serializer.instance = create_taxonomy(**serializer.validated_data, orgs=user_admin_orgs)
100101

101102
@action(detail=False, url_path="import", methods=["post"])
102-
def create_import(self, request: Request, **kwargs) -> Response: # type: ignore
103+
def create_import(self, request: RestRequest, **kwargs) -> Response: # type: ignore
103104
"""
104105
Creates a new taxonomy with the given orgs and imports the tags from the uploaded file.
105106
"""
@@ -183,7 +184,7 @@ class ObjectTagExportView(APIView):
183184
""""
184185
View to export a CSV with all children and tags for a given course/context.
185186
"""
186-
def get(self, request: Request, **kwargs) -> StreamingHttpResponse:
187+
def get(self, request: RestRequest, **kwargs) -> StreamingHttpResponse:
187188
"""
188189
Export a CSV with all children and tags for a given course/context.
189190
"""

openedx/core/types/http.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""
2+
Typing utilities for the HTTP requests, responses, etc.
3+
4+
Includes utilties to work with both vanilla django as well as djangorestframework.
5+
"""
6+
from __future__ import annotations
7+
8+
import django.contrib.auth.models # pylint: disable=imported-auth-user
9+
import django.http
10+
import rest_framework.request
11+
12+
import openedx.core.types.user
13+
from openedx.core.types.meta import type_annotation_only
14+
15+
16+
@type_annotation_only
17+
class HttpRequest(django.http.HttpRequest):
18+
"""
19+
A request which either has a concrete User (from django.contrib.auth) or is anonymous.
20+
"""
21+
user: openedx.core.types.User
22+
23+
24+
@type_annotation_only
25+
class AuthenticatedHttpRequest(HttpRequest):
26+
"""
27+
A request which is guaranteed to have a concrete User (from django.contrib.auth).
28+
"""
29+
user: django.contrib.auth.models.User
30+
31+
32+
@type_annotation_only
33+
class RestRequest(rest_framework.request.Request):
34+
"""
35+
Same as HttpRequest, but extended for rest_framework views.
36+
"""
37+
user: openedx.core.types.User
38+
39+
40+
@type_annotation_only
41+
class AuthenticatedRestRequest(RestRequest):
42+
"""
43+
Same as AuthenticatedHttpRequest, but extended for rest_framework views.
44+
"""
45+
user: django.contrib.auth.models.User

openedx/core/types/meta.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Typing utilities for use on other typing utilities.
3+
"""
4+
from __future__ import annotations
5+
6+
import typing as t
7+
8+
9+
def type_annotation_only(cls: type) -> type:
10+
"""
11+
Decorates class which should only be used in type annotations.
12+
13+
This is useful when you want to enhance an existing 3rd-party concrete class with
14+
type annotations for its members, but don't want the enhanced class to ever actually
15+
be instantiated. For examples, see openedx.core.types.http.
16+
"""
17+
if t.TYPE_CHECKING:
18+
return cls
19+
return _forbid_init(cls)
20+
21+
22+
def _forbid_init(forbidden: type) -> type:
23+
"""
24+
Return a class which refuses to be instantiated.
25+
"""
26+
class _ForbidInit:
27+
"""
28+
The resulting class.
29+
"""
30+
def __init__(self, *args, **kwargs):
31+
raise NotImplementedError(
32+
f"Class {forbidden.__module__}:{forbidden.__name__} "
33+
"cannot be instantiated. You may use it as a type annotation, but objects "
34+
"can only be created from its concrete superclasses."
35+
)
36+
37+
return _ForbidInit

openedx/core/types/user.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""
22
Typing utilities for the User models.
33
"""
4-
from typing import Union
4+
from __future__ import annotations
5+
6+
import typing as t
57

68
import django.contrib.auth.models
79

8-
User = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]
10+
User: t.TypeAlias = django.contrib.auth.models.User | django.contrib.auth.models.AnonymousUser

0 commit comments

Comments
 (0)