pytransitions / transitions

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

Adding non-python state name support #546

Closed memetb closed 2 years ago

memetb commented 2 years ago

I'd like to be able to name my states with identifiers that aren't necessarily valid python identifiers: e.g. "high:active" and "high:idle" might be two states I want to have (these states allow me to create statechart compatible fsm's). But I'd also like to be able to declare on_enter_high_idle .

This could be implemented for instance, by changing the following line (and all other relevant spots):

       method = "{0}_{1}".format(callback, state.name)

To:

        method = "{0}_{1}".format(callback, make_python_token(state.name))

where make_python_token is default implemented as follows:

       def make_python_token(name):
               return name

This would allow any user of the library to inherit and specialize the make_python_token method.

aleneum commented 2 years ago

Hello @memetb,

Machine contains a method called _checked_assignment that you can use to achieve this, I guess.

This is the default implementation:

    def _checked_assignment(self, model, name, func):
        if hasattr(model, name):
            _LOGGER.warning("%sModel already contains an attribute '%s'. Skip binding.", self.name, name)
        else:
            setattr(model, name, func)

So your use case could be achieved with

from transitions import Machine

class ColonMachine(Machine):

    def _checked_assignment(self, model, name, func):
        super()._checked_assignment(model, name.replace(":", "_"), func)

s = ColonMachine(states=["foo:a", "foo:b"], initial="foo:a")
assert s.is_foo_a()
s.to_foo_b()
print(s.state)
memetb commented 2 years ago

Thanks @aleneum: can you please maybe help me out with this following test case?

When using "_" for the nested state separator, the following basic example works as expected (I get some transitions and my on_enter callbacks work as expected). I'd like to be able to do this with ":" instead of "_" for the NestedState.separator.

from transitions.extensions import HierarchicalMachine
from transitions.extensions.nesting import NestedState

c = '_' # <-- replace this with ':' to test desired test scenario
NestedState.separator = c

class Foo(HierarchicalMachine):
    def _checked_assignment(self, model, name, func):
        #print(f"check: {model} {name} {func}")
        super()._checked_assignment(model, name.replace(":", "_"), func)

    def conditionA(self) : return self.tick % 5 == 0
    def conditionB(self) : return self.tick % 5 == 1
    def conditionC(self) : return self.tick % 5 == 2
    def conditionD(self) : return self.tick % 5 == 3
    def conditionE(self) : return self.tick % 5 == 4

    def on_enter_foo(self):
        print("entered foo")

    def on_enter_bar(self):
        print("entered bar")

    def on_enter_foo_A(self):
        print("entered foo:A")

    def on_enter_foo_B(self):
        print("entered foo:B")

    def __init__(self):
        states = [
            { "name": "foo", "children" : [ "A", "B"], "initial": "A"},
            { "name": "bar", "children" : [ "B", "C", "D"]}
        ]
        HierarchicalMachine.__init__(self, states=states, initial='foo')

        self.add_transition(trigger='impl', source='*',           dest=f'foo{c}A', conditions='conditionE')
        self.add_transition(trigger='impl', source=f'foo{c}A',    dest=f'foo{c}B', conditions='conditionB')
        self.add_transition(trigger='impl', source='foo', dest=f'bar{c}C', conditions='conditionC')

        self.tick = 0

    def run(self):
        self.tick += 1

        self.impl()

fsm = Foo()
for i in range(10):
    fsm.run()
aleneum commented 2 years ago

When using HSMs transitions will NOT decorate models with to_stateA_stateB when the separator has been changed. See the documentation:

This means that in the standard configuration, state names in HSMs MUST NOT contain underscores. For transitions it's impossible to tell whether machine.add_state('state_name') should add a state named state_name or add a substate name to the state state. In some cases this is not sufficient however. For instance if state names consists of more than one word and you want/need to use underscore to separate them instead of CamelCase.

Instead of to_C_3_a() auto transition is called as to_C.s3.a(). If your substate starts with a digit, transitions adds a prefix 's' ('3' becomes 's3') to the auto transition FunctionWrapper to comply with the attribute naming scheme of Python. If interactive completion is not required, to('C↦3↦a') can be called directly.

Since StateA:StateA_1 and StateA:StateA:1 would basically both result in to_StateA_StateA_1 or on_enter_StateA_StateA_1 the above mentioned workaround can be used.

memetb commented 2 years ago

Thank you for your response. I understand the complication.