Skip to content

Commit 0b2c6cd

Browse files
committed
Merge branch 'release/2.1.1'
2 parents f95391a + 1b0664c commit 0b2c6cd

File tree

11 files changed

+123
-35
lines changed

11 files changed

+123
-35
lines changed

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ build:
2121
- poetry config virtualenvs.create false
2222
# Install dependencies with 'docs' dependency group
2323
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
24-
- poetry install --with diagrams,docs
24+
- poetry install --extras diagrams --with docs
2525

2626
# Build documentation in the docs/ directory with Sphinx
2727
sphinx:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ https://python-statemachine.readthedocs.io.
346346
- If you found this project helpful, please consider giving it a star on GitHub.
347347

348348
- **Contribute code**: If you would like to contribute code to this project, please submit a pull
349-
request. For more information on how to contribute, please see our [contributing.md]contributing.md) file.
349+
request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
350350

351351
- **Report bugs**: If you find any bugs in this project, please report them by opening an issue
352352
on our GitHub issue tracker.

docs/releases/2.1.1.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# StateMachine 2.1.1
2+
3+
*August 3, 2023*
4+
5+
6+
## Bugfixes in 2.1.1
7+
8+
- Fixes [#391](https://github.com/fgmacedo/python-statemachine/issues/391) adding support to
9+
[pytest-mock](https://pytest-mock.readthedocs.io/en/latest/index.html) `spy` method.
10+
- Improved factory type hints [#399](https://github.com/fgmacedo/python-statemachine/pull/399).

docs/releases/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Below are release notes through StateMachine and its patch releases.
1515
```{toctree}
1616
:maxdepth: 1
1717
18+
2.1.1
1819
2.1.0
1920
2.0.0
2021

poetry.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-statemachine"
3-
version = "2.1.0"
3+
version = "2.1.1"
44
description = "Python Finite State Machines made easy."
55
authors = ["Fernando Macedo <[email protected]>"]
66
maintainers = [
@@ -45,6 +45,7 @@ pre-commit = "^2.21.0"
4545
mypy = "^0.991"
4646
black = "^22.12.0"
4747
pdbpp = "^0.10.3"
48+
pytest-mock = "^3.10.0"
4849

4950
[tool.poetry.group.docs.dependencies]
5051
Sphinx = "4.5.0"

statemachine/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
__author__ = """Fernando Macedo"""
55
__email__ = "[email protected]"
6-
__version__ = "2.1.0"
6+
__version__ = "2.1.1"
77

88
__all__ = ["StateMachine", "State"]

statemachine/factory.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import TYPE_CHECKING
22
from typing import Any
33
from typing import Dict
4+
from typing import List
45
from typing import Tuple
56
from uuid import uuid4
67

@@ -31,7 +32,13 @@ def __init__(cls, name: str, bases: Tuple[type], attrs: Dict[str, Any]):
3132
cls.add_inherited(bases)
3233
cls.add_from_attributes(attrs)
3334

34-
cls._set_special_states()
35+
try:
36+
cls.initial_state: State = next(s for s in cls.states if s.initial)
37+
except StopIteration:
38+
cls.initial_state = None # Abstract SM still don't have states
39+
40+
cls.final_states: List[State] = [state for state in cls.states if state.final]
41+
3542
cls._check()
3643

3744
if TYPE_CHECKING:
@@ -40,35 +47,6 @@ def __init__(cls, name: str, bases: Tuple[type], attrs: Dict[str, Any]):
4047
def __getattr__(self, attribute: str) -> Any:
4148
...
4249

43-
def _set_special_states(cls):
44-
if not cls.states:
45-
return
46-
initials = [s for s in cls.states if s.initial]
47-
if len(initials) != 1:
48-
raise InvalidDefinition(
49-
_(
50-
"There should be one and only one initial state. "
51-
"Your currently have these: {!r}"
52-
).format([s.id for s in initials])
53-
)
54-
cls.initial_state = initials[0]
55-
cls.final_states = [state for state in cls.states if state.final]
56-
57-
def _disconnected_states(cls, starting_state):
58-
visitable_states = set(visit_connected_states(starting_state))
59-
return set(cls.states) - visitable_states
60-
61-
def _check_disconnected_state(cls):
62-
disconnected_states = cls._disconnected_states(cls.initial_state)
63-
if disconnected_states:
64-
raise InvalidDefinition(
65-
_(
66-
"There are unreachable states. "
67-
"The statemachine graph should have a single component. "
68-
"Disconnected states: {}"
69-
).format([s.id for s in disconnected_states])
70-
)
71-
7250
def _check(cls):
7351
has_states = bool(cls.states)
7452
has_events = bool(cls._events)
@@ -85,8 +63,21 @@ def _check(cls):
8563
if not has_events:
8664
raise InvalidDefinition(_("There are no events."))
8765

66+
cls._check_initial_state()
67+
cls._check_final_states()
8868
cls._check_disconnected_state()
8969

70+
def _check_initial_state(cls):
71+
initials = [s for s in cls.states if s.initial]
72+
if len(initials) != 1:
73+
raise InvalidDefinition(
74+
_(
75+
"There should be one and only one initial state. "
76+
"Your currently have these: {!r}"
77+
).format([s.id for s in initials])
78+
)
79+
80+
def _check_final_states(cls):
9081
final_state_with_invalid_transitions = [
9182
state for state in cls.final_states if state.transitions
9283
]
@@ -98,6 +89,21 @@ def _check(cls):
9889
).format([s.id for s in final_state_with_invalid_transitions])
9990
)
10091

92+
def _disconnected_states(cls, starting_state):
93+
visitable_states = set(visit_connected_states(starting_state))
94+
return set(cls.states) - visitable_states
95+
96+
def _check_disconnected_state(cls):
97+
disconnected_states = cls._disconnected_states(cls.initial_state)
98+
if disconnected_states:
99+
raise InvalidDefinition(
100+
_(
101+
"There are unreachable states. "
102+
"The statemachine graph should have a single component. "
103+
"Disconnected states: {}"
104+
).format([s.id for s in disconnected_states])
105+
)
106+
101107
def add_inherited(cls, bases):
102108
for base in bases:
103109
for state in getattr(base, "states", []):

statemachine/signature.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any:
2525
ba = self.bind_expected(*args, **kwargs)
2626
return self.method(*ba.args, **ba.kwargs)
2727

28+
@classmethod
29+
def from_callable(cls, method):
30+
if hasattr(method, "__signature__"):
31+
sig = method.__signature__
32+
return SignatureAdapter(
33+
sig.parameters.values(),
34+
return_annotation=sig.return_annotation,
35+
)
36+
return super().from_callable(method)
37+
2838
def bind_expected(self, *args: Any, **kwargs: Any) -> BoundArguments: # noqa: C901
2939
"""Get a BoundArguments object, that maps the passed `args`
3040
and `kwargs` to the function's signature. It avoids to raise `TypeError`

tests/test_mock_compatibility.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from statemachine import State
2+
from statemachine import StateMachine
3+
4+
5+
def test_minimal(mocker):
6+
class Observer:
7+
def on_enter_state(self, event, model, source, target, state):
8+
...
9+
10+
obs = Observer()
11+
on_enter_state = mocker.spy(obs, "on_enter_state")
12+
13+
class Machine(StateMachine):
14+
a = State("Init", initial=True)
15+
b = State("Fin")
16+
17+
cycle = a.to(b) | b.to(a)
18+
19+
state = Machine().add_observer(obs)
20+
assert state.a.is_active
21+
22+
state.cycle()
23+
24+
assert state.b.is_active
25+
on_enter_state.assert_called_once()

0 commit comments

Comments
 (0)