Skip to content

Commit 90c05e1

Browse files
committed
Step 7
1 parent 26368af commit 90c05e1

File tree

8 files changed

+125
-77
lines changed

8 files changed

+125
-77
lines changed

.mypy.ini

Lines changed: 0 additions & 38 deletions
This file was deleted.

.pre-commit-config.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ repos:
2525

2626

2727
- repo: https://github.com/python-poetry/poetry
28-
rev: 1.6.1
28+
rev: 1.7.0
2929
hooks:
3030
- id: poetry-check
3131
additional_dependencies:
@@ -42,8 +42,9 @@ repos:
4242
- id: ruff
4343

4444
- repo: https://github.com/pre-commit/mirrors-mypy
45-
rev: v1.7.0
45+
rev: v1.7.1
4646
hooks:
4747
- id: mypy
4848
additional_dependencies:
4949
- django-stubs==4.2.6
50+
- django-guardian

django_fsm/__init__.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
from __future__ import annotations
55

66
import inspect
7+
from collections.abc import Callable
8+
from collections.abc import Collection
9+
from collections.abc import Generator
10+
from collections.abc import Iterable
11+
from collections.abc import Sequence
712
from functools import partialmethod
813
from functools import wraps
914
from typing import TYPE_CHECKING
15+
from typing import Any
16+
from typing import Self
1017

1118
from django.apps import apps as django_apps
1219
from django.db import models
1320
from django.db.models import Field
21+
from django.db.models import QuerySet
1422
from django.db.models.query_utils import DeferredAttribute
1523
from django.db.models.signals import class_prepared
1624

@@ -33,26 +41,21 @@
3341
]
3442

3543
if TYPE_CHECKING:
36-
from collections.abc import Callable
37-
from collections.abc import Generator
38-
from collections.abc import Iterable
39-
from collections.abc import Sequence
40-
from typing import Any
41-
44+
from _typeshed import Incomplete
4245
from django.contrib.auth.models import PermissionsMixin as UserWithPermissions
4346
from django.utils.functional import _StrOrPromise
4447

45-
_Model = models.Model
48+
_FSMModel = models.Model
4649
_Field = models.Field[Any, Any]
4750
CharField = models.CharField[Any, Any]
4851
IntegerField = models.IntegerField[Any, Any]
4952
ForeignKey = models.ForeignKey[Any, Any]
5053

5154
_StateValue = str | int
55+
_Permission = str | Callable[[_FSMModel, UserWithPermissions], bool]
5256
_Instance = models.Model # TODO: use real type
53-
_ToDo = Any # TODO: use real type
5457
else:
55-
_Model = object
58+
_FSMModel = object
5659
_Field = object
5760
CharField = models.CharField
5861
IntegerField = models.IntegerField
@@ -265,7 +268,7 @@ class FSMFieldMixin(_Field):
265268

266269
def __init__(self, *args: Any, **kwargs: Any) -> None:
267270
self.protected = kwargs.pop("protected", False)
268-
self.transitions: dict[type[_Model], dict[str, Any]] = {} # cls -> (transitions name -> method)
271+
self.transitions: dict[type[_FSMModel], dict[str, Any]] = {} # cls -> (transitions name -> method)
269272
self.state_proxy = {} # state -> ProxyClsRef
270273

271274
state_choices = kwargs.pop("state_choices", None)
@@ -317,7 +320,7 @@ def set_proxy(self, instance: _Instance, state: str) -> None:
317320

318321
instance.__class__ = model
319322

320-
def change_state(self, instance: _Instance, method: _ToDo, *args: Any, **kwargs: Any) -> Any:
323+
def change_state(self, instance: _Instance, method: Incomplete, *args: Any, **kwargs: Any) -> Any:
321324
meta = method._django_fsm
322325
method_name = method.__name__
323326
current_state = self.get_state(instance)
@@ -370,7 +373,7 @@ def change_state(self, instance: _Instance, method: _ToDo, *args: Any, **kwargs:
370373

371374
return result
372375

373-
def get_all_transitions(self, instance_cls: type[_Model]) -> Generator[Transition, None, None]:
376+
def get_all_transitions(self, instance_cls: type[_FSMModel]) -> Generator[Transition, None, None]:
374377
"""
375378
Returns [(source, target, name, method)] for all field transitions
376379
"""
@@ -382,7 +385,7 @@ def get_all_transitions(self, instance_cls: type[_Model]) -> Generator[Transitio
382385
for transition in meta.transitions.values():
383386
yield transition
384387

385-
def contribute_to_class(self, cls: type[_Model], name: str, private_only: bool = False, **kwargs: Any) -> None:
388+
def contribute_to_class(self, cls: type[_FSMModel], name: str, private_only: bool = False, **kwargs: Any) -> None:
386389
self.base_cls = cls
387390

388391
super().contribute_to_class(cls, name, private_only=private_only, **kwargs)
@@ -403,7 +406,7 @@ def _collect_transitions(self, *args: Any, **kwargs: Any) -> None:
403406
if not issubclass(sender, self.base_cls):
404407
return
405408

406-
def is_field_transition_method(attr: _ToDo) -> bool:
409+
def is_field_transition_method(attr: Incomplete) -> bool:
407410
return (
408411
(inspect.ismethod(attr) or inspect.isfunction(attr))
409412
and hasattr(attr, "_django_fsm")
@@ -449,14 +452,14 @@ class FSMKeyField(FSMFieldMixin, ForeignKey):
449452
State Machine support for Django model
450453
"""
451454

452-
def get_state(self, instance: _Instance) -> _ToDo:
455+
def get_state(self, instance: _Instance) -> Incomplete:
453456
return instance.__dict__[self.attname]
454457

455458
def set_state(self, instance: _Instance, state: str) -> None:
456459
instance.__dict__[self.attname] = self.to_python(state)
457460

458461

459-
class ConcurrentTransitionMixin(_Model):
462+
class ConcurrentTransitionMixin(_FSMModel):
460463
"""
461464
Protects a Model from undesirable effects caused by concurrently executed transitions,
462465
e.g. running the same transition multiple times at the same time, or running different
@@ -490,7 +493,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
490493
def state_fields(self) -> Iterable[Any]:
491494
return filter(lambda field: isinstance(field, FSMFieldMixin), self._meta.fields)
492495

493-
def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update): # type: ignore[no-untyped-def]
496+
def _do_update(
497+
self,
498+
base_qs: QuerySet[Self],
499+
using: Any,
500+
pk_val: Any,
501+
values: Collection[Any],
502+
update_fields: Iterable[str],
503+
forced_update: bool,
504+
) -> bool:
494505
# _do_update is called once for each model class in the inheritance hierarchy.
495506
# We can only filter the base_qs on state fields (can be more than one!) present in this particular model.
496507

@@ -500,7 +511,7 @@ def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_updat
500511
# state filter will be used to narrow down the standard filter checking only PK
501512
state_filter = {field.attname: self.__initial_states[field.attname] for field in filter_on}
502513

503-
updated = super()._do_update( # type: ignore[misc]
514+
updated: bool = super()._do_update( # type: ignore[misc]
504515
base_qs=base_qs.filter(**state_filter),
505516
using=using,
506517
pk_val=pk_val,
@@ -538,7 +549,7 @@ def transition(
538549
target: _StateValue | State | None = None,
539550
on_error: _StateValue | None = None,
540551
conditions: list[Callable[[Any], bool]] = [],
541-
permission: str | Callable[[models.Model, UserWithPermissions], bool] | None = None,
552+
permission: _Permission | None = None,
542553
custom: dict[str, _StrOrPromise] = {},
543554
) -> Callable[[Any], Any]:
544555
"""
@@ -548,21 +559,22 @@ def transition(
548559
has not changed after the function call.
549560
"""
550561

551-
def inner_transition(func: _ToDo) -> _ToDo:
562+
def inner_transition(func: Incomplete) -> Incomplete:
552563
wrapper_installed, fsm_meta = True, getattr(func, "_django_fsm", None)
553564
if not fsm_meta:
554565
wrapper_installed = False
555566
fsm_meta = FSMMeta(field=field, method=func)
556567
setattr(func, "_django_fsm", fsm_meta)
557568

569+
# if isinstance(source, Iterable):
558570
if isinstance(source, (list, tuple, set)):
559571
for state in source:
560572
func._django_fsm.add_transition(func, state, target, on_error, conditions, permission, custom)
561573
else:
562574
func._django_fsm.add_transition(func, source, target, on_error, conditions, permission, custom)
563575

564576
@wraps(func)
565-
def _change_state(instance: _Instance, *args: Any, **kwargs: Any) -> _ToDo:
577+
def _change_state(instance: _Instance, *args: Any, **kwargs: Any) -> Incomplete:
566578
return fsm_meta.field.change_state(instance, func, *args, **kwargs)
567579

568580
if not wrapper_installed:
@@ -573,7 +585,7 @@ def _change_state(instance: _Instance, *args: Any, **kwargs: Any) -> _ToDo:
573585
return inner_transition
574586

575587

576-
def can_proceed(bound_method: _ToDo, check_conditions: bool = True) -> bool:
588+
def can_proceed(bound_method: Incomplete, check_conditions: bool = True) -> bool:
577589
"""
578590
Returns True if model in state allows to call bound_method
579591
@@ -590,7 +602,7 @@ def can_proceed(bound_method: _ToDo, check_conditions: bool = True) -> bool:
590602
return meta.has_transition(current_state) and (not check_conditions or meta.conditions_met(self, current_state))
591603

592604

593-
def has_transition_perm(bound_method: _ToDo, user: UserWithPermissions) -> bool:
605+
def has_transition_perm(bound_method: Incomplete, user: UserWithPermissions) -> bool:
594606
"""
595607
Returns True if model in state allows to call bound_method and user have rights on it
596608
"""
@@ -609,15 +621,15 @@ def has_transition_perm(bound_method: _ToDo, user: UserWithPermissions) -> bool:
609621

610622

611623
class State:
612-
def get_state(self, model: _Model, transition: Transition, result: Any, args: Any = [], kwargs: Any = {}) -> _ToDo:
624+
def get_state(self, model: _FSMModel, transition: Transition, result: Any, args: Any = [], kwargs: Any = {}) -> Incomplete:
613625
raise NotImplementedError
614626

615627

616628
class RETURN_VALUE(State):
617629
def __init__(self, *allowed_states: Sequence[_StateValue]) -> None:
618630
self.allowed_states = allowed_states if allowed_states else None
619631

620-
def get_state(self, model: _Model, transition: Transition, result: Any, args: Any = [], kwargs: Any = {}) -> _ToDo:
632+
def get_state(self, model: _FSMModel, transition: Transition, result: Any, args: Any = [], kwargs: Any = {}) -> Incomplete:
621633
if self.allowed_states is not None:
622634
if result not in self.allowed_states:
623635
raise InvalidResultState(f"{result} is not in list of allowed states\n{self.allowed_states}")
@@ -630,8 +642,8 @@ def __init__(self, func: Callable[..., _StateValue | Any], states: Sequence[_Sta
630642
self.allowed_states = states
631643

632644
def get_state(
633-
self, model: _Model, transition: Transition, result: _StateValue | Any, args: Any = [], kwargs: Any = {}
634-
) -> _ToDo:
645+
self, model: _FSMModel, transition: Transition, result: _StateValue | Any, args: Any = [], kwargs: Any = {}
646+
) -> Incomplete:
635647
result_state = self.func(model, *args, **kwargs)
636648
if self.allowed_states is not None:
637649
if result_state not in self.allowed_states:

pyproject.toml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,79 @@ fixable = ["I"]
6969
force-single-line = true
7070
required-imports = ["from __future__ import annotations"]
7171

72+
[tool.django-stubs]
73+
django_settings_module = "tests.settings"
74+
75+
[tool.mypy]
76+
python_version = 3.11
77+
plugins = ["mypy_django_plugin.main"]
78+
79+
# Start off with these
80+
warn_unused_configs = true
81+
warn_redundant_casts = true
82+
warn_unused_ignores = true
83+
84+
# Getting these passing should be easy
85+
strict_equality = true
86+
extra_checks = true
87+
88+
# Strongly recommend enabling this one as soon as you can
89+
check_untyped_defs = true
90+
91+
# These shouldn't be too much additional work, but may be tricky to
92+
# get passing if you use a lot of untyped libraries
93+
disallow_subclassing_any = true
94+
disallow_untyped_decorators = true
95+
disallow_any_generics = true
96+
97+
# These next few are various gradations of forcing use of type annotations
98+
disallow_untyped_calls = true
99+
disallow_incomplete_defs = true
100+
disallow_untyped_defs = true
101+
102+
# This one isn't too hard to get passing, but return on investment is lower
103+
no_implicit_reexport = true
104+
105+
# This one can be tricky to get passing if you use a lot of untyped libraries
106+
warn_return_any = true
107+
108+
[[tool.mypy.overrides]]
109+
module = [
110+
"tests.*",
111+
"django_fsm.tests.*"
112+
]
113+
ignore_errors = true
114+
115+
# Start off with these
116+
warn_unused_ignores = true
117+
118+
# Getting these passing should be easy
119+
strict_equality = false
120+
extra_checks = false
121+
122+
# Strongly recommend enabling this one as soon as you can
123+
check_untyped_defs = false
124+
# These shouldn't be too much additional work, but may be tricky to
125+
# get passing if you use a lot of untyped libraries
126+
disallow_subclassing_any = false
127+
disallow_untyped_decorators = false
128+
disallow_any_generics = false
129+
130+
# These next few are various gradations of forcing use of type annotations
131+
disallow_untyped_calls = false
132+
disallow_incomplete_defs = false
133+
disallow_untyped_defs = false
134+
135+
# This one isn't too hard to get passing, but return on investment is lower
136+
no_implicit_reexport = false
137+
138+
# This one can be tricky to get passing if you use a lot of untyped libraries
139+
warn_return_any = false
140+
141+
[[tool.mypy.overrides]]
142+
module = "django_fsm.management.commands.graph_transitions"
143+
ignore_errors = true
144+
72145
[build-system]
73146
requires = ["poetry-core"]
74147
build-backend = "poetry.core.masonry.api"

tests/testapp/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class BlogPost(models.Model):
9393

9494
state = FSMField(default="new", protected=True)
9595

96-
def can_restore(self, user):
96+
def can_restore(self, user) -> bool:
9797
return user.is_superuser or user.is_staff
9898

9999
@transition(field=state, source="new", target="published", on_error="failed", permission="testapp.can_publish_post")

tests/testapp/tests/test_multidecorators.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django_fsm.signals import post_transition
99

1010

11-
class TestModel(models.Model):
11+
class MultipletransitionsModel(models.Model):
1212
counter = models.IntegerField(default=0)
1313
signal_counter = models.IntegerField(default=0)
1414
state = FSMField(default="SUBMITTED_BY_USER")
@@ -27,12 +27,12 @@ def count_calls(sender, instance, name, source, target, **kwargs):
2727
instance.signal_counter += 1
2828

2929

30-
post_transition.connect(count_calls, sender=TestModel)
30+
post_transition.connect(count_calls, sender=MultipletransitionsModel)
3131

3232

3333
class TestStateProxy(TestCase):
3434
def test_transition_method_called_once(self):
35-
model = TestModel()
35+
model = MultipletransitionsModel()
3636
model.review()
3737
self.assertEqual(1, model.counter)
3838
self.assertEqual(1, model.signal_counter)

0 commit comments

Comments
 (0)