pythonmypypython-typingpytransitions

What is the proper way to handle mypy [attr-defined] errors, due to transitions dynamically adding is_* attributes?


MRE

from transitions import Machine


class TradingSystem:
    def __init__(self):
        self.machine = Machine(model=self, states=['RUNNING'], initial='RUNNING')

    def check_running(self) -> None:
        if self.is_RUNNING():              
            print("System is running")

Example usage

system = TradingSystem()
system.check_running()

Issue

mypy transitions_mypy.py

gives the error:

transitions_mypy.py:9: error: "TradingSystem" has no attribute "is_RUNNING"  [attr-defined]

This can be avoided by bypassing mypy, for example adding # type: ignore[attr-defined] at the end of line 9.

But what is the proper way? Is it better to avoid bypassing mypy? Perhaps by manually defining the attribute?


Solution

  • The answer @Mark proposed won't work since transitions does not override already existing model attributes because that would be unexpected (mis)behaviour (as @Mark pointed out). If you enable logging, transitions will also tell you that:

    import logging
    logging.basicConfig(level=logging.DEBUG)
    

    The output should contain:

    WARNING:transitions.core:Model already contains an attribute 'is_RUNNING'. Skip binding.
    

    In the past, I suggested to inherit from Machine and override Machine._checked_assignment to workaround that safeguard. However, @james-hirschorn's approach using attrs which was posted in the transitions issue tracker is a better solution in my oppinion:

    from typing import Callable
    from attrs import define, field
    from transitions import Machine
    
    
    @define(slots=False)
    class TradingSystem:
        is_RUNNING: Callable[[], bool] = field(init=False)
        stop: Callable[[], None] = field(init=False)
    
        def __attrs_post_init__(self):
            self.machine = Machine(model=self, states=['RUNNING', 'STOPPED'], initial='RUNNING')
            self.machine.add_transition(trigger='stop', source='RUNNING', dest='STOPPED')
    
        def check_running(self) -> None:
            if self.is_RUNNING():  
                print("System is running")
    
        def stop_system(self) -> None:
            self.stop()
            print("System stopped")
    
    # Example usage
    system = TradingSystem()
    system.check_running()
    system.stop_system()
    

    This produces some overhead since you have to define triggers and convenience methods twice. Furthermore, type information are also incomplete because transitions will also add is_STOPPED and auto transitions like to_STOPPED/RUNNING or 'peek' transition methods such as may_stop to TradingSystem during runtime.

    Improving transitions typing support is currently an open issue.