pytransitions / transitions

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

AsyncGraphMachine returns `True` whether or not conditions fail #401

Closed kbinpgh closed 4 years ago

kbinpgh commented 4 years ago

This example has a condition for the transition labelled reset. calling the reset() trigger returns True whether or not the transition's condition callback check failed/passed.

from transitions.extensions import MachineFactory
from enum import Enum, auto
import time
from IPython.display import Image, display, display_png
import os, sys, inspect, io, asyncio
import logging

class State(Enum):
    START = auto(),
    ONE = auto(),
    TWO = auto(),
    THREE = auto(),

class Model(object):
    transition_1 = dict(
        trigger='one',
        source=State.START,
        dest=State.ONE,
        before='before',
        after='after',
    )

    transition_2 = dict(
        trigger='two',
        source=State.ONE,
        dest=State.TWO,
        before='before',
        after='after',
    )

    transition_3 = dict(
        trigger='three',
        source=State.TWO,
        dest=State.THREE,
        before='before',
        after='after',
    )

    transition_0 = dict(
        trigger='reset',
        source=State.THREE,
        dest=State.START,
        before='before',
        after='after',
        conditions='sooner_than',
    )

    def __init__(self):
        self.start_time = None
        machine = MachineFactory.get_predefined(graph=True, asyncio=True)(
            model=self,
            states = [v for v in State],
            transitions = [
                self.transition_0, 
                self.transition_1, 
                self.transition_2, 
                self.transition_3
            ],
            queued = True,
        )

    def start(self):
        self.cycles = 0
        self.start_time = time.time()
        print(f"{time.time()}> started")

    async def after(self, which=None):
        print(f"{time.time()}> After transition. which={which}, model={self}")
        if which == 'one':
            #start_time = time.time()
            await self.two(which='two')
        if which == 'two':
            #start_time = time.time()
            await self.three(which='three')
        if which == 'three':
            #start_time = time.time()
            result = await self.reset(which='reset')
            print(f"reset returned {result}")
        if which == 'reset':
            #start_time = time.time()
            await self.one(which='one')

    async def before(self, which=None):
        print(f"{time.time()}> Before transition {which}")
        if which == 'reset':
            end_time = time.time()
            print(f"Reset at {end_time} in {end_time-self.start_time}s") 
            self.cycles += 1

    async def sooner_than(self, which=None):
        duration = time.time()-self.start_time
        print(f"{duration}s elapsed")
        return duration < 0.0

async def main():
    model = Model()

    logging.basicConfig(level=logging.DEBUG)

    await model.to_START()
    model.start()
    await model.one(which='one')

    await asyncio.sleep(4)

    assert model.is_THREE()

    result = await model.reset(which='reset')
    print(f"reset returned :{result}")

    print(model.cycles)
asyncio.run(main())

a snippet of the Ooutput (with log level set to DEBUG):

DEBUG:transitions.extensions.asyncio:Initiating transition from state THREE to state START...
DEBUG:transitions.extensions.asyncio:Executed callbacks before conditions.
4.008964776992798s elapsed
DEBUG:transitions.extensions.asyncio:Transition condition failed: Transition halted.
DEBUG:transitions.extensions.asyncio:Executed machine finalize callbacks
reset returned :True
0
aleneum commented 4 years ago

calling the reset() trigger returns True whether or not the transition's condition callback check failed/passed.

As mentioned in the ReadMe:

Important note: when processing events in a queue, the trigger call will always return True, since there is no way to determine at queuing time whether a transition involving queued calls will ultimately complete successfully. This is true even when only a single event is processed.

Reducing your code and removing queued has the expected result:

from transitions.extensions import MachineFactory
from enum import Enum, auto
import time
import logging
import asyncio

class State(Enum):
    START = auto(),
    ONE = auto(),
    TWO = auto(),
    THREE = auto(),

class Model(object):

    transition_0 = dict(
        trigger='reset',
        source=State.THREE,
        dest=State.START,
        before='before',
        after='after',
        conditions='sooner_than',
    )

    def __init__(self):
        self.start_time = None
        machine = MachineFactory.get_predefined(graph=True, asyncio=True)(
            model=self,
            states=[v for v in State],
            transitions=[
                self.transition_0
            ],
            initial=State.THREE
        )
        self.start_time = time.time()
        self.cycles = 0

    async def after(self, which=None):
        if which == 'reset':
            await self.one(which='one')

    async def before(self, which=None):
        if which == 'reset':
            end_time = time.time()
            print(f"Reset at {end_time} in {end_time - self.start_time}s")
            self.cycles += 1

    async def sooner_than(self, which=None):
        duration = time.time() - self.start_time
        print(f"{duration}s elapsed")
        return duration < 0.0

async def main():
    model = Model()
    logging.basicConfig(level=logging.DEBUG)
    result = await model.reset(which='reset')
    print(f"reset returned :{result}")
    print(model.cycles)

asyncio.run(main())

Output:

DEBUG:transitions.extensions.asyncio:Initiating transition from state THREE to state START...
DEBUG:transitions.extensions.asyncio:Executed callbacks before conditions.
DEBUG:transitions.extensions.asyncio:Transition condition failed: Transition halted.
DEBUG:transitions.extensions.asyncio:Executed machine finalize callbacks
0.00033402442932128906s elapsed
reset returned :False
0
kbinpgh commented 4 years ago

:+1: Thank you. My specific use case may need queued=True, but I will verify. I feel that the condition checking is more important.

And thanks for your great work here :)