pytransitions / transitions

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

Check may apply transition #549

Closed artofhuman closed 2 years ago

artofhuman commented 2 years ago

Add method to check may model to be transited to target state.

Currently, we have a method to check the current state ex: is_STOPPED() but don't have a method to check if a model may be reached new state ex: may_STOPPED()

Example api:

machine.add_transition(
    "stop", "started",  "stopped",
)

timer.start()
timer.is_started() # => true

# new method
timer.may_stopped() # => true

timer.stop()

timer.may_stopped() # => false

Additional context For example one of the popular ruby libs AASM has a similar API for check transitions

Example how we've implemented it in our code, but I think it should be in core lib to make client code more clear and useful

def may_to_state(self, state)
    self.state_machine.get_transitions(source=self.state.name, dest=state.name)
aleneum commented 2 years ago

Hello again, @artofhuman 👋 !

For example one of the popular ruby libs AASM has a similar API for check transitions

getting this functionality for transitions can be achieved by a) extending _add_model_to_trigger and optionally b) evaluating conditions to only return True when condition checks pass:

from transitions import Machine, EventData

class MayMachine(Machine):

    def _can_trigger(self, model, *args, **kwargs):
        e = EventData(None, None, self, model, args, kwargs)

        return [trigger_name for trigger_name in self.get_triggers(model.state)
                if any(all(c.check(e) for c in t.conditions)
                       for t in self.events[trigger_name].transitions[model.state])]

    def _add_trigger_to_model(self, trigger, model):
        super()._add_trigger_to_model(trigger, model)
        self._checked_assignment(model, f"may_{trigger}", lambda: trigger in self._can_trigger(model))

m = MayMachine(states=["A", "B", "C"],
               transitions=[["go", "A", "B"],
                            dict(trigger='do', source='A', dest='C', conditions=lambda: False),
                            ["do", "B", "C"]],
               initial="A")

assert m.state == 'A'
assert m.may_go()
assert not m.may_do()

But you request this for destination states, right? This is not supported by AASM, is it?

artofhuman commented 2 years ago

AASM is just an example lib API.

My point is it will be good to have this feature in core without overriding methods. I think overriding private methods is not good practice because they may be changed in future versions.

So these are only options for how it may look. More realistic example:

class States(Enum):
    PENDING = "pending"
    PRINTED = "printed"
    MARKED = "marked"

order = Order()

machine = Machine(
        states= States,
        initial=instance.state,
        auto_transitions=False,
        model=order,
    )

machine.add_transition(
        "print", States.PENDING, States.PRINTED,
    )
machine.add_transition(
        "mark", [States.PENDING, States.PRINTED], States.MARKED,
)

Option1.

order.may_print()

Option2

order.may_to_state(State.PRINTED)
aleneum commented 2 years ago

I think overriding private methods is not good practice because they may be changed in future versions.

fair enough

Option A is something that is occasionally requested. Would that be sufficient for your use case?

artofhuman commented 2 years ago

Yes 😄

aleneum commented 2 years ago

may has been introduced in 0.9.0 and was recently pushed to master