Skip to content

Commit d4f5d80

Browse files
committed
Merge branch 'release/2.3.1'
2 parents d011271 + c1a6a63 commit d4f5d80

File tree

8 files changed

+143
-8
lines changed

8 files changed

+143
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machin
1717
</div>
1818

1919
Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
20-
great developer experience. We provide an _pythonic_ and expressive API for implementing state
20+
great developer experience. We provide a _pythonic_ and expressive API for implementing state
2121
machines in sync or asynchonous Python codebases.
2222

2323
## Features

docs/authors.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [Guilherme Nepomuceno](mailto:piercio@loggi.com)
1010
* [Rafael Rêgo](mailto:crafards@gmail.com)
1111
* [Raphael Schrader](mailto:raphael@schradercloud.de)
12+
* [João S. O. Bueno](mailto:gwidion@gmail.com)
1213

1314

1415
## Scaffolding

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: 2
1717
18+
2.3.1
1819
2.3.0
1920
2.2.0
2021
2.1.2

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-statemachine"
3-
version = "2.3.0"
3+
version = "2.3.1"
44
description = "Python Finite State Machines made easy."
55
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
66
maintainers = [

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__ = "fgmacedo@gmail.com"
6-
__version__ = "2.3.0"
6+
__version__ = "2.3.1"
77

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

statemachine/utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import asyncio
2+
import threading
3+
4+
_cached_loop = threading.local()
5+
"""Loop that will be used when the SM is running in a synchronous context. One loop per thread."""
26

37

48
def qualname(cls):
@@ -23,11 +27,13 @@ def ensure_iterable(obj):
2327

2428
def run_async_from_sync(coroutine):
2529
"""
26-
Run an async coroutine from a synchronous context.
30+
Compatibility layer to run an async coroutine from a synchronous context.
2731
"""
32+
global _cached_loop
2833
try:
29-
loop = asyncio.get_running_loop()
34+
asyncio.get_running_loop()
3035
return asyncio.ensure_future(coroutine)
3136
except RuntimeError:
32-
loop = asyncio.get_event_loop()
33-
return loop.run_until_complete(coroutine)
37+
if not hasattr(_cached_loop, "loop"):
38+
_cached_loop.loop = asyncio.new_event_loop()
39+
return _cached_loop.loop.run_until_complete(coroutine)

tests/test_deepcopy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_deepcopy_with_observers(caplog):
7171

7272
assert sm1.model is not sm2.model
7373

74-
caplog.set_level(logging.DEBUG)
74+
caplog.set_level(logging.DEBUG, logger="tests")
7575

7676
def assertions(sm, _reference):
7777
caplog.clear()

tests/test_threading.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import threading
2+
import time
3+
4+
from statemachine.state import State
5+
from statemachine.statemachine import StateMachine
6+
7+
8+
def test_machine_should_allow_multi_thread_event_changes():
9+
"""
10+
Test for https://github.com/fgmacedo/python-statemachine/issues/443
11+
"""
12+
13+
class CampaignMachine(StateMachine):
14+
"A workflow machine"
15+
16+
draft = State(initial=True)
17+
producing = State()
18+
closed = State()
19+
add_job = draft.to(producing) | producing.to(closed)
20+
21+
machine = CampaignMachine()
22+
23+
def off_thread_change_state():
24+
time.sleep(0.01)
25+
machine.add_job()
26+
27+
thread = threading.Thread(target=off_thread_change_state)
28+
thread.start()
29+
thread.join()
30+
assert machine.current_state.id == "producing"
31+
32+
33+
def test_regression_443():
34+
"""
35+
Test for https://github.com/fgmacedo/python-statemachine/issues/443
36+
"""
37+
time_collecting = 0.2
38+
time_to_send = 0.125
39+
time_sampling_current_state = 0.05
40+
41+
class TrafficLightMachine(StateMachine):
42+
"A traffic light machine"
43+
44+
green = State(initial=True)
45+
yellow = State()
46+
red = State()
47+
48+
cycle = green.to(yellow) | yellow.to(red) | red.to(green)
49+
50+
class Controller:
51+
def __init__(self):
52+
self.statuses_history = []
53+
self.fsm = TrafficLightMachine()
54+
# set up thread
55+
t = threading.Thread(target=self.recv_cmds)
56+
t.start()
57+
58+
def recv_cmds(self):
59+
"""Pretend we receive a command triggering a state change after Xs."""
60+
waiting_time = 0
61+
sent = False
62+
while waiting_time < time_collecting:
63+
if waiting_time >= time_to_send and not sent:
64+
self.fsm.cycle()
65+
sent = True
66+
67+
waiting_time += time_sampling_current_state
68+
self.statuses_history.append(self.fsm.current_state.id)
69+
time.sleep(time_sampling_current_state)
70+
71+
c1 = Controller()
72+
c2 = Controller()
73+
time.sleep(time_collecting + 0.01)
74+
assert c1.statuses_history == ["green", "green", "green", "yellow"]
75+
assert c2.statuses_history == ["green", "green", "green", "yellow"]
76+
77+
78+
def test_regression_443_with_modifications():
79+
"""
80+
Test for https://github.com/fgmacedo/python-statemachine/issues/443
81+
"""
82+
time_collecting = 0.2
83+
time_to_send = 0.125
84+
time_sampling_current_state = 0.05
85+
86+
class TrafficLightMachine(StateMachine):
87+
"A traffic light machine"
88+
89+
green = State(initial=True)
90+
yellow = State()
91+
red = State()
92+
93+
cycle = green.to(yellow) | yellow.to(red) | red.to(green)
94+
95+
def __init__(self, name):
96+
self.name = name
97+
self.statuses_history = []
98+
super().__init__()
99+
100+
def beat(self):
101+
waiting_time = 0
102+
sent = False
103+
while waiting_time < time_collecting:
104+
if waiting_time >= time_to_send and not sent:
105+
self.cycle()
106+
sent = True
107+
108+
self.statuses_history.append(f"{self.name}.{self.current_state.id}")
109+
110+
time.sleep(time_sampling_current_state)
111+
waiting_time += time_sampling_current_state
112+
113+
class Controller:
114+
def __init__(self, name):
115+
self.fsm = TrafficLightMachine(name)
116+
# set up thread
117+
t = threading.Thread(target=self.fsm.beat)
118+
t.start()
119+
120+
c1 = Controller("c1")
121+
c2 = Controller("c2")
122+
c3 = Controller("c3")
123+
time.sleep(time_collecting + 0.01)
124+
125+
assert c1.fsm.statuses_history == ["c1.green", "c1.green", "c1.green", "c1.yellow"]
126+
assert c2.fsm.statuses_history == ["c2.green", "c2.green", "c2.green", "c2.yellow"]
127+
assert c3.fsm.statuses_history == ["c3.green", "c3.green", "c3.green", "c3.yellow"]

0 commit comments

Comments
 (0)