pytransitions / transitions

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

add label to Graph node and edge #452

Closed badiku closed 4 years ago

badiku commented 4 years ago

hi, I like GraphMachine, and I'd like to add some label to nodes and edges.

I see there're some basic label codes in diagrams.py for supporting label, but when building a machine with label, got this error:

from transitions.extensions import GraphMachine
machine = GraphMachine(states=dict(name='test', label='test'))

site-packages\transitions\core.py in _create_state(cls, *args, kwargs)   616 @classmethod   617 def _create_state(cls, *args, *kwargs): --> 618 return cls.state_cls(args, kwargs)   619   620 @property

TypeError: init() got an unexpected keyword argument 'label'

so, the problem is : diagrams.py support label, but by default in core.py State and Transition class do not support label argument.

old codes for getting label: in diagrams.py:

class BaseGraph(object)
    def _convert_state_attributes(self, state):
            label = state.get('label', state['name'])

    def _transition_label(self, tran):
      edge_label = tran.get('label', tran['trigger'])

does author forget these codes?

to support label, I tried to add label argument in core.py

class State(object):
    def __init__(..., label=None):
        ...
        self.label = label

class Transition(object):
    def __init__(..., label=None):
        ...
        self.label = label

and in markup.py, add label to attributes list:

class MarkupMachine(Machine):
    state_attributes = ['on_exit', 'on_enter', 'ignore_invalid_triggers', 'timeout', 'on_timeout', 'tags', 'label'] 
    transition_attributes = ['source', 'dest', 'prepare', 'before', 'after', 'label']

seemed ok. but there'd be some better method.

related issue: #442

badiku commented 4 years ago

I think this'd be added to GraphMachine, but first I tried to add label using subClass:

from transitions import State
from transitions.extensions.diagrams import  TransitionGraphSupport
from transitions.extensions import GraphMachine

class StateLabelSupport(State):
    """ add label support
    """

    def __init__(self, *args, **kwargs):
        label = kwargs.pop('label', None)
        super().__init__(*args, **kwargs)
        if label: self.label = label

    def __repr__(self):
        return "<%s('%s')@%s> %s" % (type(self).__name__,
            self.name, id(self),
            self.label if hasattr(self, 'label') else "")

class TransitionLabelSupport(TransitionGraphSupport):
    """ add label support
    """

    def __init__(self, *args, **kwargs):
        label = kwargs.pop('label', None)
        super().__init__(*args, **kwargs)
        if label: self.label = label

    def __repr__(self):
        return "<%s('%s', '%s')@%s> %s" % (type(self).__name__,
            self.source, self.dest, id(self),
            self.label if hasattr(self, 'label') else "")

class LabelGraphMachine(GraphMachine):
    '''add label support'''
    state_attributes = GraphMachine.state_attributes + ['label']
    transition_attributes = GraphMachine.transition_attributes + ['label']
    state_cls = StateLabelSupport
    transition_cls = TransitionLabelSupport

# build machine
machine = LabelGraphMachine(states=dict(name='test', label='test'))

print(machine.get_graph())

strict digraph { graph [label="State Machine", rankdir=LR ]; node [color=black, fillcolor=white, label="\N", peripheries=1, shape=rectangle, style="rounded, filled" ]; edge [color=black]; test [label=test]; initial [color=red, fillcolor=darksalmon, label=initial, peripheries=2]; }

badiku commented 4 years ago

to support updating label, I changed function _change_state and _get_graph:

from transitions import State
from transitions.extensions.diagrams import  TransitionGraphSupport
from transitions.extensions import GraphMachine

class StateLabelSupport(State):
    """ add label support
    """

    def __init__(self, *args, **kwargs):
        label = kwargs.pop('label', None)
        super().__init__(*args, **kwargs)
        if label: self.label = label

    def __repr__(self):
        return "<%s('%s')@%s> %s" % (type(self).__name__,
            self.name, id(self),
            self.label if hasattr(self, 'label') else "")

class TransitionLabelSupport(TransitionGraphSupport):
    """ add label support
    """

    def __init__(self, *args, **kwargs):
        label = kwargs.pop('label', None)
        super().__init__(*args, **kwargs)
        if label: self.label = label

    def _change_state(self, event_data):
        super()._change_state(event_data)
        event_data.model.previous = self.source

    def __repr__(self):
        return "<%s('%s', '%s')@%s> %s" % (type(self).__name__,
            self.source, self.dest, id(self),
            self.label if hasattr(self, 'label') else "")

class LabelGraphMachine(GraphMachine):
    '''add label support
    '''
    state_attributes = GraphMachine.state_attributes + ['label']
    transition_attributes = GraphMachine.transition_attributes + ['label']
    state_cls = StateLabelSupport
    transition_cls = TransitionLabelSupport

    def _get_graph(self, model, title=None, force_new=False, show_roi=False):
        '''force update label, and reserve previous status
        '''
        if force_new:
            self._needs_update = True
        grph = super()._get_graph(model, title, force_new, show_roi)
        if force_new and hasattr(self.model, 'previous'):
              self.model_graphs[self.model].set_previous_transition(self.model.previous, self.model.state)
        return grph

# build machine
machine = LabelGraphMachine(states=dict(name='test', label='test'))

# change label
machine.get_state('test').label = 'NewLabel'
print(machine.get_graph())

# use force_new=True to update label
print(machine.get_graph(force_new=True))

strict digraph { graph [label="State Machine", rankdir=LR ]; node [color=black, fillcolor=white, label="\N", peripheries=1, shape=rectangle, style="rounded, filled" ]; edge [color=black]; test [label=test]; initial [color=red, fillcolor=darksalmon, label=initial, peripheries=2]; }

strict digraph { graph [label="State Machine", rankdir=LR ]; node [color=black, fillcolor=white, label="\N", peripheries=1, shape=rectangle, style="rounded, filled" ]; edge [color=black]; test [label=NewLabel]; initial [color=red, fillcolor=darksalmon, label=initial, peripheries=2]; }

aleneum commented 4 years ago

Hi @badiku,

I see there're some basic label codes in diagrams.py for supporting label, but when building a machine with label, got this error:

the line you discovered in diagrams.py is intended to support custom state definitions. State never supported a label attribute as far as I remember. So subclassing is the suggested way to use a graph label which differs from the state's name. As far as I can tell, this is also the road you have taken.

However, it sucks that you have to do this:

state_attributes = GraphMachine.state_attributes + ['label']

having label in diagrams.py but not in markup.py sends indeed mixed messages. I will add 'label' to state_attributes and transition_attributes in MarkupMachine. This won't do any harm to states/transitions not using labels. Since every transition already has to be derived from TransitionGraphSupport, I will add the label attribute to it as well. This way, only State needs to be explicitely subclassed when labels are required.

When I get your last post correctly, using labels instead of named will mess with the graph styling. This looks buggy to me. I will check if this can be handled internally as well.

aleneum commented 4 years ago

With the recent push, this should be sufficient:

from transitions import State
from transitions.extensions import GraphMachine

class StateLabelSupport(State):

    def __init__(self, *args, **kwargs):
        label = kwargs.pop('label', None)
        super().__init__(*args, **kwargs)
        if label: self.label = label

    def __repr__(self):
        return "<%s('%s')@%s> %s" % (type(self).__name__,
            self.name, id(self),
            self.label if hasattr(self, 'label') else "")

class LabelGraphMachine(GraphMachine):
    state_cls = StateLabelSupport

    def set_label(self, state_name, label):
        self.get_state(state_name).label = label
        for model in self.models:
            model.get_graph().get_node(state_name).attr['label'] = label

machine = LabelGraphMachine(states=[dict(name='A', label='LabelA'), dict(name='B', label='LabelB')], initial='A')
machine.to_B()
machine.set_label('A', 'NewLabel')
machine.to_A()
machine.get_graph().draw('diagram.png', prog='dot')

Result

diagram

Seems like labels are handled alright. I added a set_label method to your LabelGraphMachine. However, this will only work well with the pygraphviz backend. If you are looking for a more versatile solution, your attempt looks more generic but may require more graph generations.

aleneum commented 4 years ago

Since there is no new feedback I will close this issue. Feel free to comment anyway. I will reopen it if necessary.