pytransitions / transitions

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

Graphs do not show conditions that are references #520

Closed ghost closed 3 years ago

ghost commented 3 years ago
from transitions.extensions import LockedGraphMachine as Machine

class Task(Machine):
    def __init__(self):
        transitions = [
            {
                "trigger": "next",
                "source": "Stage 1",
                "dest": "Stage 2",
                "conditions": ["can_go_forwards"],
            },
            {
                "trigger": "next",
                "source": "Stage 2",
                "dest": "Stage 3",
                "conditions": [self.can_go_forwards],
            },
            {
                "trigger": "next",
                "source": "Stage 3",
                "dest": "Stage 2",
                "conditions": [self.can_go_backwards],
            },
            {
                "trigger": "next",
                "source": "Stage 2",
                "dest": "Stage 1",
                "conditions": [self.can_go_backwards],
            },
        ]

        Machine.__init__(
            self,
            show_conditions=True,
            show_state_attributes=True,
            states=["Stage 1", "Stage 2", "Stage 3"],
            initial="Stage 1",
            transitions=transitions,
        )

    def can_go_backwards(self):
        return False

    def can_go_forwards(self):
        return True

if __name__ == "__main__":

    m = Task()
    m.next()
    m.next()
    m.next()
    m.next()
    m.next()
    m.next()

    print(m.state)

    m.get_graph().draw("diagram.png", prog="dot", args="-Gdpi=300")

The machine behaves as expected, but the diagram does not show all the conditions.

diagram

aleneum commented 3 years ago

Hello @StephenCarboni,

this feature is included but not (yet) accessible. After you initialize your Task, do this:

# m = Task()
m.skip_references = False  # next time markup is updated, include reference labels
m.add_transition(trigger='reset', source="*", dest="Stage 1")  # force a markup update

The output looks like this:

diagram

As you see, printing the reference function name clutters the output significantly. Imo, just printing the function's name removes too much vital information (that it is a reference and not a callback present in the model). Do you have any suggestions for a good compromise between readability and "informativeness"?

ghost commented 3 years ago

If the callable is passed directly and it exists in the model, then it is practically the same as if I had provided a string. So if we assert that the callable is in the model, then just printing the name should be okay?

If the callable is outside the model, then printing its qualified name (other_module.task.can_go_backwards) should be enough, but I'm not sure.

Idea: a formatting hook so that users can decide the final string.

aleneum commented 3 years ago

I refactored MarkupMachine a bit and introduced MarkupMachine.format_references with a default formatting. This works quite well but has to be overridden for classes derived from LockedMachine. The reason why they are so cluttered in your example is the fact that the partial is wrapped into _locked_method. This is taken care of in factory and should now return the proper callback. The example below will be added to the ReadMe:

from transitions.extensions import GraphMachine as Machine
from functools import partial

class Model:

    def clear_state(self, deep=False, force=False):
        print("Clearing state ...")
        return True

model = Model()
machine = Machine(model=model, states=['A', 'B', 'C'],
                  transitions=[
                      {'trigger': 'clear', 'source': 'B', 'dest': 'A', 'conditions': model.clear_state},
                      {'trigger': 'clear', 'source': 'C', 'dest': 'A',
                       'conditions': partial(model.clear_state, False, force=True)},
                  ],
                  initial='A', show_conditions=True)

model.get_graph().draw('my_state_diagram.png', prog='dot')

resulting in:

my_state_diagram

Your example above looks like this:

diagram

MarkupMachine.format_references or (Locked)GraphMachine.format_references can be overridden if this is not sufficient.