Skip to content

Commit ae8ac62

Browse files
refactor: improve typing of mixins
1 parent b2f07f4 commit ae8ac62

File tree

5 files changed

+61
-35
lines changed

5 files changed

+61
-35
lines changed

openedx_learning/apps/authoring/components/models.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"""
1818
from __future__ import annotations
1919

20+
from typing import ClassVar
21+
2022
from django.db import models
2123

2224
from ....lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field
@@ -76,7 +78,7 @@ def __str__(self):
7678
return f"{self.namespace}:{self.name}"
7779

7880

79-
class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
81+
class Component(PublishableEntityMixin):
8082
"""
8183
This represents any Component that has ever existed in a LearningPackage.
8284
@@ -120,14 +122,12 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
120122
Make a foreign key to the Component model when you need a stable reference
121123
that will exist for as long as the LearningPackage itself exists.
122124
"""
123-
# Tell mypy what type our objects manager has.
124-
# It's actually PublishableEntityMixinManager, but that has the exact same
125-
# interface as the base manager class.
126-
objects: models.Manager[Component] = WithRelationsManager(
125+
# Set up our custom manager. It has the same API as the default one, but selects related objects by default.
126+
objects: ClassVar[WithRelationsManager[Component]] = WithRelationsManager( # type: ignore[assignment]
127127
'component_type'
128128
)
129129

130-
with_publishing_relations: models.Manager[Component] = WithRelationsManager(
130+
with_publishing_relations = WithRelationsManager(
131131
'component_type',
132132
'publishable_entity',
133133
'publishable_entity__draft__version',
@@ -201,10 +201,6 @@ class ComponentVersion(PublishableEntityVersionMixin):
201201
This holds the content using a M:M relationship with Content via
202202
ComponentVersionContent.
203203
"""
204-
# Tell mypy what type our objects manager has.
205-
# It's actually PublishableEntityVersionMixinManager, but that has the exact
206-
# same interface as the base manager class.
207-
objects: models.Manager[ComponentVersion]
208204

209205
# This is technically redundant, since we can get this through
210206
# publishable_entity_version.publishable.component, but this is more

openedx_learning/apps/authoring/publishing/model_mixins.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from __future__ import annotations
55

66
from functools import cached_property
7+
from typing import ClassVar, Self
78

89
from django.core.exceptions import ImproperlyConfigured
910
from django.db import models
10-
from django.db.models.query import QuerySet
11+
12+
from openedx_learning.lib.managers import WithRelationsManager
1113

1214
from .models import PublishableEntity, PublishableEntityVersion
1315

@@ -28,17 +30,12 @@ class PublishableEntityMixin(models.Model):
2830
the publishing app's api.register_content_models (see its docstring for
2931
details).
3032
"""
31-
32-
class PublishableEntityMixinManager(models.Manager):
33-
def get_queryset(self) -> QuerySet:
34-
return super().get_queryset() \
35-
.select_related(
36-
"publishable_entity",
37-
"publishable_entity__published",
38-
"publishable_entity__draft",
39-
)
40-
41-
objects: models.Manager[PublishableEntityMixin] = PublishableEntityMixinManager()
33+
# select these related entities by default for all queries
34+
objects: ClassVar[WithRelationsManager[Self]] = WithRelationsManager(
35+
"publishable_entity",
36+
"publishable_entity__published",
37+
"publishable_entity__draft",
38+
)
4239

4340
publishable_entity = models.OneToOneField(
4441
PublishableEntity, on_delete=models.CASCADE, primary_key=True
@@ -294,17 +291,10 @@ class PublishableEntityVersionMixin(models.Model):
294291
details).
295292
"""
296293

297-
class PublishableEntityVersionMixinManager(models.Manager):
298-
def get_queryset(self) -> QuerySet:
299-
return (
300-
super()
301-
.get_queryset()
302-
.select_related(
303-
"publishable_entity_version",
304-
)
305-
)
306-
307-
objects: models.Manager[PublishableEntityVersionMixin] = PublishableEntityVersionMixinManager()
294+
# select these related entities by default for all queries
295+
objects: ClassVar[WithRelationsManager[Self]] = WithRelationsManager(
296+
"publishable_entity_version",
297+
)
308298

309299
publishable_entity_version = models.OneToOneField(
310300
PublishableEntityVersion, on_delete=models.CASCADE, primary_key=True

openedx_learning/lib/managers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
"""
22
Custom Django ORM Managers.
33
"""
4+
from __future__ import annotations
5+
6+
from typing import TypeVar
7+
48
from django.db import models
59
from django.db.models.query import QuerySet
610

11+
M = TypeVar('M', bound=models.Model)
12+
713

8-
class WithRelationsManager(models.Manager):
14+
class WithRelationsManager(models.Manager[M]):
915
"""
1016
Custom Manager that adds select_related to the default queryset.
1117

tests/openedx_learning/apps/authoring/components/test_models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Tests related to the Component models
33
"""
44
from datetime import datetime, timezone
5+
from typing import TYPE_CHECKING, assert_type
56

67
from freezegun import freeze_time
78

@@ -10,7 +11,7 @@
1011
get_component,
1112
get_or_create_component_type,
1213
)
13-
from openedx_learning.apps.authoring.components.models import ComponentType
14+
from openedx_learning.apps.authoring.components.models import Component, ComponentType, ComponentVersion
1415
from openedx_learning.apps.authoring.publishing.api import (
1516
LearningPackage,
1617
create_learning_package,
@@ -19,6 +20,15 @@
1920
)
2021
from openedx_learning.lib.test_utils import TestCase
2122

23+
if TYPE_CHECKING:
24+
# Test that our mixins on Component.objects and PublishableEntityVersionMixin etc. haven't broken manager typing
25+
assert_type(Component.objects.create(), Component)
26+
assert_type(Component.objects.get(), Component)
27+
assert_type(Component.with_publishing_relations.create(), Component)
28+
assert_type(Component.with_publishing_relations.get(), Component)
29+
assert_type(ComponentVersion.objects.create(), ComponentVersion)
30+
assert_type(ComponentVersion.objects.get(), ComponentVersion)
31+
2232

2333
class TestModelVersioningQueries(TestCase):
2434
"""
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Tests related to the Component models
3+
"""
4+
from typing import TYPE_CHECKING, assert_type
5+
6+
from openedx_learning.apps.authoring.publishing.model_mixins import (
7+
PublishableEntityMixin,
8+
PublishableEntityVersionMixin,
9+
)
10+
from openedx_learning.lib.managers import WithRelationsManager
11+
12+
if TYPE_CHECKING:
13+
# Test that our mixins provide the right typing for 'objects'
14+
class FooEntity(PublishableEntityMixin):
15+
pass
16+
17+
assert_type(FooEntity.objects.create(), FooEntity)
18+
assert_type(FooEntity.objects, WithRelationsManager[FooEntity])
19+
20+
class FooEntityVersion(PublishableEntityVersionMixin):
21+
pass
22+
23+
assert_type(FooEntityVersion.objects.create(), FooEntityVersion)
24+
assert_type(FooEntityVersion.objects, WithRelationsManager[FooEntityVersion])

0 commit comments

Comments
 (0)