pytransitions / transitions

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

Multiple models with HierarchicalMachine : Parent does not resolve child conditions #456

Closed alexandretanem closed 3 years ago

alexandretanem commented 3 years ago

Hello,

When using the Reuse of previously created HSMs principle of HierarchicalMachine, I can't make the Parent state machine being aware of Children SM conditions.

In the example below there is no difference with or without the line machineParent.add_model(modelChild). Constraint: I don't want Parent to inherit from Child... in the sense of python inheritance.

Is this an issue from transitions or there is another way to achieve this ?


class Child():
    def __init__(self):
        self.true = True

    def ok(self):
        return self.true

class Parent():
    pass

modelChild = Child()
machineChild = HierarchicalGraphMachine(model=modelChild, states=['init', 'done'], initial='init', auto_transitions=False)
machineChild.add_transition('go', 'init', 'done', conditions=['ok'])

modelParent = Parent()
machineParent = HierarchicalGraphMachine(model=modelParent, states=['init', 'done', {'name':'child', 'children':machineChild}], initial='init', auto_transitions=False)
machineParent.add_model(modelChild)
machineParent.add_transition('go', 'init', 'child')
machineParent.add_transition('finish', 'child_done', 'done')

def printState():
    print("# ===")
    print("# PARENT : {} => {}".format(modelParent.state, machineParent.get_triggers(modelParent.state)))
    print("# CHILD : {} => {}".format(modelChild.state, machineChild.get_triggers(modelChild.state)))

printState()
# ===
# PARENT : init => ['go']
# CHILD : init => ['go']
modelParent.go()
printState()
# ===
# PARENT : child_init => ['go']
# CHILD : init => ['go']
modelParent.go()

# Raise an exception
> Traceback (most recent call last):
>  File "/usr/local/lib/python3.8/dist-packages/transitions/core.py", line 1087, in resolve_callable
>    func = getattr(event_data.model, func)
> AttributeError: 'Parent' object has no attribute 'ok'
> During handling of the above exception, another exception occurred:
[...]
aleneum commented 3 years ago

Hello @alexandretanem,

conditions are resolved during runtime on the model in question. This means that when you pass a condition as a string, it is expected that the model in question (parent) has a fitting method/property definition. You can however pass conditions 'by reference' instead. In this case transitions does not attempt to find it on the model:

from transitions.extensions.nesting import HierarchicalMachine as HierarchicalGraphMachine

class Child():
    def __init__(self):
        self.true = True

    def ok(self):
        return self.true

class Parent():
    pass

modelChild = Child()
machineChild = HierarchicalGraphMachine(model=modelChild, states=['init', 'done'], initial='init', auto_transitions=False)
machineChild.add_transition('go', 'init', 'done', conditions=modelChild.ok)

modelParent = Parent()
machineParent = HierarchicalGraphMachine(model=modelParent, states=['init', 'done', {'name':'child', 'children':machineChild}], initial='init', auto_transitions=False)
machineParent.add_model(modelChild)
machineParent.add_transition('go', 'init', 'child')
machineParent.add_transition('finish', 'child_done', 'done')

def printState():
    print("# ===")
    print("# PARENT : {} => {}".format(modelParent.state, machineParent.get_triggers(modelParent.state)))
    print("# CHILD : {} => {}".format(modelChild.state, machineChild.get_triggers(modelChild.state)))

printState()
# ===
# PARENT : init => ['go']
# CHILD : init => ['go']
modelParent.go()
printState()
# ===
# PARENT : child_init => ['go']
# CHILD : init => ['go']
modelParent.go()
printState()
# ===
# PARENT : child_done => ['finish']
# CHILD : init => ['go']
alexandretanem commented 3 years ago

Hello @aleneum, thank you for the quick answer ! Maybe this way of referencing conditions could also appear in this section of the readme ?

Solving this raised another problem with "invalid / False" conditions :

If we add a "False" condition on a child machine's transition and try to trigger this transition, it raises a MachineError exception only when parent state machine have a same trigger name.

Here is the example :

class Child():
    def __init__(self):
        self.true = True

    def ok(self):
        return self.true

    def nok(self):
        return not self.ok()

class Parent():
    pass

modelChild = Child()
machineChild = HierarchicalGraphMachine(model=modelChild, states=['init', 'done', 'nodone'], initial='init', auto_transitions=False)
machineChild.add_transition('go', 'init', 'done', conditions=modelChild.ok)
machineChild.add_transition('nogo', 'init', 'nodone', conditions=modelChild.nok)

modelParent = Parent()
machineParent = HierarchicalGraphMachine(model=modelParent, states=['init', 'done', {'name':'child', 'children':machineChild}, 'nodone'], initial='init', auto_transitions=False)
machineParent.add_model(modelChild)
machineParent.add_transition('go', 'init', 'child')
machineParent.add_transition('finish', 'child_done', 'done')
#machineParent.add_transition('nogo','child_done','nodone') # /!\ Line to uncomment

def printState():
    print("# PARENT : {} => {}".format(modelParent.state, machineParent.get_triggers(modelParent.state)))

printState()
# PARENT : init => ['go']
modelParent.go()
printState()
# PARENT : child_init => ['go', 'nogo']
modelParent.nogo()
printState()
# PARENT : child_init => ['go', 'nogo']
# Ok => the transition is not executed as modelChild.nok returns False

Now we execute the same thing with the following line uncommented

machineParent.add_transition('nogo','child_done','nodone')

printState()
# PARENT : init => ['go']
modelParent.go()
printState()
# PARENT : child_init => ['go', 'nogo']
modelParent.nogo()

#Traceback (most recent call last):
# File "testissue.py", line 36, in <module>
#  modelParent.nogo()
# File "/usr/local/lib/python3.8/dist-packages/transitions/extensions/nesting.py", line 749, in trigger_event
#  return self._check_event_result(res, _model, _trigger)
# File "/usr/local/lib/python3.8/dist-packages/transitions/extensions/nesting.py", line 820, in _check_event_result
#  raise MachineError(msg)
#transitions.core.MachineError: "Can't trigger event 'nogo' from state(s) child_init!"

The MachineError exception is triggered, while calling modelParent.nogo() should not just simply return False, as with non-HierarchicalMachine ?

aleneum commented 3 years ago

Maybe this way of referencing conditions could also appear in this section of the readme ?

This functionality is already documented here. I know, all developers are super busy but explaining the same functionality multiple times for convenience causes redundancy and may lead to out-of-sync and conflicting readme passages in the long run. If you think that a use case isn't covered in the docs, I suggest heading over to Stackoverflow and ask for assistance. Users of transitions are usually more active there than in this issue tracker. If readme issues pop up frequently (on SO) (e.g. override rules for convenience functions), we will of course address them even if it means redundancy.

The MachineError exception is triggered, while calling modelParent.nogo() should not just simply return False, as with non-HierarchicalMachine?

I agree that a definition in a child should be enough to not trigger an invalid transition exception. 6ad4a4d should fix this.

alexandretanem commented 3 years ago

I understand. Thank you for the quick fix !