diff --git a/secop/states.py b/secop/states.py index 9767920..4dbd049 100644 --- a/secop/states.py +++ b/secop/states.py @@ -27,30 +27,27 @@ handles status depending on statemachine state from secop.core import BUSY, IDLE, ERROR, Parameter, Command, Done -from secop.errors import ProgrammingError from secop.lib.newstatemachine import StateMachine, Retry, Finish, Start, Stop -class status_code: - """decorator for state methods""" - def __init__(self, code, text=None): - self.code = code - self.text = text +def status_code(code, text=None): + """decorator, attaching a status to a state function - def __set_name__(self, owner, name): - if not issubclass(owner, HasStates): - raise ProgrammingError('when using decorator "status_code", %s must inherit HasStates' % owner.__name__) - self.cls = owner - self.name = name - if 'statusMap' not in owner.__dict__: - # we need a copy on each inheritance level - owner.statusMap = owner.statusMap.copy() - owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text - setattr(owner, name, self.func) # replace with original method + :param code: the first element of the secop status tuple + :param text: the second element of the secop status tuple. if not given, + the name of the state function is used (underscores replaced by spaces) + :return: the decorator function - def __call__(self, func): - self.func = func - return self + if a state function has not attached status and is a method of the module running + the state machine, the status is inherited from an overridden method, if available + + a state function without attached status does not change the status, or, if it is + used as the start function, BUSY is taken as default status code + """ + def wrapper(func): + func.status = code, func.__name__.replace('_', ' ') if text is None else text + return func + return wrapper class HasStates: @@ -58,7 +55,7 @@ class HasStates: all_status_changes = False # when true, send also updates for status changes within a cycle _state_machine = None _status = IDLE, '' - statusMap = {} + statusMap = None def init_state_machine(self, **kwds): self._state_machine = StateMachine( @@ -71,6 +68,7 @@ class HasStates: def initModule(self): super().initModule() + self.statusMap = {} self.init_state_machine() def state_transition(self, sm, newstate): @@ -97,7 +95,22 @@ class HasStates: status = self._state_machine.idle_status or (ERROR, 'Finish was returned without final status') else: name = statefunc.__name__ - status = self.statusMap.get(name) + try: + # look up in statusMap cache + status = self.statusMap[name] + except KeyError: + # try to get status from method or inherited method + cls = type(self) + for base in cls.__mro__: + try: + status = getattr(base, name, None).status + break + except AttributeError: + pass + else: + status = None + # store it in the cache for all further calls + self.statusMap[name] = status if status is None and default_code is not None: status = default_code, name.replace('_', ' ') return status @@ -108,8 +121,7 @@ class HasStates: return Done return sm.status - def doPoll(self): - super().doPoll() + def cycle_machine(self): sm = self._state_machine sm.cycle() if sm.statefunc is None: @@ -118,6 +130,10 @@ class HasStates: self.setFastPoll(False) self.read_status() + def doPoll(self): + super().doPoll() + self.cycle_machine() + def on_cleanup(self, sm): if isinstance(sm.cleanup_reason, Exception): return self.on_error(sm)