From 6e01447c20d6b4a68ff44515aff0dfcff5928a39 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 2 Oct 2021 15:20:11 -0500 Subject: [PATCH 01/13] add test args for makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2be0397..454483d 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ install-localdev: ## install library for local development; pip install -e . flit install --symlink test: ## run tests - pytest + pytest $(args) test-cov: ## run tests with coverage pytest --cov finite_state_machine/ --cov examples/ From b5fc95851f7d71ac96ef2450fb3b241a8a6030c1 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 2 Oct 2021 15:20:32 -0500 Subject: [PATCH 02/13] add pytest asyncio as dev requirement --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index 482c17e..e4a0c63 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,6 +3,7 @@ # testing pytest==6.2.5 pytest-cov==2.10.1 +pytest-asyncio==0.15.1 # package management flit==3.0.0 From 9b502daedff982d32bb2cc18afe1e13073a2f117 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 2 Oct 2021 15:21:03 -0500 Subject: [PATCH 03/13] add code and tests --- examples/async_turnstile.py | 17 +++ finite_state_machine/state_machine.py | 120 +++++++++++++------- tests/examples/test_async_state_machinne.py | 40 +++++++ 3 files changed, 139 insertions(+), 38 deletions(-) create mode 100644 examples/async_turnstile.py create mode 100644 tests/examples/test_async_state_machinne.py diff --git a/examples/async_turnstile.py b/examples/async_turnstile.py new file mode 100644 index 0000000..f206140 --- /dev/null +++ b/examples/async_turnstile.py @@ -0,0 +1,17 @@ +from finite_state_machine import StateMachine, transition + + +class Turnstile(StateMachine): + initial_state = "close" + + def __init__(self): + self.state = self.initial_state + super().__init__() + + @transition(source=["close", "open"], target="open") + async def insert_coin(self): + return "inserted coin" + + @transition(source="open", target="close") + async def pass_thru(self): + return "passed thru" diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 399b59d..9d1cad9 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -1,3 +1,4 @@ +import asyncio from enum import Enum import functools import types @@ -51,43 +52,86 @@ def transition(source, target, conditions=None, on_error=None): def transition_decorator(func): func.__fsm = Transition(func.__name__, source, target, conditions, on_error) - @functools.wraps(func) - def _wrapper(*args, **kwargs): - try: - self, rest = args - except ValueError: - self = args[0] - - if self.state not in source: - exception_message = ( - f"Current state is {self.state}. " - f"{func.__name__} allows transitions from {source}." - ) - raise InvalidStartState(exception_message) - - conditions_not_met = [] - for condition in conditions: - if not condition(*args, **kwargs): - conditions_not_met.append(condition) - if conditions_not_met: - raise ConditionsNotMet(conditions_not_met) - - if not on_error: - result = func(*args, **kwargs) - self.state = target - return result - - try: - result = func(*args, **kwargs) - self.state = target - return result - except Exception: - # TODO should we log this somewhere? - # logger.error? maybe have an optional parameter to set this up - # how to libraries log? - self.state = on_error - return - - return _wrapper + synchronous_execution = not asyncio.iscoroutinefunction(func) + if synchronous_execution: + + @functools.wraps(func) + def _wrapper(*args, **kwargs): + try: + self, rest = args + except ValueError: + self = args[0] + + if self.state not in source: + exception_message = ( + f"Current state is {self.state}. " + f"{func.__name__} allows transitions from {source}." + ) + raise InvalidStartState(exception_message) + + conditions_not_met = [] + for condition in conditions: + if not condition(*args, **kwargs): + conditions_not_met.append(condition) + if conditions_not_met: + raise ConditionsNotMet(conditions_not_met) + + if not on_error: + result = func(*args, **kwargs) + self.state = target + return result + + try: + result = func(*args, **kwargs) + self.state = target + return result + except Exception: + # TODO should we log this somewhere? + # logger.error? maybe have an optional parameter to set this up + # how to libraries log? + self.state = on_error + return + + return _wrapper + else: + + @functools.wraps(func) + async def _wrapper(*args, **kwargs): + try: + self, rest = args + except ValueError: + self = args[0] + + if self.state not in source: + exception_message = ( + f"Current state is {self.state}. " + f"{func.__name__} allows transitions from {source}." + ) + raise InvalidStartState(exception_message) + + conditions_not_met = [] + for condition in conditions: + if not condition(*args, **kwargs): + conditions_not_met.append(condition) + if conditions_not_met: + raise ConditionsNotMet(conditions_not_met) + + if not on_error: + result = await func(*args, **kwargs) + self.state = target + return result + + try: + result = await func(*args, **kwargs) + self.state = target + return result + except Exception: + # TODO should we log this somewhere? + # logger.error? maybe have an optional parameter to set this up + # how to libraries log? + self.state = on_error + return + + return _wrapper return transition_decorator diff --git a/tests/examples/test_async_state_machinne.py b/tests/examples/test_async_state_machinne.py new file mode 100644 index 0000000..09b8eb8 --- /dev/null +++ b/tests/examples/test_async_state_machinne.py @@ -0,0 +1,40 @@ +import warnings + +import pytest + +from finite_state_machine.exceptions import InvalidStartState +from examples.async_turnstile import Turnstile + + +@pytest.mark.asyncio +async def test_async_turnstile_does_not_raise_coroutine_not_awaited_warnings(): + with warnings.catch_warnings(record=True) as caught_warnings: + t = Turnstile() + assert t.state == "close" + + result = await t.insert_coin() + assert t.state == "open" + assert result == "inserted coin" + + result = await t.insert_coin() + assert t.state == "open" + assert result == "inserted coin" + + result = await t.pass_thru() + assert t.state == "close" + assert result == "passed thru" + + assert len(caught_warnings) == 0 + + +@pytest.mark.asyncio +async def test_async_turnstile__cannot_pass_thru_closed_turnstile(): + t = Turnstile() + assert t.state == "close" + + with pytest.raises( + InvalidStartState, match="Current state is close" + ), warnings.catch_warnings(record=True) as caught_warnings: + await t.pass_thru() + + assert len(caught_warnings) == 0 From 1074487014fe928a7216528682750f550ff82fdf Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 2 Oct 2021 15:37:36 -0500 Subject: [PATCH 04/13] async transition function goes to error state --- examples/async_turnstile.py | 4 ++++ tests/examples/test_async_state_machinne.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/examples/async_turnstile.py b/examples/async_turnstile.py index f206140..4801a0d 100644 --- a/examples/async_turnstile.py +++ b/examples/async_turnstile.py @@ -15,3 +15,7 @@ async def insert_coin(self): @transition(source="open", target="close") async def pass_thru(self): return "passed thru" + + @transition(source="close", target="close", on_error="error_state") + async def error_function(self): + raise ValueError diff --git a/tests/examples/test_async_state_machinne.py b/tests/examples/test_async_state_machinne.py index 09b8eb8..1b0b4e8 100644 --- a/tests/examples/test_async_state_machinne.py +++ b/tests/examples/test_async_state_machinne.py @@ -38,3 +38,15 @@ async def test_async_turnstile__cannot_pass_thru_closed_turnstile(): await t.pass_thru() assert len(caught_warnings) == 0 + + +@pytest.mark.asyncio +async def test_async_turnstile__goes_into_error_state(): + t = Turnstile() + assert t.state == "close" + + with warnings.catch_warnings(record=True) as caught_warnings: + await t.error_function() + + assert t.state == "error_state" + assert len(caught_warnings) == 0 From a9ae3f9cc44e179182dc2e1e3965350ef213d14d Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 2 Oct 2021 15:40:58 -0500 Subject: [PATCH 05/13] improve code cov --- examples/async_turnstile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/async_turnstile.py b/examples/async_turnstile.py index 4801a0d..1bf45f9 100644 --- a/examples/async_turnstile.py +++ b/examples/async_turnstile.py @@ -8,7 +8,7 @@ def __init__(self): self.state = self.initial_state super().__init__() - @transition(source=["close", "open"], target="open") + @transition(source=["close", "open"], target="open", on_error="error_state") async def insert_coin(self): return "inserted coin" From 8ff48d37de7c1595d28d525d760bfa723a982c44 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 9 Oct 2021 15:07:52 -0500 Subject: [PATCH 06/13] Allow for asynchronous condition functions (#33) --- .pre-commit-config.yaml | 2 +- README.md | 14 ++++ examples/async_github_pull_request.py | 46 +++++++++++ finite_state_machine/state_machine.py | 8 +- .../test_async_github_pull_request.py | 78 +++++++++++++++++++ 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 examples/async_github_pull_request.py create mode 100644 tests/examples/test_async_github_pull_request.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23ea442..1fb63f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/ambv/black - rev: stable + rev: 21.9b0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 diff --git a/README.md b/README.md index 6308f23..ac61355 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Lightweight, decorator-based Python implementation of a [Finite State Machine](h - [Installation](#installation) - [Usage](#usage) - [Example](#example) +- [Async Support](#async-support) - [State Diagram](#state-diagram) - [Contributing](#contributing) - [Inspiration](#inspiration) @@ -134,6 +135,19 @@ InvalidStartState: The [examples](/examples) folder contains additional workflows. +## Async Support + +This library supports asynchronous state machines +through the use of async `@transition` decorator and async condition functions. + +See the following matrix on how synchronous and asynchronous functions +can be combined to build state machines: + +||Sync `@transition` decorator|Async `@transition` decorator| +|---|:---:|:---:| +|Sync condition function|✅|❌| +|Async condition function|✅|✅| + ## State Diagram State Machine workflows can be visualized using a diff --git a/examples/async_github_pull_request.py b/examples/async_github_pull_request.py new file mode 100644 index 0000000..411040b --- /dev/null +++ b/examples/async_github_pull_request.py @@ -0,0 +1,46 @@ +from finite_state_machine import StateMachine, transition + + +async def async_is_approved_or_is_admin(machine, user): + return machine.num_approvals >= 1 or user.is_admin + + +def sync_is_approved_or_is_admin(machine, user): + return machine.num_approvals >= 1 or user.is_admin + + +class GitHubPullRequest(StateMachine): + """State Machine to represent a GitHub Pull Request workflow""" + + def __init__(self): + self.state = "opened" + self.num_approvals = 0 + self.changes_request = False + + @transition(source="opened", target="opened") + async def approve(self): + self.num_approvals += 1 + + @transition(source="opened", target="opened") + async def request_changes(self): + self.changes_request = True + + @transition(source="opened", target="closed") + async def close_pull_request(self): + pass + + @transition( + source="opened", + target="merged", + conditions=[async_is_approved_or_is_admin], + ) + async def fully_async_merge_pull_request(self, user): + pass + + @transition( + source="opened", + target="merged", + conditions=[sync_is_approved_or_is_admin], + ) + async def async_merge_pull_request_with_sync_condition(self, user): + pass diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 9d1cad9..c5f9548 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -71,7 +71,7 @@ def _wrapper(*args, **kwargs): conditions_not_met = [] for condition in conditions: - if not condition(*args, **kwargs): + if condition(*args, **kwargs) is not True: conditions_not_met.append(condition) if conditions_not_met: raise ConditionsNotMet(conditions_not_met) @@ -111,7 +111,11 @@ async def _wrapper(*args, **kwargs): conditions_not_met = [] for condition in conditions: - if not condition(*args, **kwargs): + if asyncio.iscoroutinefunction(condition): + condition_result = await condition(*args, **kwargs) + else: + condition_result = condition(*args, **kwargs) + if condition_result is not True: conditions_not_met.append(condition) if conditions_not_met: raise ConditionsNotMet(conditions_not_met) diff --git a/tests/examples/test_async_github_pull_request.py b/tests/examples/test_async_github_pull_request.py new file mode 100644 index 0000000..0a6cee0 --- /dev/null +++ b/tests/examples/test_async_github_pull_request.py @@ -0,0 +1,78 @@ +from collections import namedtuple + +import pytest + +from examples.async_github_pull_request import GitHubPullRequest + +User = namedtuple("User", "name github_id is_admin") + + +@pytest.mark.asyncio +async def test_approved_pr_can_be_merged__with_sync_condition_function(): + # Arrange + github_pr = GitHubPullRequest() + await github_pr.approve() + assert github_pr.state == "opened" + user = User(name="Aly Sivji", github_id="alysivji", is_admin=False) + + # Act + await github_pr.async_merge_pull_request_with_sync_condition(user) + + # Assert + assert github_pr.state == "merged" + + +@pytest.mark.asyncio +async def test_approved_pr_can_be_merged__fully_async(): + # Arrange + github_pr = GitHubPullRequest() + await github_pr.approve() + assert github_pr.state == "opened" + user = User(name="Aly Sivji", github_id="alysivji", is_admin=False) + + # Act + await github_pr.fully_async_merge_pull_request(user) + + # Assert + assert github_pr.state == "merged" + + +@pytest.mark.asyncio +async def test_pr_with_no_approvals_can_be_merged_by_admin(): + # Arrange + github_pr = GitHubPullRequest() + assert github_pr.state == "opened" + user = User(name="Aly Sivji", github_id="alysivji", is_admin=True) + + # Act + await github_pr.fully_async_merge_pull_request(user) + + # Assert + assert github_pr.state == "merged" + + +@pytest.mark.asyncio +async def test_request_approval(): + # Arrange + github_pr = GitHubPullRequest() + assert github_pr.state == "opened" + + # Act + await github_pr.request_changes() + + # Assert + assert github_pr.state == "opened" + assert github_pr.changes_request is True + + +@pytest.mark.asyncio +async def test_close_pull_request(): + # Arrange + github_pr = GitHubPullRequest() + assert github_pr.state == "opened" + + # Act + await github_pr.close_pull_request() + + # Assert + assert github_pr.state == "closed" From 50ce62dd21c76cb2921e99c24b5c9e46e9322fa2 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 9 Oct 2021 15:16:49 -0500 Subject: [PATCH 07/13] improve codecov --- tests/examples/test_async_github_pull_request.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/examples/test_async_github_pull_request.py b/tests/examples/test_async_github_pull_request.py index 0a6cee0..a0d4070 100644 --- a/tests/examples/test_async_github_pull_request.py +++ b/tests/examples/test_async_github_pull_request.py @@ -2,6 +2,7 @@ import pytest +from finite_state_machine.exceptions import ConditionsNotMet from examples.async_github_pull_request import GitHubPullRequest User = namedtuple("User", "name github_id is_admin") @@ -37,6 +38,18 @@ async def test_approved_pr_can_be_merged__fully_async(): assert github_pr.state == "merged" +@pytest.mark.asyncio +async def test_pr_with_no_approvals_cannot_be_merged(): + # Arrange + github_pr = GitHubPullRequest() + assert github_pr.state == "opened" + user = User(name="Aly Sivji", github_id="alysivji", is_admin=False) + + # Act + with pytest.raises(ConditionsNotMet): + await github_pr.fully_async_merge_pull_request(user) + + @pytest.mark.asyncio async def test_pr_with_no_approvals_can_be_merged_by_admin(): # Arrange From 3e9fb7cda7e7145e4986bf8cfad22c2e8160a6f9 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 9 Oct 2021 16:53:55 -0500 Subject: [PATCH 08/13] make decorator a callable class --- finite_state_machine/state_machine.py | 238 ++++++++++++++------------ 1 file changed, 124 insertions(+), 114 deletions(-) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index c5f9548..a855726 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -23,119 +23,129 @@ class Transition(NamedTuple): on_error: Union[bool, int, str] -def transition(source, target, conditions=None, on_error=None): - allowed_types = (str, bool, int, Enum) - - if isinstance(source, allowed_types): - source = [source] - if not isinstance(source, list): - raise ValueError("Source can be a bool, int, string, Enum, or list") - for item in source: - if not isinstance(item, allowed_types): - raise ValueError("Source can be a bool, int, string, Enum, or list") +class transition: + _fsm_transition_mapping = {} - if not isinstance(target, allowed_types): - raise ValueError("Target needs to be a bool, int or string") - - if not conditions: - conditions = [] - if not isinstance(conditions, list): - raise ValueError("conditions must be a list") - for condition in conditions: - if not isinstance(condition, types.FunctionType): - raise ValueError("conditions list must contain functions") - - if on_error: - if not isinstance(on_error, allowed_types): - raise ValueError("on_error needs to be a bool, int or string") - - def transition_decorator(func): - func.__fsm = Transition(func.__name__, source, target, conditions, on_error) - - synchronous_execution = not asyncio.iscoroutinefunction(func) - if synchronous_execution: - - @functools.wraps(func) - def _wrapper(*args, **kwargs): - try: - self, rest = args - except ValueError: - self = args[0] - - if self.state not in source: - exception_message = ( - f"Current state is {self.state}. " - f"{func.__name__} allows transitions from {source}." - ) - raise InvalidStartState(exception_message) - - conditions_not_met = [] - for condition in conditions: - if condition(*args, **kwargs) is not True: - conditions_not_met.append(condition) - if conditions_not_met: - raise ConditionsNotMet(conditions_not_met) - - if not on_error: - result = func(*args, **kwargs) - self.state = target - return result - - try: - result = func(*args, **kwargs) - self.state = target - return result - except Exception: - # TODO should we log this somewhere? - # logger.error? maybe have an optional parameter to set this up - # how to libraries log? - self.state = on_error - return - - return _wrapper - else: + def __init__(self, source, target, conditions=None, on_error=None): + allowed_types = (str, bool, int, Enum) - @functools.wraps(func) - async def _wrapper(*args, **kwargs): - try: - self, rest = args - except ValueError: - self = args[0] - - if self.state not in source: - exception_message = ( - f"Current state is {self.state}. " - f"{func.__name__} allows transitions from {source}." - ) - raise InvalidStartState(exception_message) - - conditions_not_met = [] - for condition in conditions: - if asyncio.iscoroutinefunction(condition): - condition_result = await condition(*args, **kwargs) - else: - condition_result = condition(*args, **kwargs) - if condition_result is not True: - conditions_not_met.append(condition) - if conditions_not_met: - raise ConditionsNotMet(conditions_not_met) - - if not on_error: - result = await func(*args, **kwargs) - self.state = target - return result - - try: - result = await func(*args, **kwargs) - self.state = target - return result - except Exception: - # TODO should we log this somewhere? - # logger.error? maybe have an optional parameter to set this up - # how to libraries log? - self.state = on_error - return - - return _wrapper - - return transition_decorator + if isinstance(source, allowed_types): + source = [source] + if not isinstance(source, list): + raise ValueError("Source can be a bool, int, string, Enum, or list") + for item in source: + if not isinstance(item, allowed_types): + raise ValueError("Source can be a bool, int, string, Enum, or list") + self.source = source + + if not isinstance(target, allowed_types): + raise ValueError("Target needs to be a bool, int or string") + self.target = target + + if not conditions: + conditions = [] + if not isinstance(conditions, list): + raise ValueError("conditions must be a list") + for condition in conditions: + if not isinstance(condition, types.FunctionType): + raise ValueError("conditions list must contain functions") + self.conditions = conditions + + if on_error: + if not isinstance(on_error, allowed_types): + raise ValueError("on_error needs to be a bool, int or string") + self.on_error = on_error + + def __call__(self, func): + func.__fsm = Transition( + func.__name__, + self.source, + self.target, + self.conditions, + self.on_error, + ) + # TODO update on class transition mapping + self.__class__._fsm_transition_mapping[func.__qualname__] = func + + @functools.wraps(func) + def callable(*args, **kwargs): + try: + state_machine, rest = args + except ValueError: + state_machine = args[0] + + if state_machine.state not in self.source: + exception_message = ( + f"Current state is {state_machine.state}. " + f"{func.__name__} allows transitions from {self.source}." + ) + raise InvalidStartState(exception_message) + + conditions_not_met = [] + for condition in self.conditions: + if condition(*args, **kwargs) is not True: + conditions_not_met.append(condition) + if conditions_not_met: + raise ConditionsNotMet(conditions_not_met) + + if not self.on_error: + result = func(*args, **kwargs) + state_machine.state = self.target + return result + + try: + result = func(*args, **kwargs) + state_machine.state = self.target + return result + except Exception: + # TODO should we log this somewhere? + # logger.error? maybe have an optional parameter to set this up + # how to libraries log? + state_machine.state = self.on_error + return + + @functools.wraps(func) + async def async_callable(*args, **kwargs): + try: + state_machine, rest = args + except ValueError: + state_machine = args[0] + + if state_machine.state not in self.source: + exception_message = ( + f"Current state is {state_machine.state}. " + f"{func.__name__} allows transitions from {self.source}." + ) + raise InvalidStartState(exception_message) + + conditions_not_met = [] + for condition in self.conditions: + if asyncio.iscoroutinefunction(condition): + condition_result = await condition(*args, **kwargs) + else: + condition_result = condition(*args, **kwargs) + if condition_result is not True: + conditions_not_met.append(condition) + if conditions_not_met: + raise ConditionsNotMet(conditions_not_met) + + if not self.on_error: + result = await func(*args, **kwargs) + state_machine.state = self.target + return result + + try: + result = await func(*args, **kwargs) + state_machine.state = self.target + return result + except Exception: + # TODO should we log this somewhere? + # logger.error? maybe have an optional parameter to set this up + # how to libraries log? + state_machine.state = self.on_error + return + + if asyncio.iscoroutinefunction(func): + return async_callable + else: + return callable From 13ccb6db2e9b00728c9bdbbf15ed1fa095329404 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 9 Oct 2021 16:55:04 -0500 Subject: [PATCH 09/13] get draw fsm tests to pass --- finite_state_machine/draw_state_diagram.py | 2 +- finite_state_machine/state_machine.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/finite_state_machine/draw_state_diagram.py b/finite_state_machine/draw_state_diagram.py index 0d12c19..176fecf 100644 --- a/finite_state_machine/draw_state_diagram.py +++ b/finite_state_machine/draw_state_diagram.py @@ -29,7 +29,7 @@ def generate_state_diagram_markdown(cls, initial_state): class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) state_transitions: List[Transition] = [ - func.__fsm for name, func in class_fns if hasattr(func, "__fsm") + func._fsm for name, func in class_fns if hasattr(func, "_fsm") ] transition_template = " {source} --> {target} : {name}\n" diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index a855726..236f8dc 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -57,7 +57,7 @@ def __init__(self, source, target, conditions=None, on_error=None): self.on_error = on_error def __call__(self, func): - func.__fsm = Transition( + func._fsm = Transition( func.__name__, self.source, self.target, From 19b31aba9d02d345b0192b5858a4eae96a3a2216 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 9 Oct 2021 16:56:08 -0500 Subject: [PATCH 10/13] improve name --- finite_state_machine/state_machine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 236f8dc..5479e3f 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -68,7 +68,7 @@ def __call__(self, func): self.__class__._fsm_transition_mapping[func.__qualname__] = func @functools.wraps(func) - def callable(*args, **kwargs): + def sync_callable(*args, **kwargs): try: state_machine, rest = args except ValueError: @@ -148,4 +148,4 @@ async def async_callable(*args, **kwargs): if asyncio.iscoroutinefunction(func): return async_callable else: - return callable + return sync_callable From 90e0e8cdb6551fa11b4b4588615b80c41e00a8d1 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 9 Oct 2021 17:11:36 -0500 Subject: [PATCH 11/13] add comment --- finite_state_machine/state_machine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 5479e3f..8ff670c 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -24,6 +24,7 @@ class Transition(NamedTuple): class transition: + # TODO remove from part 1 PR _fsm_transition_mapping = {} def __init__(self, source, target, conditions=None, on_error=None): From 29b65cddf4007a0c480774b1b9e2896c45a54958 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sun, 10 Oct 2021 16:04:50 -0500 Subject: [PATCH 12/13] remove todo --- finite_state_machine/state_machine.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 8ff670c..0f24a1c 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -24,9 +24,6 @@ class Transition(NamedTuple): class transition: - # TODO remove from part 1 PR - _fsm_transition_mapping = {} - def __init__(self, source, target, conditions=None, on_error=None): allowed_types = (str, bool, int, Enum) @@ -65,8 +62,6 @@ def __call__(self, func): self.conditions, self.on_error, ) - # TODO update on class transition mapping - self.__class__._fsm_transition_mapping[func.__qualname__] = func @functools.wraps(func) def sync_callable(*args, **kwargs): From ef3cba9b0a579ed698a3ae4f27b6f758e2e30c9e Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sun, 10 Oct 2021 16:05:57 -0500 Subject: [PATCH 13/13] improve name of dto --- finite_state_machine/draw_state_diagram.py | 4 ++-- finite_state_machine/state_machine.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/finite_state_machine/draw_state_diagram.py b/finite_state_machine/draw_state_diagram.py index 176fecf..0018a52 100644 --- a/finite_state_machine/draw_state_diagram.py +++ b/finite_state_machine/draw_state_diagram.py @@ -5,7 +5,7 @@ import sys from typing import List -from finite_state_machine.state_machine import Transition +from finite_state_machine.state_machine import TransitionDetails def import_state_machine_class(path): # pragma: no cover @@ -28,7 +28,7 @@ def generate_state_diagram_markdown(cls, initial_state): """ class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) - state_transitions: List[Transition] = [ + state_transitions: List[TransitionDetails] = [ func._fsm for name, func in class_fns if hasattr(func, "_fsm") ] diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 0f24a1c..ca5b9f6 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -15,7 +15,7 @@ def __init__(self): raise ValueError("Need to set a state instance variable") -class Transition(NamedTuple): +class TransitionDetails(NamedTuple): name: str source: Union[list, bool, int, str] target: Union[bool, int, str] @@ -55,7 +55,7 @@ def __init__(self, source, target, conditions=None, on_error=None): self.on_error = on_error def __call__(self, func): - func._fsm = Transition( + func._fsm = TransitionDetails( func.__name__, self.source, self.target,