-
Notifications
You must be signed in to change notification settings - Fork 12
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
b7464b4
84654d5
e44968d
496284c
7efea97
b5ea22b
c6a63dd
16f897f
732e456
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing this exception all together. |
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: | ||
|
@@ -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 | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could be replced by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I tried this it would not work because Although, let me play around with it again. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Will be easier to find the next optimization There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
@@ -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: | ||
|
There was a problem hiding this comment.
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.