pytransitions / transitions

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

on_exception and before_state_change callbacks fail if created in-class #571

Closed andrewvaughan closed 1 year ago

andrewvaughan commented 2 years ago

Using the NarcolepticSuperhero pattern fails when trying to also set on_exception or before_state_change callbacks:

class Automator():

    # ... removed for brevity ...

    def __init__(self):
        """
        Create a new Automator.
        """

        self.__machine  = Machine(
            model               = self,

            before_state_change = '__check_login',
            on_exception        = '__handle_exception',
            send_event          = True,

            states              = Automator.__states,
            transitions         = Automator.__transitions,
            initial             = 'starting'
        )

    # ... removed for brevity ...

    def __check_login(self, event):
        """
        Checks if the user is logged in before each call (and logs in, if necessary).
        """

        self.__logger.info("CHECKING LOGIN")

    def __handle_exception(self, event):
        """
        Gracefully handle any exceptions during the automation.
        """

        self.__logger.error(f"Exception: {event.error}")

        self.to_shutdown()

This creates a cascade of errors when attempting to be called:

Traceback (most recent call last):
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1158, in resolve_callable
    func = getattr(event_data.model, func)
AttributeError: 'Automator' object has no attribute '__check_login'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1165, in resolve_callable
    mod, name = func.rsplit('.', 1)
ValueError: not enough values to unpack (expected 2, got 1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../lib/python3.9/site-packages/transitions/core.py", line 435, in _process
    if trans.execute(event_data):
  File ".../lib/python3.9/site-packages/transitions/core.py", line 272, in execute
    event_data.machine.callbacks(itertools.chain(event_data.machine.before_state_change, self.before), event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1123, in callbacks
    self.callback(func, event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1140, in callback
    func = self.resolve_callable(func, event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1171, in resolve_callable
    raise AttributeError("Callable with name '%s' could neither be retrieved from the passed "
AttributeError: Callable with name '__check_login' could neither be retrieved from the passed model nor imported from a module.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1158, in resolve_callable
    func = getattr(event_data.model, func)
AttributeError: 'Automator' object has no attribute '__handle_exception'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1165, in resolve_callable
    mod, name = func.rsplit('.', 1)
ValueError: not enough values to unpack (expected 2, got 1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "...", line 142, in run
    self.automator.next()
  File ".../lib/python3.9/site-packages/transitions/core.py", line 401, in trigger
    return self.machine._process(func)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1188, in _process
    return trigger()
  File ".../lib/python3.9/site-packages/transitions/core.py", line 426, in _trigger
    return self._process(event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 441, in _process
    self.machine.callbacks(self.machine.on_exception, event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1123, in callbacks
    self.callback(func, event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1140, in callback
    func = self.resolve_callable(func, event_data)
  File ".../lib/python3.9/site-packages/transitions/core.py", line 1171, in resolve_callable
    raise AttributeError("Callable with name '%s' could neither be retrieved from the passed "
AttributeError: Callable with name '__handle_exception' could neither be retrieved from the passed model nor imported from a module.

It seems like the Machine cannot resolve callbacks from __init__ when they're created in the NarcolepticSuperhero pattern.

andrewvaughan commented 2 years ago

Upon further review, it's because I had __ in front of the class names as private methods... not sure if that's resolvable with this scope. This may be closeable.

aleneum commented 1 year ago

Hello @andrewvaughan,

when passed as strings, callbacks will be resolved during runtime. This does not work with double underscore functions since their names will get obfuscated. If you NEED double underscore functions you could pass them by reference:

from transitions import Machine

class Automator:

    def __init__(self):
        self.__machine = Machine(
            model=self,
            before_state_change=self.__check_login,
            on_exception=self.__handle_exception,
            send_event=True,
            states=['starting'],
            initial='starting'
        )

    # ... removed for brevity ...

    def __check_login(self, event):
        print("CHECKING LOGIN")

    def __handle_exception(self, event):
        print(f"Exception: {event.error}")

m = Automator()
m.to_starting()