pytransitions / transitions

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

Conditions on nested state machines don't get called properly #595

Closed translunar closed 1 year ago

translunar commented 1 year ago

Describe the bug When I define a machine with

states=[{"name": "A"}, {"children", child.machine}]

and the child model defines a transition with "conditions": "test", it attempts to call test() on the parent model rather than the nested model.

Minimal working example

import unittest

from transitions.extensions import MachineFactory

class Parent(object):
    def __init__(self, child):
        states = [
            { "name": "A" },
            { "name": "B", "children": child.machine }
        ]

        transitions = [
            {"trigger": "go", "source": "A", "dest": "B_a"},
            {"trigger": "go", "source": "B_b", "dest": "A"},
        ]

        machine_cls = MachineFactory.get_predefined(nested=True)
        self.machine = machine_cls(
            model=self,
            states=states,
            transitions=transitions,
            initial="A"
        )
        self.test_called = False

    def test(self):
        self.test_called = True

class Child(object):
    def __init__(self):
        states = [
            {"name": "a"},
            {"name": "b"},
        ]

        transitions = [
            {"trigger": "go", "source": "a", "dest": "b", "conditions": "test"}
        ]

        machine_cls = MachineFactory.get_predefined(nested=True)
        self.machine = machine_cls(model=self, states=states, transitions=transitions)
        self.test_called = False

    def test(self):
        self.test_called = True

class TestNestedModel(unittest.TestCase):

    def test_mwe(self):
        c = Child()
        p = Parent(c)

        assert p.test_called == False
        assert c.test_called == False

        p.go()

        assert p.test_called == False
        assert c.test_called == False

        p.go()

        assert c.test_called == True
        assert p.test_called == False

Expected behavior I expect test() to get called on the child model and not the parent model.

Additional context Add any other context about the problem here.

translunar commented 1 year ago

I found a workaround, which is to refer to the function by reference instead of by string, e.g. "conditions": self.test. However, this condition seems to get called on every internal state transition instead of just the trigger it's defined on.

aleneum commented 1 year ago

Hello @translunar,

as of now this is intended behaviour. The documentation mentions

(Nested)State instances are just referenced which means changes in one machine's collection of states and events will influence the other machine instance. Models and their state will not be shared though.

Strings as callbacks will always be treated as methods assigned to the currently processed model. You could either use inheritance or reference methods directly (as you just did).

translunar commented 1 year ago

It might be helpful to clarify this in the documentation. I had to dig through the issues to find it.