Skip to content

Commit

Permalink
Merge branch 'release/2.3.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
fgmacedo committed Jun 10, 2024
2 parents d011271 + c1a6a63 commit d4f5d80
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machin
</div>

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

## Features
Expand Down
1 change: 1 addition & 0 deletions docs/authors.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [Guilherme Nepomuceno](mailto:piercio@loggi.com)
* [Rafael Rêgo](mailto:crafards@gmail.com)
* [Raphael Schrader](mailto:raphael@schradercloud.de)
* [João S. O. Bueno](mailto:gwidion@gmail.com)


## Scaffolding
Expand Down
1 change: 1 addition & 0 deletions docs/releases/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Below are release notes through StateMachine and its patch releases.
```{toctree}
:maxdepth: 2
2.3.1
2.3.0
2.2.0
2.1.2
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-statemachine"
version = "2.3.0"
version = "2.3.1"
description = "Python Finite State Machines made easy."
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
maintainers = [
Expand Down
2 changes: 1 addition & 1 deletion statemachine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

__author__ = """Fernando Macedo"""
__email__ = "fgmacedo@gmail.com"
__version__ = "2.3.0"
__version__ = "2.3.1"

__all__ = ["StateMachine", "State"]
14 changes: 10 additions & 4 deletions statemachine/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import asyncio
import threading

_cached_loop = threading.local()
"""Loop that will be used when the SM is running in a synchronous context. One loop per thread."""


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

def run_async_from_sync(coroutine):
"""
Run an async coroutine from a synchronous context.
Compatibility layer to run an async coroutine from a synchronous context.
"""
global _cached_loop
try:
loop = asyncio.get_running_loop()
asyncio.get_running_loop()
return asyncio.ensure_future(coroutine)
except RuntimeError:
loop = asyncio.get_event_loop()
return loop.run_until_complete(coroutine)
if not hasattr(_cached_loop, "loop"):
_cached_loop.loop = asyncio.new_event_loop()
return _cached_loop.loop.run_until_complete(coroutine)
2 changes: 1 addition & 1 deletion tests/test_deepcopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_deepcopy_with_observers(caplog):

assert sm1.model is not sm2.model

caplog.set_level(logging.DEBUG)
caplog.set_level(logging.DEBUG, logger="tests")

def assertions(sm, _reference):
caplog.clear()
Expand Down
127 changes: 127 additions & 0 deletions tests/test_threading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import threading
import time

from statemachine.state import State
from statemachine.statemachine import StateMachine


def test_machine_should_allow_multi_thread_event_changes():
"""
Test for https://github.com/fgmacedo/python-statemachine/issues/443
"""

class CampaignMachine(StateMachine):
"A workflow machine"

draft = State(initial=True)
producing = State()
closed = State()
add_job = draft.to(producing) | producing.to(closed)

machine = CampaignMachine()

def off_thread_change_state():
time.sleep(0.01)
machine.add_job()

thread = threading.Thread(target=off_thread_change_state)
thread.start()
thread.join()
assert machine.current_state.id == "producing"


def test_regression_443():
"""
Test for https://github.com/fgmacedo/python-statemachine/issues/443
"""
time_collecting = 0.2
time_to_send = 0.125
time_sampling_current_state = 0.05

class TrafficLightMachine(StateMachine):
"A traffic light machine"

green = State(initial=True)
yellow = State()
red = State()

cycle = green.to(yellow) | yellow.to(red) | red.to(green)

class Controller:
def __init__(self):
self.statuses_history = []
self.fsm = TrafficLightMachine()
# set up thread
t = threading.Thread(target=self.recv_cmds)
t.start()

def recv_cmds(self):
"""Pretend we receive a command triggering a state change after Xs."""
waiting_time = 0
sent = False
while waiting_time < time_collecting:
if waiting_time >= time_to_send and not sent:
self.fsm.cycle()
sent = True

waiting_time += time_sampling_current_state
self.statuses_history.append(self.fsm.current_state.id)
time.sleep(time_sampling_current_state)

c1 = Controller()
c2 = Controller()
time.sleep(time_collecting + 0.01)
assert c1.statuses_history == ["green", "green", "green", "yellow"]
assert c2.statuses_history == ["green", "green", "green", "yellow"]


def test_regression_443_with_modifications():
"""
Test for https://github.com/fgmacedo/python-statemachine/issues/443
"""
time_collecting = 0.2
time_to_send = 0.125
time_sampling_current_state = 0.05

class TrafficLightMachine(StateMachine):
"A traffic light machine"

green = State(initial=True)
yellow = State()
red = State()

cycle = green.to(yellow) | yellow.to(red) | red.to(green)

def __init__(self, name):
self.name = name
self.statuses_history = []
super().__init__()

def beat(self):
waiting_time = 0
sent = False
while waiting_time < time_collecting:
if waiting_time >= time_to_send and not sent:
self.cycle()
sent = True

self.statuses_history.append(f"{self.name}.{self.current_state.id}")

time.sleep(time_sampling_current_state)
waiting_time += time_sampling_current_state

class Controller:
def __init__(self, name):
self.fsm = TrafficLightMachine(name)
# set up thread
t = threading.Thread(target=self.fsm.beat)
t.start()

c1 = Controller("c1")
c2 = Controller("c2")
c3 = Controller("c3")
time.sleep(time_collecting + 0.01)

assert c1.fsm.statuses_history == ["c1.green", "c1.green", "c1.green", "c1.yellow"]
assert c2.fsm.statuses_history == ["c2.green", "c2.green", "c2.green", "c2.yellow"]
assert c3.fsm.statuses_history == ["c3.green", "c3.green", "c3.green", "c3.yellow"]

0 comments on commit d4f5d80

Please sign in to comment.