Experimental State Management in Python
I needed a way to manage state that is loaded from a set of configs, as well as state that is dependent on those states. I thought the solution that I came up with was worth sharing.
Some useful properties of the object below:
- Indicates when it needs to be rebuilt via .valid
- Raises an error if the value is requested but the state has been invalidated
- Recurses any dependent StateObjects used to create the state
- States are only built when needed
- States are defined by how they are built
""" State Object that can be invalidated """
from typing import (
Any,
Union,
List,
TypeVar
)
import threading
from functools import reduce
# pylint: disable=invalid-name
T = TypeVar('T')
class InvalidStateObjectRequested(Exception):
""" Thrown when invalidated StateObject value is requested """
class Validatable:
""" class with valid property """
@property
def valid(self):
""" Intended as Abstract method, must override """
return False
class StateObject(Validatable):
""" manages a state object that can be invalidated """
__recalc_required = True
__value: Any
__lock = threading.Lock()
__depends_on: List[Validatable]
def __init__(self, depends_on: Union[List[Validatable], Validatable, None] = None):
if depends_on is None:
self.__depends_on = []
elif isinstance(depends_on, list):
self.__depends_on = depends_on
else:
self.__depends_on = [depends_on]
def invalidate(self):
""" Invalidates current value. Must be set before data can be returned """
self.__recalc_required = True
@property
def valid(self) -> bool:
""" Property, true if a the value is still valid. Otherwise false """
return not self.__recalc_required and reduce(lambda a, b: a and b, map(lambda a: a.valid, self.__depends_on), True)
def set(self, value: T) -> None:
""" Set the value, resets value to valid """
self.__lock.acquire()
self.__recalc_required = False
self.__value = value
self.__lock.release()
def get(self) -> T:
""" Returns the value, throws InvalidStateObjectRequested if invalid """
self.__lock.acquire()
if self.__recalc_required:
self.__lock.release()
raise InvalidStateObjectRequested()
result = self.__value
self.__lock.release()
return result
The state then can be used like so...
from .load_state import (
rebuild_my_state,
rebuild_my_sub_state
)
class MyStatefulObject:
my_state_cache: StateObject
my_sub_state_cache: StateObject
def __init__(self):
self.my_state_cache = StateObject(depends_on=None)
self.my_sub_state_cache = StateObject(depends_on=self.my_state_cache)
@property
def my_state(self):
if not self.my_state_cache.valid:
self.my_state_cache.set(rebuild_my_state())
return self.my_state_cache.get()
@property
def my_sub_state(self):
if not self.my_sub_state_cache.valid:
self.my_sub_state_cache.set(rebuild_my_sub_state(self.my_state))
return self.my_sub_state_cache.get()
def rebuild(self, target_state: str = 'all'):
if target_state == 'my_state' or target_state == 'all':
self.my_state_cache.invalidate()
if target_state == 'my_sub_state' or target_state == 'all':
self.my_sub_state_cache.invalidate()