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.
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.