pythonstate-machinepytransitions

Model already contains an attribute 'trigger'. Skip binding


I want to implement state machine for my Order model, and I am using this beautiful pytransitions library. But I am facing this strange issue.

This is my order.py with model:

from order_state_machine import OrderStateMachine

class Order(BaseModel):
    def __init__(self, order_dict):
        super().__init__(order_dict)
        # next line basically creates Customer object inside Order model
        self.set('customer', Customer(order_dict['customer']))
        self.machine = OrderStateMachine(self)

This my order_state_machine.py:

from transitions import Machine

class OrderStateMachine(Machine):
    order_states = ['pending', 'paid', 'shipped', 'delivered', 'canceled']

    order_transitions = [
        {'trigger': 'pay', 'source': 'pending', 'dest': 'paid'},
        {'trigger': 'deliver', 'source': 'shipped', 'dest': 'delivered'},
        {'trigger': 'cancel', 'source': 'shipped', 'dest': 'canceled'},
    ]

    def __init__(self, order):
        super().__init__(
            model=order,
            states=OrderStateMachine.order_states,
            transitions=OrderStateMachine.order_transitions,
            initial='pending'
        )

And when I do:

from order import Order

new_order = Order(order_dict)

new_order.state  # returns 'pending'
new_order.pay()
new_order.state  # I expect 'paid'

It, new_order.pay() line, gives me TypeError: 'NoneType' object is not callable error. And also a Model already contains an attribute 'trigger'. Skip binding. warning, and a lot of warnings like this.

Could someone help me to resolve this problem, may be maintainers of the library. Thanks.


Solution

  • I cannot tell with one hundred percent certainty but I assume your BaseModel already contains attributes and methods that overlap with methods that transitions wants to dynamically decorate your model with.

    transitions will do a 'checked' assignment which means it will only assign triggers and convenience functions to a model when the desired name is not already taken by existing attributes or methods. The reason behind this is that sometimes people do not care about the triggers and call functions exclusively by their name (using the trigger method actually).

    I assume your BaseModel contains an attribute assignment similarly to [1] as seen below.

    from transitions import Machine
    import logging
    
    
    class BaseModel:
    
        def __init__(self, order_dict):
            self.pay = None  # [1] already defined attribute
    
        def trigger(self):  # [2] already defined method
            pass
    
    
    class OrderStateMachine(Machine):
        order_states = ['pending', 'paid', 'shipped', 'delivered', 'canceled']
    
        order_transitions = [
            {'trigger': 'pay', 'source': 'pending', 'dest': 'paid'},
            {'trigger': 'deliver', 'source': 'shipped', 'dest': 'delivered'},
            {'trigger': 'cancel', 'source': 'shipped', 'dest': 'canceled'},
        ]
    
        def __init__(self, order):
            super().__init__(
                model=order,
                states=OrderStateMachine.order_states,
                transitions=OrderStateMachine.order_transitions,
                initial='pending'
            )
    
    
    class Order(BaseModel):
        def __init__(self, order_dict):
            super().__init__(order_dict)
            self.machine = OrderStateMachine(self)
    
    logging.basicConfig(level=logging.DEBUG)
    # [2] will cause 'Model already contains an attribute 'trigger'. Skip binding.'
    new_order = Order({})  
    new_order.state  # returns 'pending'
    # [1] will cause a TypeError: 'NoneType' object is not callable
    new_order.pay()
    new_order.state
    

    To deal with this you should make sure that trigger names and model attributes are mutually exclusive to prevent naming collisions. If you did define all those methods on purpose -- for instance to drop some code completion hints to your IDE -- and you REALLY want to keep it that way you can override the Machine._checked_assignment method:

    class OveriddingMachine(Machine):
    
        # assign everything to the model ignoring already existing attributes
        def _checked_assignment(self, model, name, func):
            setattr(model, name, func)
    

    Note that if you do that your machine might mess with your model in undesired ways.