Skip to content

Commit 2fd3f00

Browse files
samiashipfouque
andauthored
Fix admin import without django-stubs runtime monkeypatch (#113)
* Fix admin import without django-stubs runtime monkeypatch Keep the Django generic typing in django_fsm.admin under TYPE_CHECKING so the module no longer evaluates ModelForm[...] or ModelAdmin[...] at runtime. This avoids import-time failures in downstream projects that do not call django_stubs_ext.monkeypatch() before Django admin autodiscovery. Add a regression test that imports django_fsm.admin in a fresh subprocess with only minimal Django settings configured, proving the admin module loads without relying on the test suite's django-stubs monkeypatch. Also document the fix under Unreleased in the changelog. * Move test to dedicated file --------- Co-authored-by: pfouque <pfouque@users.noreply.github.com>
1 parent 6002d07 commit 2fd3f00

3 files changed

Lines changed: 70 additions & 3 deletions

File tree

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Changelog
44
Unreleased
55
~~~~~~~~~~
66

7+
- Fix ``django_fsm.admin`` import failures when Django generic types are not
8+
runtime-subscriptable
9+
710
django-fsm-2 4.2.0 2026-03-07
811
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
912

django_fsm/admin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
from typing_extensions import override
3434

3535
if typing.TYPE_CHECKING: # pragma: no cover
36-
_ModelAdmin = admin.ModelAdmin[fsm._FSMModel]
36+
_ModelAdmin: typing.TypeAlias = admin.ModelAdmin[fsm._FSMModel]
37+
_FormType: typing.TypeAlias = type[Form | ModelForm[fsm._FSMModel]]
3738
else:
3839
_ModelAdmin = admin.ModelAdmin
39-
40-
_FormType: typing.TypeAlias = type[Form | ModelForm[fsm._FSMModel]]
40+
_FormType = type[Form | ModelForm]
4141

4242

4343
@dataclass

tests/testapp/tests/test_typing.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
from django.test import TestCase
9+
10+
11+
class BaseAdminTestCase(TestCase):
12+
project_root: Path
13+
env: dict[str, str]
14+
15+
def setUp(self) -> None:
16+
self.project_root = Path(__file__).resolve().parents[3]
17+
self.env = os.environ.copy()
18+
self.env.pop("DJANGO_SETTINGS_MODULE", None)
19+
python_path = self.env.get("PYTHONPATH")
20+
self.env["PYTHONPATH"] = (
21+
f"{self.project_root}{os.pathsep}{python_path}"
22+
if python_path
23+
else str(self.project_root)
24+
)
25+
26+
def test_admin_module_imports_without_django_stubs_monkeypatch(self) -> None:
27+
result = subprocess.run( # noqa: S603
28+
[
29+
sys.executable,
30+
"-c",
31+
(
32+
"from django.conf import settings; "
33+
"settings.configure(SECRET_KEY='test', USE_I18N=False); "
34+
"import django_fsm.admin"
35+
),
36+
],
37+
capture_output=True,
38+
check=False,
39+
cwd=self.project_root,
40+
env=self.env,
41+
text=True,
42+
)
43+
44+
assert result.returncode == 0, result.stderr
45+
46+
def test_main_module_imports_without_django_stubs_monkeypatch(self) -> None:
47+
result = subprocess.run( # noqa: S603
48+
[
49+
sys.executable,
50+
"-c",
51+
(
52+
"from django.conf import settings; "
53+
"settings.configure(SECRET_KEY='test', USE_I18N=False); "
54+
"import django_fsm"
55+
),
56+
],
57+
capture_output=True,
58+
check=False,
59+
cwd=self.project_root,
60+
env=self.env,
61+
text=True,
62+
)
63+
64+
assert result.returncode == 0, result.stderr

0 commit comments

Comments
 (0)