Skip to content

Commit b658162

Browse files
committed
feat: Make conditions chainable through & and |
1 parent 6eb62e7 commit b658162

File tree

9 files changed

+333
-206
lines changed

9 files changed

+333
-206
lines changed

django_lifecycle/conditions.py

Lines changed: 0 additions & 161 deletions
This file was deleted.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Any
5+
from typing import Iterable
6+
from typing import Union
7+
8+
from django_lifecycle import NotSet
9+
from django_lifecycle.conditions.base import ChainableCondition
10+
11+
12+
__all__ = [
13+
"WhenFieldWas",
14+
"WhenFieldIsNow",
15+
"WhenFieldHasChanged",
16+
"WhenFieldIsNot",
17+
"WhenFieldWasNot",
18+
"WhenFieldChangesTo",
19+
"Always",
20+
]
21+
22+
23+
@dataclass
24+
class WhenFieldWas(ChainableCondition):
25+
field_name: str
26+
was: Any = "*"
27+
28+
def __call__(
29+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
30+
) -> bool:
31+
return self.was in (instance.initial_value(self.field_name), "*")
32+
33+
34+
@dataclass
35+
class WhenFieldIsNow(ChainableCondition):
36+
field_name: str
37+
is_now: Any = "*"
38+
39+
def __call__(
40+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
41+
) -> bool:
42+
return self.is_now in (instance._current_value(self.field_name), "*")
43+
44+
45+
@dataclass
46+
class WhenFieldHasChanged(ChainableCondition):
47+
field_name: str
48+
has_changed: bool | None = None
49+
50+
def __call__(
51+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
52+
) -> bool:
53+
is_partial_fields_update = update_fields is not None
54+
is_synced = (
55+
is_partial_fields_update is False or self.field_name in update_fields
56+
)
57+
if not is_synced:
58+
return False
59+
60+
return self.has_changed is None or self.has_changed == instance.has_changed(
61+
self.field_name
62+
)
63+
64+
65+
@dataclass
66+
class WhenFieldIsNot(ChainableCondition):
67+
field_name: str
68+
is_not: Any = NotSet
69+
70+
def __call__(
71+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
72+
) -> bool:
73+
return (
74+
self.is_not is NotSet
75+
or instance._current_value(self.field_name) != self.is_not
76+
)
77+
78+
79+
@dataclass
80+
class WhenFieldWasNot(ChainableCondition):
81+
field_name: str
82+
was_not: Any = NotSet
83+
84+
def __call__(
85+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
86+
) -> bool:
87+
return (
88+
self.was_not is NotSet
89+
or instance.initial_value(self.field_name) != self.was_not
90+
)
91+
92+
93+
@dataclass
94+
class WhenFieldChangesTo(ChainableCondition):
95+
field_name: str
96+
changes_to: Any = NotSet
97+
98+
def __call__(
99+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
100+
) -> bool:
101+
is_partial_fields_update = update_fields is not None
102+
is_synced = (
103+
is_partial_fields_update is False or self.field_name in update_fields
104+
)
105+
if not is_synced:
106+
return False
107+
108+
value_has_changed = bool(
109+
instance.initial_value(self.field_name) != self.changes_to
110+
)
111+
new_value_is_the_expected = bool(
112+
instance._current_value(self.field_name) == self.changes_to
113+
)
114+
return self.changes_to is NotSet or (
115+
value_has_changed and new_value_is_the_expected
116+
)
117+
118+
119+
class Always:
120+
def __call__(self, instance: Any, update_fields=None):
121+
return True
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import annotations
2+
3+
import operator
4+
from dataclasses import dataclass
5+
from typing import Any
6+
from typing import Callable
7+
from typing import Iterable
8+
from typing import Union
9+
10+
from django_lifecycle import types
11+
12+
13+
@dataclass
14+
class ChainedCondition:
15+
def __init__(
16+
self,
17+
left: types.Condition,
18+
right: types.Condition,
19+
operator: Callable[[Any, Any], bool],
20+
):
21+
self.left = left
22+
self.right = right
23+
self.operator = operator
24+
25+
def __and__(self, other):
26+
return ChainedCondition(self, other, operator=operator.and_)
27+
28+
def __or__(self, other):
29+
return ChainedCondition(self, other, operator=operator.or_)
30+
31+
def __call__(
32+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
33+
) -> bool:
34+
left_result = self.left(instance, update_fields)
35+
right_result = self.right(instance, update_fields)
36+
return self.operator(left_result, right_result)
37+
38+
39+
class ChainableCondition:
40+
"""Base class for defining chainable conditions using `&` and `|`"""
41+
42+
def __and__(self, other) -> ChainedCondition:
43+
return ChainedCondition(self, other, operator=operator.and_)
44+
45+
def __or__(self, other) -> ChainedCondition:
46+
return ChainedCondition(self, other, operator=operator.or_)
47+
48+
def __call__(
49+
self, instance: Any, update_fields: Union[Iterable[str], None] = None
50+
) -> bool:
51+
...

0 commit comments

Comments
 (0)