-
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?
Conversation
… reference to transition objects in the wrapper function
…nsitions from TransitionMeta
Codecov Report
@@ Coverage Diff @@
## master #22 +/- ##
==========================================
- Coverage 96.10% 94.15% -1.96%
==========================================
Files 7 7
Lines 154 171 +17
==========================================
+ Hits 148 161 +13
- Misses 6 10 +4
Continue to review full report at Codecov.
|
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.
Think this should work, but it feels like you are bringing a lot of complexity from django-fsm into this project. While this library is inspired by django-fsm, we don't need to copy all of their design decisions... unless the complexity makes sense for the project
The way I would think about this is by looking at tests. This PR adds 50 lines of code, 30 of which are untested. How much work would it be to write tests for these changes? I think it's going to be a sizeable amount.
I would take a step back and think... what information do you need to get the list of available transactions? As each @transition
function is read in, we can dynamically create a source-to-target
lookup table. That table only changes when additional functions are wrapped with @transition
decorators are added.
…reation of dictionary with source->target
b522af6
to
c6a63dd
Compare
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.
- Removed meta class for transitions
- Added back in the original logic
- Added logic in the
transition_decorator
function to pull the StateMachine instance - Added logic to build out a python dict to an "__fsm" attribute of the StateMachine instance with structure {source: [targets]}
I did not realize commits automatically appeared in the PR - sorry for the mess.
finite_state_machine/exceptions.py
Outdated
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this exception all together.
mems = inspect.getmembers(func) | ||
state_machine_instance = [ | ||
mem[1]["StateMachine"] for mem in mems if mem[0] == "__globals__" | ||
][0] |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
this could be replced by state_machine_instance = func.__self__
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.
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.
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.
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.
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 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:
- Any advice on how to optimize this?
- I like collections.defaultdict as the data type. Should I build that out?
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.
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
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.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Removing an overly complex meta class.
class_fns = inspect.getmembers(cls, predicate=inspect.isfunction) | ||
state_transitions: List[Transition] = [ | ||
func.__fsm for name, func in class_fns if hasattr(func, "__fsm") | ||
] |
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.
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.
This looks good, think you might need to add the available_actions
method on the base StateMachine
class.
Can you also write a couple of tests to make sure this returns what you expect?
1 New Class:
2 New StateMachine methods:
At first, I thought this would be way simpler. Let me know if this is not the solution you were looking for!
It looks like pytest still passes, however this is only my second PR - so I would love some feedback!