Open robinharms opened 1 month ago
Hi @robinharms , how are you? Thanks for your suggestion. I like the idea, sounds great!
We need to mature the "how" and the external API... I think that the initial suggestion does not match yet the data model... there's also an "Event" class, it's today only a thin wrapper used to trigger events, just a "syntatic sugar": calling sm.cycle()
is the same as calling sm.send("cycle")
.
Concepts that we neet to address for a more future proof API:
Event
class.StateMachine.events()
that return a list of Event
instances.To make this point 5 in context, this SM is similar to the one declared in your example, even if this method is not well documented:
class TrafficLightMachine(StateMachine):
"A traffic light machine"
green = State(name=_("Green"), initial=True)
yellow = State(name=_("Yellow"))
red = State(name=_("Red"))
green.to(yellow, event="cycle")
yellow.to(red, event="cycle")
red.to(green, event="cycle")
Example of how you can accomplish something similar today using the current version of the SM, accessing an internal API (not ideal).
Maybe we can explore more this path, to find and define a stable API:
# i18n_label.py
from gettext import gettext as _
from tests.examples.order_control_machine import OrderControl
OrderControl._events["add_to_order"].label = _("Add to order")
OrderControl._events["process_order"].label = _("Process order")
OrderControl._events["receive_payment"].label = _("Receive payment")
OrderControl._events["ship_order"].label = _("Ship order")
for e in OrderControl.events:
print(f"{e.name}: {e.label}")
Output:
(python-statemachine-py3.12) ❯ python i18n_label.py
add_to_order: Add to order
process_order: Process order
receive_payment: Receive payment
ship_order: Ship order
POT can be extracted using pybabel
or similar:
pybabel extract i18n_label.py -o i18n_label.pot
Contents:
# Translations template for PROJECT.
# ...
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.8.0\n"
#: i18n_label.py:5
msgid "Add to order"
msgstr ""
#: i18n_label.py:6
msgid "Process order"
msgstr ""
#: i18n_label.py:7
msgid "Receive payment"
msgstr ""
#: i18n_label.py:8
msgid "Ship order"
msgstr ""
That's great!
In that case I guess it's just about having a nicer documented way to add an attribute to the events? (Since the translation seem to belong there and not on transitions) And since events and allowed_events already exists they're already a public API.
One thing though, it might be a bit confusing with the name/value/id/label usage on SM vs Events. It took me a while to figure out that part of the code the first time I read through it. Maybe it would be a good idea to use id for event key and name as optional translation string / human readable?
That would end up being quite similar to how states are defined now:
red = State(name=_("Red"))
<etc...>
# option 1
cycle = (
| yellow.to(red, name=_("Cycle"))
| red.to(green, name=_("Let's go!"))
)
# option 2
green.to(yellow, event="cycle", name=_("Cycle"))
And hopefully only require a bit of internal renaming :)
What do you think?
All the best!
I agree with using name as metadata instead of the event identification.
I only didn't get how the name on transition will be used. As you demonstrated, we could possibly have distinct names, one for each transition, with multiple transitions related to the same event.
Can you elaborate a bit more on the transition's name usage?
If I understod the code correctly, when you create a transition via a SM as in the example above, the transition also creates an event right? So the name arg in the above example would basically transfer to the event. But since event already has an (internal?) attribute called name it might be good to use something else, or rename event.name to event.id
Hope that was a bit clearer :)
All the best!
red = State(name=_("Red"))
# option 1 cycle = ( | yellow.to(red, name=_("Cycle")) | red.to(green, name=_("Let's go!")) ) # option 2 green.to(yellow, event="cycle", name=_("Cycle"))
I still have a concern: Given that many transitions can point to the same event, don't seem like the correct place to insert the event name. The example above exposes this issue: Both name=_("Cycle")
and name=_("Let's go!")
pointing to the same event cycle
, if we read cycle.name
, that is the expected output?
Yeah that certainly throws a wrench in it. Ugh, and the other way around, there might be several events that point to the same transition. (Something like "customer_finish_order" vs."silently_process_internal_order" should probably end up in the same state)
So I think that points in the direction of avoiding writing to event instances while defining transitions. Might be a bit more verbose (or annoying), but this is something I expect quite few developers will use right?
Here are some ideas:
class TrafficLightMachine(StateMachine):
<...>
cycle = (
| yellow.to(red)
| red.to(green)
)
TrafficLightMachine.add_event_meta('cycle', name=_('Cycle'))
TrafficLightMachine.add_event_meta('404', name=_('404'))
(raise EventNotFound)
What's the expected behavior with subclasses here though?
emergency_stop = Event('emergency_stop', name=_('Emergency stop'))
class TrafficLightMachine(StateMachine):
<...>
# Default event here
cycle = yellow.to(red) | red.to(green)
# This must be set per row in this case
yellow.to(red, event=emergency_stop)
green.to(red, event=emergency_stop)
reusable_event = Event('reusable', name=_('Reusable'))
class TrafficLightMachine(StateMachine):
<...>
# Id set from attr, event for both transitions
cycle = (yellow.to(red) | red.to(green)) & Event(name='Cycle')
# Two events for a single transition (as list or combined with &...?)
green.to(red) & Event('emergency_override', name=_('Emergency')) & reusable_event)
# But these two would cause exceptions due to
bad = yellow.to(red) & Event('other') # Ambiguous, bad or other?
yellow.to(red) & Event('emergency_override') # A new instance of event with the same id as an event that already exists
Any thoughts on this? All the best!
I liked the idea of having an explicit Event
instance when customizing the name. Options 2 & 3 seem the best.
The operators option looks like syntactic sugar on top of the Event
instance, which we may implement but the main component is still the possibility to pass an Event
where we use a string now.
# But these two would cause exceptions due to
bad = yellow.to(red) & Event('other') # Ambiguous, bad or other?
yellow.to(red) & Event('emergency_override') # A new instance of event with the same id as an event that already exists
Instead of an exception, this should be possible, it's like adding two events to the same set of transitions. Currently, it's supported: bad = yellow.to(red, event='other')
works by adding both events. Even repeating the same event is possible and de-duplicated: cycle = (yellow.to(red, event='cycle') | red.to(green, event='cycle'))
.
Another option could be:
cycle = Event(green.to(yellow) | yellow.to(red) | red.to(green), name=_("Cycle"))
So we come up with implementing both 2 and 4.
cycle = Event(
green.to(yellow, event=Event("slowdown", name=_("Slowdown")) | yellow.to(red) | red.to(green),
name=_("Cycle")
)
Similar construction of passing events as parameters
cycle = Event(name=_("Cycle")) # customized name
slowdown = Event() # name derived from event identifier
green.to(yellow, event=[cycle, slowdown]) | yellow.to(red, event=cycle) | red.to(green, event=cycle)
What do you think?
Best!
Much better, yay idea 4! Easier to read + not enforced + easy to follow!
I'm not a fan of duplicate events with the same id though, it will make it harder to pass along the correct id. (And harder to understand when developing other things based on this) But if that's another issue and a bit off topic for this thread :)
All the best
As of know, it's possible to add a translation string as state name, and then use that translation string within an application. (For instance, have an API where all state machines can be read by the frontend-part of an application)
However, there's no straight forward way (that I know of) to add translation strings for events or transitions.
How about something like this?
All the best, Robin