pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.67k stars 529 forks source link

Call triggers by name #649

Closed juan11iguel closed 5 months ago

juan11iguel commented 5 months ago

Is your feature request related to a problem? Please describe. Right now the only way to trigger a transition is by calling the method generated when adding a transition.

Describe the solution you'd like It would be nice if there was a convenience method to call triggers by its name (a string).

Additional context Here is some pseudo-code:

# From README example

# The states
states=['solid', 'liquid', 'gas', 'plasma']

# And some transitions between states. We're lazy, so we'll leave out
# the inverse phase transitions (freezing, condensation, etc.).
transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
]

# Initialize
machine = Machine(lump, states=states, transitions=transitions, initial='liquid')

# Only way (as far as I know) to trigger a transition
machine.melt()

# Workaround
trigger_id = "melt" # This could be set programatically based on external code
transition = getattr(machine,  trigger_id)
transition()  # Trigger transition

# Proposed alternative
# Add a convenience method to avoid having to call getattr every time
machine.trigger_transition_by_name(trigger_id) # I am sure a better method name could be used

PS.: Thank you @aleneum for the great work! The project is amazing, with very readable code and nice sense of humor

aleneum commented 5 months ago

Hi @juan11iguel,

It would be nice if there was a convenience method to call triggers by its name (a string).

good news: you can do that via the decorated trigger method. In the README Non-quickstart right before the next section:

Furthermore, there is a method called trigger now attached to your model (if it hasn't been there before). This method lets you execute transitions by name in case dynamic triggering is required.

Or in the section about Callback Execution Order:

In summary, there are currently three ways to trigger events. You can call a model's convenience functions like lump.melt(), execute triggers by name such as lump.trigger("melt") or dispatch events on multiple models with machine.dispatch("melt")

Long story short: That's how it's done:

from transitions import Machine

states = ['solid', 'liquid', 'gas', 'plasma']

transitions = [
    {'trigger': 'melt', 'source': 'solid', 'dest': 'liquid'},
    {'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas'},
    {'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas'},
    {'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma'}
]

machine = Machine(states=states, transitions=transitions, initial='liquid')
machine.trigger("evaporate")
assert machine.is_gas()

Note that I did not pass lump as an argument. The first argument passed to Machine acts as a model. So when I pass something there, all the convenience functions will be added to the object. If no model is provided machine itself acts as a model.

Let me know if this covers your use case and best regards.

juan11iguel commented 5 months ago

Ha! It seems I cannot read, thanks for the detailed response, this solves it then.

Best regards