Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List available transactions given the state of the state machine #22

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
12 changes: 5 additions & 7 deletions finite_state_machine/draw_state_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding back in the original logic to the file.


transition_template = " {source} --> {target} : {name}\n"

Expand Down
6 changes: 0 additions & 6 deletions finite_state_machine/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this exception all together.

74 changes: 37 additions & 37 deletions finite_state_machine/state_machine.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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


Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing an overly complex meta class.

def transition(source, target, conditions=None, on_error=None):
allowed_types = [str, bool, int]

Expand All @@ -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]
Comment on lines +49 to +52
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saving instance of StateMachine to later add "__fsm" attribute. It turned out func was type "function" not "method".

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be replced by state_machine_instance = func.__self__

Copy link
Author

@sammydowds sammydowds Oct 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tried this it would not work because func is of type function not a method. So I had to dive into the globals referenced for the function type here: https://docs.python.org/3/library/inspect.html. Perhaps I am missing something.

Although, let me play around with it again.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh.... doing a quick google search, doesn't seem like it's possible without inspect. 🤦

I think a possible workaround might be to use a class as a function decorator. Don't worry about it. This works, let's just go with it. If this library has some magic, that's okay. It makes the code which uses this library cleaner which is really what we want.

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]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to be optimized. Checks if the StateMachine has an attribute "__fsm", and appends a target to an existing source or creates an entry in a dict of structure {source: [target]}.

Two questions:

  1. Any advice on how to optimize this?
  2. I like collections.defaultdict as the data type. Should I build that out?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collections.defaultdict would definitely reduce one level of nesting. I would try that out and see what you are left with

Will be easier to find the next optimization

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since our types at the moment are booleans, ints, and strings. We can probably start with the default type of a set. Not sure how that would work for enums.

I still need to investigate that.


@functools.wraps(func)
def _wrapper(*args, **kwargs):
Expand All @@ -86,23 +86,23 @@ 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:
raise ConditionsNotMet(conditions_not_met)

if not on_error:
result = func(*args, **kwargs)
self.state = func.__fsm.transitions[self.state].target
self.state = target
return result

try:
Expand Down