Skip to content

Commit 86f03cf

Browse files
committed
feat: add comprehensive type annotations
1 parent 2468a08 commit 86f03cf

31 files changed

+771
-365
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ jobs:
7474
else
7575
just test-lock Django~=${{ matrix.django-version }}.0
7676
fi
77+
just install
7778
just install-docs
7879
- name: Install Emacs
7980
if: ${{ github.event.inputs.debug == 'true' }}

justfile

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,16 @@ install-playwright:
5353
install-docs:
5454
uv sync --group docs --all-extras
5555

56-
# run static type checking
57-
check-types:
58-
#TODO @just run mypy src/polymorphic
56+
# run static type checking with mypy
57+
check-types-mypy:
58+
@just run mypy src/polymorphic
59+
60+
# run static type checking with pyright
61+
check-types-pyright:
62+
@just run pyright src/polymorphic
63+
64+
# run all static type checking
65+
check-types: check-types-mypy check-types-pyright
5966

6067
# run package checks
6168
check-package:

pyproject.toml

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ exclude = ["src/polymorphic/tests"]
7676

7777
[tool.hatch.build.targets.wheel]
7878
packages = ["src/polymorphic"]
79-
artifacts = ["*.mo"]
79+
artifacts = ["*.mo", "py.typed"]
8080

8181
[tool.doc8]
8282
max-line-length = 100
@@ -145,15 +145,66 @@ relative_files = true
145145
[tool.coverage.report]
146146
show_missing = true
147147

148+
[tool.mypy]
149+
python_version = "3.10"
150+
strict = false
151+
warn_return_any = true
152+
warn_unused_configs = true
153+
disallow_untyped_defs = false
154+
disallow_incomplete_defs = false
155+
check_untyped_defs = true
156+
disallow_untyped_decorators = false
157+
no_implicit_optional = true
158+
warn_redundant_casts = true
159+
warn_unused_ignores = true
160+
warn_no_return = true
161+
warn_unreachable = true
162+
strict_equality = true
163+
plugins = ["mypy_django_plugin.main"]
164+
165+
[[tool.mypy.overrides]]
166+
module = "polymorphic.tests.*"
167+
ignore_errors = true
168+
169+
[[tool.mypy.overrides]]
170+
module = "example.*"
171+
ignore_errors = true
172+
173+
[tool.django-stubs]
174+
django_settings_module = "polymorphic.tests.settings"
175+
176+
[tool.pyright]
177+
pythonVersion = "3.10"
178+
typeCheckingMode = "basic"
179+
include = ["src/polymorphic"]
180+
exclude = [
181+
"**/migrations",
182+
"**/tests",
183+
"example",
184+
]
185+
reportMissingImports = true
186+
reportMissingTypeStubs = false
187+
reportUnusedImport = true
188+
reportUnusedClass = false
189+
reportUnusedFunction = false
190+
reportUnusedVariable = true
191+
reportDuplicateImport = true
192+
reportOptionalMemberAccess = false
193+
reportGeneralTypeIssues = false
194+
reportOptionalSubscript = false
195+
reportPrivateImportUsage = false
196+
148197
[dependency-groups]
149198
dev = [
150199
"coverage>=7.6.1",
151200
"dj-database-url>=2.2.0",
201+
"django-stubs>=5.1.1",
152202
"django-test-migrations>=1.5.0",
153203
"ipdb>=0.13.13",
154204
"ipython>=8.18.1",
155205
"mypy>=1.14.1",
156206
"pre-commit>=3.5.0",
207+
"pyright>=1.1.390",
157208
"pytest>=8.3.4",
158209
"pytest-cov>=5.0.0",
159210
"pytest-django>=4.10.0",

src/polymorphic/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
Seamless Polymorphic Inheritance for Django Models
2020
"""
2121

22-
VERSION = "4.10.4"
22+
from typing import Final
2323

24-
__title__ = "Django Polymorphic"
24+
VERSION: Final[str] = "4.10.4"
25+
26+
__title__: Final = "Django Polymorphic"
2527
__version__ = VERSION # version synonym for backwards compatibility
26-
__author__ = "Brian Kohan"
27-
__license__ = "BSD-3-Clause"
28-
__copyright__ = (
28+
__author__: Final[str] = "Brian Kohan"
29+
__license__: Final = "BSD-3-Clause"
30+
__copyright__: Final[str] = (
2931
"Copyright 2010-2025, Bert Constantin, Chris Glass, Diederik van der Boor, Brian Kohan"
3032
)

src/polymorphic/admin/childadmin.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,36 @@
22
The child admin displays the change/delete view of the subclass model.
33
"""
44

5+
from __future__ import annotations
6+
57
import inspect
8+
from typing import TYPE_CHECKING, Any
69

710
from django.contrib import admin
11+
from django.db import models
12+
from django.forms import ModelForm
13+
from django.http import HttpRequest
814
from django.urls import resolve
915
from django.utils.translation import gettext_lazy as _
16+
from typing_extensions import TypeVar
1017

1118
from polymorphic.utils import get_base_polymorphic_model
1219

1320
from ..admin import PolymorphicParentModelAdmin
1421

22+
_ModelT = TypeVar("_ModelT", bound=models.Model, default=models.Model)
23+
24+
if TYPE_CHECKING:
25+
_ModelAdminBase = admin.ModelAdmin[_ModelT]
26+
else:
27+
_ModelAdminBase = admin.ModelAdmin
28+
1529

1630
class ParentAdminNotRegistered(RuntimeError):
1731
"The admin site for the model is not registered."
1832

1933

20-
class PolymorphicChildModelAdmin(admin.ModelAdmin):
34+
class PolymorphicChildModelAdmin(_ModelAdminBase):
2135
"""
2236
The *optional* base class for the admin interface of derived models.
2337
@@ -30,30 +44,32 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin):
3044
"""
3145

3246
#: The base model that the class uses (auto-detected if not set explicitly)
33-
base_model = None
47+
base_model: type[models.Model] | None = None
3448

3549
#: By setting ``base_form`` instead of ``form``, any subclass fields are automatically added to the form.
3650
#: This is useful when your model admin class is inherited by others.
37-
base_form = None
51+
base_form: type[ModelForm[Any]] | None = None
3852

3953
#: By setting ``base_fieldsets`` instead of ``fieldsets``,
4054
#: any subclass fields can be automatically added.
4155
#: This is useful when your model admin class is inherited by others.
42-
base_fieldsets = None
56+
base_fieldsets: Any = None
4357

4458
#: Default title for extra fieldset
4559
extra_fieldset_title = _("Contents")
4660

4761
#: Whether the child admin model should be visible in the admin index page.
4862
show_in_index = False
4963

50-
def __init__(self, model, admin_site, *args, **kwargs):
64+
def __init__(self, model: type[_ModelT], admin_site: Any, *args: Any, **kwargs: Any) -> None:
5165
super().__init__(model, admin_site, *args, **kwargs)
5266

5367
if self.base_model is None:
5468
self.base_model = get_base_polymorphic_model(model)
5569

56-
def get_form(self, request, obj=None, **kwargs):
70+
def get_form(
71+
self, request: HttpRequest, obj: Any | None = None, change: bool = False, **kwargs: Any
72+
) -> type[ModelForm[Any]]:
5773
# The django admin validation requires the form to have a 'class Meta: model = ..'
5874
# attribute, or it will complain that the fields are missing.
5975
# However, this enforces all derived ModelAdmin classes to redefine the model as well,
@@ -77,7 +93,7 @@ def get_model_perms(self, request):
7793
return super().get_model_perms(request)
7894

7995
@property
80-
def change_form_template(self):
96+
def change_form_template(self) -> list[str]: # type: ignore[override]
8197
opts = self.model._meta
8298
app_label = opts.app_label
8399

@@ -96,7 +112,7 @@ def change_form_template(self):
96112
]
97113

98114
@property
99-
def delete_confirmation_template(self):
115+
def delete_confirmation_template(self) -> list[str]: # type: ignore[override]
100116
opts = self.model._meta
101117
app_label = opts.app_label
102118

@@ -115,7 +131,7 @@ def delete_confirmation_template(self):
115131
]
116132

117133
@property
118-
def object_history_template(self):
134+
def object_history_template(self) -> list[str]: # type: ignore[override]
119135
opts = self.model._meta
120136
app_label = opts.app_label
121137

src/polymorphic/admin/filters.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
4+
from typing import Any
5+
16
from django.contrib import admin
27
from django.core.exceptions import PermissionDenied
8+
from django.db.models import QuerySet
9+
from django.http import HttpRequest
310
from django.utils.translation import gettext_lazy as _
411

512

@@ -15,13 +22,15 @@ class PolymorphicChildModelFilter(admin.SimpleListFilter):
1522
list_filter = (PolymorphicChildModelFilter,)
1623
"""
1724

18-
title = _("Type")
19-
parameter_name = "polymorphic_ctype"
25+
title: str = _("Type") # type: ignore[assignment]
26+
parameter_name: str = "polymorphic_ctype"
2027

21-
def lookups(self, request, model_admin):
28+
def lookups(
29+
self, request: HttpRequest, model_admin: admin.ModelAdmin[Any]
30+
) -> Iterable[tuple[str, str]]:
2231
return model_admin.get_child_type_choices(request, "change")
2332

24-
def queryset(self, request, queryset):
33+
def queryset(self, request: HttpRequest, queryset: QuerySet[Any]) -> QuerySet[Any]:
2534
try:
2635
value = int(self.value())
2736
except TypeError:

src/polymorphic/admin/forms.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any
2+
13
from django import forms
24
from django.contrib.admin.widgets import AdminRadioSelect
35
from django.utils.translation import gettext_lazy as _
@@ -15,7 +17,7 @@ class PolymorphicModelChoiceForm(forms.Form):
1517
label=type_label, widget=AdminRadioSelect(attrs={"class": "radiolist"})
1618
)
1719

18-
def __init__(self, *args, **kwargs):
20+
def __init__(self, *args: Any, **kwargs: Any) -> None:
1921
# Allow to easily redefine the label (a commonly expected usecase)
2022
super().__init__(*args, **kwargs)
2123
self.fields["ct_id"].label = self.type_label

src/polymorphic/admin/generic.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Any
2+
13
from django.contrib.contenttypes.admin import GenericInlineModelAdmin
24
from django.contrib.contenttypes.models import ContentType
5+
from django.http import HttpRequest
36
from django.utils.functional import cached_property
47

58
from polymorphic.formsets import (
@@ -17,9 +20,11 @@ class GenericPolymorphicInlineModelAdmin(PolymorphicInlineModelAdmin, GenericInl
1720
"""
1821

1922
#: The formset class
20-
formset = BaseGenericPolymorphicInlineFormSet
23+
formset: type[BaseGenericPolymorphicInlineFormSet] = BaseGenericPolymorphicInlineFormSet # type: ignore[assignment]
2124

22-
def get_formset(self, request, obj=None, **kwargs):
25+
def get_formset(
26+
self, request: HttpRequest, obj: Any = None, **kwargs: Any
27+
) -> type[BaseGenericPolymorphicInlineFormSet]: # type: ignore[override]
2328
"""
2429
Construct the generic inline formset class.
2530
"""
@@ -39,19 +44,21 @@ class Child(PolymorphicInlineModelAdmin.Child):
3944
"""
4045

4146
# Make sure that the GFK fields are excluded from the child forms
42-
formset_child = GenericPolymorphicFormSetChild
43-
ct_field = "content_type"
44-
ct_fk_field = "object_id"
47+
formset_child: type[GenericPolymorphicFormSetChild] = GenericPolymorphicFormSetChild # type: ignore[assignment]
48+
ct_field: str = "content_type"
49+
ct_fk_field: str = "object_id"
4550

4651
@cached_property
47-
def content_type(self):
52+
def content_type(self) -> ContentType:
4853
"""
4954
Expose the ContentType that the child relates to.
5055
This can be used for the ``polymorphic_ctype`` field.
5156
"""
5257
return ContentType.objects.get_for_model(self.model, for_concrete_model=False)
5358

54-
def get_formset_child(self, request, obj=None, **kwargs):
59+
def get_formset_child(
60+
self, request: HttpRequest, obj: Any = None, **kwargs: Any
61+
) -> GenericPolymorphicFormSetChild: # type: ignore[override]
5562
# Similar to GenericInlineModelAdmin.get_formset(),
5663
# make sure the GFK is automatically excluded from the form
5764
defaults = {"ct_field": self.ct_field, "fk_field": self.ct_fk_field}
@@ -67,4 +74,4 @@ class GenericStackedPolymorphicInline(GenericPolymorphicInlineModelAdmin):
6774
"""
6875

6976
#: The default template to use.
70-
template = "admin/polymorphic/edit_inline/stacked.html"
77+
template: str = "admin/polymorphic/edit_inline/stacked.html"

0 commit comments

Comments
 (0)