From b7464b42f925dc40958179668273cbbd4521160b Mon Sep 17 00:00:00 2001 From: sammydowds Date: Thu, 8 Oct 2020 10:29:10 -0500 Subject: [PATCH 1/9] Adding meta class for transitions --- finite_state_machine/state_machine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index ede63e6..e21ace0 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -20,6 +20,8 @@ class Transition(NamedTuple): conditions: list on_error: Union[bool, int, str] +class TransitionMeta(object): + pass def transition(source, target, conditions=None, on_error=None): allowed_types = [str, bool, int] From 84654d542c28921ef0ca4bcd710e936ee0031a5c Mon Sep 17 00:00:00 2001 From: sammydowds Date: Thu, 8 Oct 2020 10:42:59 -0500 Subject: [PATCH 2/9] adding methods to transition meta class --- finite_state_machine/state_machine.py | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index e21ace0..cb533d0 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -14,14 +14,38 @@ def __init__(self): class Transition(NamedTuple): - name: str source: Union[list, bool, int, str] target: Union[bool, int, str] conditions: list on_error: Union[bool, int, str] class TransitionMeta(object): - pass + def __init__(self, name): + self.name = name + self.transitions = {} + + def get_transition(self, source): + transition = self.transitions.get(source, None) + if transition is None: + transition = self.transitions.get('*', None) + if transition is None: + transition = self.transitions.get('+', None) + return transition + + def add_transition(self, source, target, on_error=None, conditions=[]): + if source in self.transitions: + raise AssertionError('Duplicate transition for {0} state'.format(source)) + self.transitions[source] = Transition( + source=source, + target=target, + on_error=on_error, + conditions=conditions) + + def next_state(self, current_state): + transition = self.get_transition(current_state) + if transition is None: + raise TransitionNotAllowed('No transition from {0}'.format(current_state)) + return transition.target def transition(source, target, conditions=None, on_error=None): allowed_types = [str, bool, int] From e44968ddc96984b747d5865fa84105e01b52c972 Mon Sep 17 00:00:00 2001 From: sammydowds Date: Thu, 8 Oct 2020 10:48:51 -0500 Subject: [PATCH 3/9] Adding creation of TransitionMeta in transition function, and updated reference to transition objects in the wrapper function --- finite_state_machine/exceptions.py | 6 ++++++ finite_state_machine/state_machine.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/finite_state_machine/exceptions.py b/finite_state_machine/exceptions.py index f922edb..0e687eb 100644 --- a/finite_state_machine/exceptions.py +++ b/finite_state_machine/exceptions.py @@ -11,3 +11,9 @@ def __init__(self, conditions): conditions_not_met = ", ".join(condition.__name__ for condition in conditions) message = f"Following conditions did not return True: {conditions_not_met}" super().__init__(message) + +class TransitionNotAllowed(Exception): + def __init__(self, *args, **kwargs): + self.object = kwargs.pop('object', None) + self.method = kwargs.pop('method', None) + super(TransitionNotAllowed, self).__init__(*args, **kwargs) \ No newline at end of file diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index cb533d0..0bb4ac5 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -71,7 +71,12 @@ def transition(source, target, conditions=None, on_error=None): 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) + func.__fsm = TransitionMeta(func.__name__) + if isinstance(source, (list, tuple, set)): + for item in source: + func._fsm.add_transition(item, target, on_error, conditions) + else: + func._fsm.add_transition(source, target, on_error, conditions) @functools.wraps(func) def _wrapper(*args, **kwargs): @@ -80,15 +85,15 @@ def _wrapper(*args, **kwargs): except ValueError: self = args[0] - if self.state not in source: + if self.state not in func._fsm.transitions: exception_message = ( f"Current state is {self.state}. " - f"{func.__name__} allows transitions from {source}." + f"{func._fsm.name} allows transitions from {func._fsm.transitions}." ) raise InvalidStartState(exception_message) conditions_not_met = [] - for condition in conditions: + for condition in func._fsm.transitions[self.state].conditions: if not condition(*args, **kwargs): conditions_not_met.append(condition) if conditions_not_met: @@ -96,7 +101,7 @@ def _wrapper(*args, **kwargs): if not on_error: result = func(*args, **kwargs) - self.state = target + self.state = func._fsm.transitions[self.state].target return result try: From 496284c4187081b4df5644304d3e504ea2e59de7 Mon Sep 17 00:00:00 2001 From: sammydowds Date: Thu, 8 Oct 2020 11:05:28 -0500 Subject: [PATCH 4/9] Importing new exception, plus correct formatting errors --- finite_state_machine/state_machine.py | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 0bb4ac5..13b4527 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -2,7 +2,7 @@ import types from typing import NamedTuple, Union -from .exceptions import ConditionsNotMet, InvalidStartState +from .exceptions import ConditionsNotMet, InvalidStartState, TransitionNotAllowed class StateMachine: @@ -19,34 +19,34 @@ class Transition(NamedTuple): conditions: list on_error: Union[bool, int, str] + class TransitionMeta(object): - def __init__(self, name): + def __init__(self, name): self.name = name self.transitions = {} - + def get_transition(self, source): transition = self.transitions.get(source, None) if transition is None: - transition = self.transitions.get('*', None) + transition = self.transitions.get("*", None) if transition is None: - transition = self.transitions.get('+', None) + transition = self.transitions.get("+", None) return transition - + def add_transition(self, source, target, on_error=None, conditions=[]): if source in self.transitions: - raise AssertionError('Duplicate transition for {0} state'.format(source)) + raise AssertionError("Duplicate transition for {0} state".format(source)) self.transitions[source] = Transition( - source=source, - target=target, - on_error=on_error, - conditions=conditions) + source=source, target=target, on_error=on_error, conditions=conditions + ) def next_state(self, current_state): transition = self.get_transition(current_state) if transition is None: - raise TransitionNotAllowed('No transition from {0}'.format(current_state)) + raise TransitionNotAllowed("No transition from {0}".format(current_state)) return transition.target + def transition(source, target, conditions=None, on_error=None): allowed_types = [str, bool, int] @@ -74,9 +74,9 @@ def transition_decorator(func): func.__fsm = TransitionMeta(func.__name__) if isinstance(source, (list, tuple, set)): for item in source: - func._fsm.add_transition(item, target, on_error, conditions) + func.__fsm.add_transition(item, target, on_error, conditions) else: - func._fsm.add_transition(source, target, on_error, conditions) + func.__fsm.add_transition(source, target, on_error, conditions) @functools.wraps(func) def _wrapper(*args, **kwargs): @@ -85,7 +85,7 @@ def _wrapper(*args, **kwargs): except ValueError: self = args[0] - if self.state not in func._fsm.transitions: + if self.state not in func.__fsm.transitions: exception_message = ( f"Current state is {self.state}. " f"{func._fsm.name} allows transitions from {func._fsm.transitions}." @@ -93,7 +93,7 @@ def _wrapper(*args, **kwargs): raise InvalidStartState(exception_message) conditions_not_met = [] - for condition in func._fsm.transitions[self.state].conditions: + for condition in func.__fsm.transitions[self.state].conditions: if not condition(*args, **kwargs): conditions_not_met.append(condition) if conditions_not_met: @@ -101,7 +101,7 @@ def _wrapper(*args, **kwargs): if not on_error: result = func(*args, **kwargs) - self.state = func._fsm.transitions[self.state].target + self.state = func.__fsm.transitions[self.state].target return result try: From 7efea97cc256311fa25559cc3d30dff60d2a6a2f Mon Sep 17 00:00:00 2001 From: sammydowds Date: Thu, 8 Oct 2020 11:15:52 -0500 Subject: [PATCH 5/9] Fixing formatting --- finite_state_machine/state_machine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 13b4527..f41fb3a 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -88,7 +88,7 @@ def _wrapper(*args, **kwargs): if self.state not in func.__fsm.transitions: exception_message = ( f"Current state is {self.state}. " - f"{func._fsm.name} allows transitions from {func._fsm.transitions}." + f"{func.__fsm.name} allows transitions from {func.__fsm.transitions}." ) raise InvalidStartState(exception_message) From b5ea22b82b8a397a41073733528aa58abf262777 Mon Sep 17 00:00:00 2001 From: sammydowds Date: Thu, 8 Oct 2020 12:08:13 -0500 Subject: [PATCH 6/9] Updating logic in the generating state markdown to create list of transitions from TransitionMeta --- finite_state_machine/draw_state_diagram.py | 11 +++++++---- finite_state_machine/state_machine.py | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/finite_state_machine/draw_state_diagram.py b/finite_state_machine/draw_state_diagram.py index 0d12c19..ae122a6 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 TransitionMeta def import_state_machine_class(path): # pragma: no cover @@ -28,9 +28,12 @@ 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") - ] + + state_transitions = [] + for func in class_fns: + if hasattr(func[1], "__fsm"): + for val in list(func[1].__fsm.transitions.values()): + state_transitions.append(val) transition_template = " {source} --> {target} : {name}\n" diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index f41fb3a..90cdbfa 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -14,6 +14,7 @@ def __init__(self): class Transition(NamedTuple): + name: str source: Union[list, bool, int, str] target: Union[bool, int, str] conditions: list @@ -37,7 +38,7 @@ def add_transition(self, source, target, on_error=None, conditions=[]): if source in self.transitions: raise AssertionError("Duplicate transition for {0} state".format(source)) self.transitions[source] = Transition( - source=source, target=target, on_error=on_error, conditions=conditions + name=self.name, source=source, target=target, on_error=on_error, conditions=conditions ) def next_state(self, current_state): From c6a63dd59484118994648c76af4624ffd8539e3e Mon Sep 17 00:00:00 2001 From: sammydowds Date: Wed, 14 Oct 2020 08:45:52 -0500 Subject: [PATCH 7/9] Added in inspect module to pull instance of StateMachine, and added creation of dictionary with source->target --- finite_state_machine/draw_state_diagram.py | 12 ++-- finite_state_machine/exceptions.py | 6 -- finite_state_machine/state_machine.py | 74 +++++++++++----------- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/finite_state_machine/draw_state_diagram.py b/finite_state_machine/draw_state_diagram.py index ae122a6..5d7b17f 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 TransitionMeta +from finite_state_machine.state_machine import Transition def import_state_machine_class(path): # pragma: no cover @@ -28,12 +28,10 @@ def generate_state_diagram_markdown(cls, initial_state): """ class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) - - state_transitions = [] - for func in class_fns: - if hasattr(func[1], "__fsm"): - for val in list(func[1].__fsm.transitions.values()): - state_transitions.append(val) + class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) + state_transitions: List[Transition] = [ + func.__fsm for name, func in class_fns if hasattr(func, "__fsm") + ] transition_template = " {source} --> {target} : {name}\n" diff --git a/finite_state_machine/exceptions.py b/finite_state_machine/exceptions.py index 0e687eb..f922edb 100644 --- a/finite_state_machine/exceptions.py +++ b/finite_state_machine/exceptions.py @@ -11,9 +11,3 @@ def __init__(self, conditions): conditions_not_met = ", ".join(condition.__name__ for condition in conditions) message = f"Following conditions did not return True: {conditions_not_met}" super().__init__(message) - -class TransitionNotAllowed(Exception): - def __init__(self, *args, **kwargs): - self.object = kwargs.pop('object', None) - self.method = kwargs.pop('method', None) - super(TransitionNotAllowed, self).__init__(*args, **kwargs) \ No newline at end of file diff --git a/finite_state_machine/state_machine.py b/finite_state_machine/state_machine.py index 90cdbfa..c07f6b4 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -1,12 +1,14 @@ import functools import types from typing import NamedTuple, Union +import inspect -from .exceptions import ConditionsNotMet, InvalidStartState, TransitionNotAllowed +from .exceptions import ConditionsNotMet, InvalidStartState class StateMachine: def __init__(self): + self.avail_transitions = {} try: self.state except AttributeError: @@ -21,33 +23,6 @@ class Transition(NamedTuple): on_error: Union[bool, int, str] -class TransitionMeta(object): - def __init__(self, name): - self.name = name - self.transitions = {} - - def get_transition(self, source): - transition = self.transitions.get(source, None) - if transition is None: - transition = self.transitions.get("*", None) - if transition is None: - transition = self.transitions.get("+", None) - return transition - - def add_transition(self, source, target, on_error=None, conditions=[]): - if source in self.transitions: - raise AssertionError("Duplicate transition for {0} state".format(source)) - self.transitions[source] = Transition( - name=self.name, source=source, target=target, on_error=on_error, conditions=conditions - ) - - def next_state(self, current_state): - transition = self.get_transition(current_state) - if transition is None: - raise TransitionNotAllowed("No transition from {0}".format(current_state)) - return transition.target - - def transition(source, target, conditions=None, on_error=None): allowed_types = [str, bool, int] @@ -72,12 +47,37 @@ def transition(source, target, conditions=None, on_error=None): raise ValueError("on_error needs to be a bool, int or string") def transition_decorator(func): - func.__fsm = TransitionMeta(func.__name__) - if isinstance(source, (list, tuple, set)): - for item in source: - func.__fsm.add_transition(item, target, on_error, conditions) + mems = inspect.getmembers(func) + state_machine_instance = [ + mem[1]["StateMachine"] for mem in mems if mem[0] == "__globals__" + ][0] + func.__fsm = Transition( + name=func.__name__, + source=source, + target=target, + conditions=conditions, + on_error=on_error, + ) + # need to optimize + if hasattr(state_machine_instance, "__fsm"): + if isinstance(source, list): + for src in source: + if src in state_machine_instance.__fsm: + state_machine_instance.__fsm[src].append(target) + else: + state_machine_instance.__fsm[src] = [target] + else: + if source in state_machine_instance.__fsm: + state_machine_instance.__fsm[src].append(target) + else: + state_machine_instance.__fsm[src] = [target] else: - func.__fsm.add_transition(source, target, on_error, conditions) + if isinstance(source, list): + state_machine_instance.__fsm = {} + for src in source: + state_machine_instance.__fsm[src] = [target] + else: + state_machine_instance.__fsm.source = [target] @functools.wraps(func) def _wrapper(*args, **kwargs): @@ -86,15 +86,15 @@ def _wrapper(*args, **kwargs): except ValueError: self = args[0] - if self.state not in func.__fsm.transitions: + if self.state not in source: exception_message = ( f"Current state is {self.state}. " - f"{func.__fsm.name} allows transitions from {func.__fsm.transitions}." + f"{func.__name__} allows transitions from {source}." ) raise InvalidStartState(exception_message) conditions_not_met = [] - for condition in func.__fsm.transitions[self.state].conditions: + for condition in conditions: if not condition(*args, **kwargs): conditions_not_met.append(condition) if conditions_not_met: @@ -102,7 +102,7 @@ def _wrapper(*args, **kwargs): if not on_error: result = func(*args, **kwargs) - self.state = func.__fsm.transitions[self.state].target + self.state = target return result try: From 16f897f2df7833dd8b8fe9e9b157f9c3033f1169 Mon Sep 17 00:00:00 2001 From: sammydowds Date: Wed, 14 Oct 2020 09:24:24 -0500 Subject: [PATCH 8/9] Removing init attr dict, adding comment on lines 61-80. --- 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 c07f6b4..2bd2704 100644 --- a/finite_state_machine/state_machine.py +++ b/finite_state_machine/state_machine.py @@ -8,7 +8,6 @@ class StateMachine: def __init__(self): - self.avail_transitions = {} try: self.state except AttributeError: @@ -58,7 +57,8 @@ def transition_decorator(func): conditions=conditions, on_error=on_error, ) - # need to optimize + + # creating and/or adding items to __fsm attribute if hasattr(state_machine_instance, "__fsm"): if isinstance(source, list): for src in source: From 732e4560215f4bf4c8ba0147502c5cc4d222b08b Mon Sep 17 00:00:00 2001 From: sammydowds Date: Wed, 14 Oct 2020 09:26:05 -0500 Subject: [PATCH 9/9] Removing accidental duplication --- finite_state_machine/draw_state_diagram.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/finite_state_machine/draw_state_diagram.py b/finite_state_machine/draw_state_diagram.py index 5d7b17f..2f02ae1 100644 --- a/finite_state_machine/draw_state_diagram.py +++ b/finite_state_machine/draw_state_diagram.py @@ -26,8 +26,6 @@ def generate_state_diagram_markdown(cls, initial_state): https://mermaid-js.github.io/mermaid/diagrams-and-syntax-and-examples/stateDiagram.html """ - - class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) state_transitions: List[Transition] = [ func.__fsm for name, func in class_fns if hasattr(func, "__fsm")