commit 5cc0af80d64381084a72be73632f6e4fb8b7ba96
parent dbb2280a034bea0954e95945bee4820e5b873c85
Author: lash <dev@holbrook.no>
Date: Wed, 9 Feb 2022 16:47:29 +0000
WIP add docstrings to persist
Diffstat:
M | shep/persist.py | | | 71 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- |
M | shep/state.py | | | 319 | ++++++++++++++++++++++++++++++++++++++++++------------------------------------- |
2 files changed, 229 insertions(+), 161 deletions(-)
diff --git a/shep/persist.py b/shep/persist.py
@@ -4,6 +4,15 @@ from .error import StateItemExists
class PersistedState(State):
+ """Adapter for persisting state changes and synchronising states between memory and persisted backend.
+
+ :param factory: A function capable of returning a persisted store from a single path argument.
+ :type factory: function
+ :param bits: Number of pure states. Passed to the superclass.
+ :type bits: int
+ :param logger: Logger to capture logging output, or None for no logging.
+ :type logger: object
+ """
def __init__(self, factory, bits, logger=None):
super(PersistedState, self).__init__(bits, logger=logger)
@@ -11,12 +20,17 @@ class PersistedState(State):
self.__stores = {}
+ # Create state store container if missing.
def __ensure_store(self, k):
if self.__stores.get(k) == None:
self.__stores[k] = self.__store_factory(k)
def put(self, key, contents=None, state=None):
+ """Persist a key or key/content pair.
+
+ See shep.state.State.put
+ """
to_state = super(PersistedState, self).put(key, state=state, contents=contents)
k = self.name(to_state)
@@ -26,6 +40,10 @@ class PersistedState(State):
def set(self, key, or_state):
+ """Persist a new state for a key or key/content.
+
+ See shep.state.State.set
+ """
from_state = self.state(key)
k_from = self.name(from_state)
@@ -41,6 +59,10 @@ class PersistedState(State):
def unset(self, key, not_state):
+ """Persist a new state for a key or key/content.
+
+ See shep.state.State.unset
+ """
from_state = self.state(key)
k_from = self.name(from_state)
@@ -57,11 +79,16 @@ class PersistedState(State):
def move(self, key, to_state):
+ """Persist a new state for a key or key/content.
+
+ See shep.state.State.move
+ """
from_state = self.state(key)
to_state = super(PersistedState, self).move(key, to_state)
return self.__movestore(key, from_state, to_state)
+ # common procedure for safely moving a persisted resource from one state to another.
def __movestore(self, key, from_state, to_state):
k_from = self.name(from_state)
k_to = self.name(to_state)
@@ -76,6 +103,13 @@ class PersistedState(State):
def sync(self, state):
+ """Reload resources for a single state in memory from the persisted state store.
+
+ :param state: State to load
+ :type state: int
+ :raises StateItemExists: A content key is already recorded with a different state in memory than in persisted store.
+ # :todo: if sync state is none, sync all
+ """
k = self.name(state)
self.__ensure_store(k)
@@ -89,36 +123,51 @@ class PersistedState(State):
def list(self, state):
+ """List all content keys for a particular state.
+
+ This method will return from memory, and will not sync the persisted state first.
+
+ See shep.state.State.list
+ """
k = self.name(state)
self.__ensure_store(k)
#return self.__stores[k].list(state)
return super(PersistedState, self).list(state)
- # Return a file path or URL pointing to the persisted state.
- #
- # If the key is omitted, the URL to the state item's container must be returned, and None if no such container exists.
- #
- # :param state: State to locate
- # :type state: int
- # :param key: Content key to locate
- # :type key: str
- # :rtype: str
- # :returns: Locator pointng to persisted state
- # :todo: rename to "location"
def path(self, state, key=None):
+ """Return a file path or URL pointing to the persisted state.
+
+ If the key is omitted, the URL to the state item's container must be returned, and None if no such container exists.
+
+ :param state: State to locate
+ :type state: int
+ :param key: Content key to locate
+ :type key: str
+ :rtype: str
+ :returns: Locator pointng to persisted state
+ :todo: rename to "location"
+ """
k = self.name(state)
self.__ensure_store(k)
return self.__stores[k].path(key=key)
def next(self, key=None):
+ """Advance and persist to the next pure state.
+
+ See shep.state.State.next
+ """
from_state = self.state(key)
to_state = super(PersistedState, self).next(key)
return self.__movestore(key, from_state, to_state)
def replace(self, key, contents):
+ """Replace contents associated by content key.
+
+ See shep.state.State.replace
+ """
super(PersistedState, self).replace(key, contents)
state = self.state(key)
k = self.name(state)
diff --git a/shep/state.py b/shep/state.py
@@ -118,10 +118,11 @@ class State:
self.__keys_reverse[item] = state
- # Get index of a key for a given state.
- # A key should only ever exist in one state.
- # A failed lookup should indicate a mistake on the caller part, (it may also indicate corruption, but probanbly impossible to tell the difference)
def __state_list_index(self, item, state_list):
+ """Get index of a key for a given state.
+ A key should only ever exist in one state.
+ A failed lookup should indicate a mistake on the caller part, (it may also indicate corruption, but probanbly impossible to tell the difference)
+ """
idx = -1
try:
idx = state_list.index(item)
@@ -134,29 +135,31 @@ class State:
return idx
- # Add a state to the store.
- #
- # :param k: State name
- # :type k: str
- # :raises shep.error.StateExists: State name is already registered
def add(self, k):
+ """Add a state to the store.
+
+ :param k: State name
+ :type k: str
+ :raises shep.error.StateExists: State name is already registered
+ """
v = 1 << self.__c
k = self.__check_name(k)
v = self.__check_value(v)
self.__set(k, v)
- # Add an alias for a combination of states in the store.
- #
- # State aggregates may be provided as comma separated values or as a single (or'd) integer value.
- #|
- # :param k: Alias name
- # :type k: str
- # :param *args: One or more states to aggregate for this alias.
- # :type *args: int or list of ints
- # :raises StateInvalid: Attempt to create alias for one or more atomic states that do not exist.
- # :raises ValueError: Attempt to use bit value as alias
def alias(self, k, *args):
+ """Add an alias for a combination of states in the store.
+
+ State aggregates may be provided as comma separated values or as a single (or'd) integer value.
+
+ :param k: Alias name
+ :type k: str
+ :param *args: One or more states to aggregate for this alias.
+ :type *args: int or list of ints
+ :raises StateInvalid: Attempt to create alias for one or more atomic states that do not exist.
+ :raises ValueError: Attempt to use bit value as alias
+ """
k = self.__check_name(k)
v = 0
for a in args:
@@ -167,11 +170,12 @@ class State:
self.__set(k, v)
- # Return list of all unique atomic and alias states.
- #
- # :rtype: list of ints
- # :return: states
def all(self):
+ """Return list of all unique atomic and alias states.
+
+ :rtype: list of ints
+ :return: states
+ """
l = []
for k in dir(self):
if k[0] == '_':
@@ -183,14 +187,15 @@ class State:
return l
- # Retrieve that string representation of the state attribute represented by the given state integer value.
- #
- # :param v: State integer
- # :type v: int
- # :raises StateInvalid: State corresponding to given integer not found
- # :rtype: str
- # :return: State name
def name(self, v):
+ """Retrieve that string representation of the state attribute represented by the given state integer value.
+
+ :param v: State integer
+ :type v: int
+ :raises StateInvalid: State corresponding to given integer not found
+ :rtype: str
+ :return: State name
+ """
if v == None or v == 0:
return 'NEW'
k = self.__reverse.get(v)
@@ -199,31 +204,33 @@ class State:
return k
- # Retrieve the real state integer value corresponding to an attribute name.
- #
- # :param k: Attribute name
- # :type k: str
- # :raises ValueError: Invalid attribute name
- # :raises AttributeError: Attribute not found
- # :rtype: int
- # :return: Numeric state value
def from_name(self, k):
+ """Retrieve the real state integer value corresponding to an attribute name.
+
+ :param k: Attribute name
+ :type k: str
+ :raises ValueError: Invalid attribute name
+ :raises AttributeError: Attribute not found
+ :rtype: int
+ :return: Numeric state value
+ """
k = self.__check_name_valid(k)
return getattr(self, k)
- # Match against all stored states.
- #
- # If pure is set, only match against the single atomic state will be returned.
- #
- # :param v: Integer state to match
- # :type v: int
- # :param pure: Match only pure states
- # :type pure: bool
- # :raises KeyError: Unknown state
- # :rtype: tuple
- # :return: 0: Alias that input resolves to, 1: list of atomic states that matches the state
def match(self, v, pure=False):
+ """Match against all stored states.
+
+ If pure is set, only match against the single atomic state will be returned.
+
+ :param v: Integer state to match
+ :type v: int
+ :param pure: Match only pure states
+ :type pure: bool
+ :raises KeyError: Unknown state
+ :rtype: tuple
+ :return: 0: Alias that input resolves to, 1: list of atomic states that matches the state
+ """
alias = None
if not pure:
alias = self.__reverse.get(v)
@@ -242,23 +249,24 @@ class State:
return (alias, r,)
- # Add a key to an existing state.
- #
- # If no state it specified, the default state attribute "NEW" will be used.
- #
- # Contents may be supplied as value to pair with the given key. Contents may be changed later by calling the `replace` method.
- #
- # :param key: Content key to add
- # :type key: str
- # :param state: Initial state for the put. If not given, initial state will be NEW
- # :type state: int
- # :param contents: Contents to associate with key. A valie of None should be recognized as an undefined value as opposed to a zero-length value throughout any backend
- # :type contents: str
- # :raises StateItemExists: Content key has already been added
- # :raises StateInvalid: Given state has not been registered
- # :rtype: integer
- # :return: Resulting state that key is put under (should match the input state)
def put(self, key, state=None, contents=None):
+ """Add a key to an existing state.
+
+ If no state it specified, the default state attribute "NEW" will be used.
+
+ Contents may be supplied as value to pair with the given key. Contents may be changed later by calling the `replace` method.
+
+ :param key: Content key to add
+ :type key: str
+ :param state: Initial state for the put. If not given, initial state will be NEW
+ :type state: int
+ :param contents: Contents to associate with key. A valie of None should be recognized as an undefined value as opposed to a zero-length value throughout any backend
+ :type contents: str
+ :raises StateItemExists: Content key has already been added
+ :raises StateInvalid: Given state has not been registered
+ :rtype: integer
+ :return: Resulting state that key is put under (should match the input state)
+ """
if state == None:
state = self.NEW
elif self.__reverse.get(state) == None:
@@ -271,17 +279,18 @@ class State:
return state
- # Move a given content key from one state to another.
- #
- # :param key: Key to move
- # :type key: str
- # :param to_state: Numeric state to move to (may be atomic or alias)
- # :type to_state: integer
- # :raises StateItemNotFound: Given key has not been registered
- # :raises StateInvalid: Given state has not been registered
- # :rtype: integer
- # :return: Resulting state from move (should match the state given as input)
def move(self, key, to_state):
+ """Move a given content key from one state to another.
+
+ :param key: Key to move
+ :type key: str
+ :param to_state: Numeric state to move to (may be atomic or alias)
+ :type to_state: integer
+ :raises StateItemNotFound: Given key has not been registered
+ :raises StateInvalid: Given state has not been registered
+ :rtype: integer
+ :return: Resulting state from move (should match the state given as input)
+ """
current_state = self.__keys_reverse.get(key)
if current_state == None:
raise StateItemNotFound(key)
@@ -311,18 +320,19 @@ class State:
return to_state
- # Move to an alias state by setting a single bit.
- #
- # :param key: Content key to modify state for
- # :type key: str
- # :param or_state: Atomic stat to add
- # :type or_state: int
- # :raises ValueError: State is not a single bit state
- # :raises StateItemNotFound: Content key is not registered
- # :raises StateInvalid: Resulting state after addition of atomic state is unknown
- # :rtype: int
- # :returns: Resulting state
def set(self, key, or_state):
+ """Move to an alias state by setting a single bit.
+
+ :param key: Content key to modify state for
+ :type key: str
+ :param or_state: Atomic stat to add
+ :type or_state: int
+ :raises ValueError: State is not a single bit state
+ :raises StateItemNotFound: Content key is not registered
+ :raises StateInvalid: Resulting state after addition of atomic state is unknown
+ :rtype: int
+ :returns: Resulting state
+ """
if not self.__is_pure(or_state):
raise ValueError('can only apply using single bit states')
@@ -338,20 +348,21 @@ class State:
return self.__move(key, current_state, to_state)
- # Unset a single bit, moving to a pure or alias state.
- #
- # The resulting state cannot be NEW (0).
- #
- # :param key: Content key to modify state for
- # :type key: str
- # :param or_state: Atomic stat to add
- # :type or_state: int
- # :raises ValueError: State is not a single bit state, or attempts to revert to NEW
- # :raises StateItemNotFound: Content key is not registered
- # :raises StateInvalid: Resulting state after addition of atomic state is unknown
- # :rtype: int
- # :returns: Resulting state
def unset(self, key, not_state):
+ """Unset a single bit, moving to a pure or alias state.
+
+ The resulting state cannot be NEW (0).
+
+ :param key: Content key to modify state for
+ :type key: str
+ :param or_state: Atomic stat to add
+ :type or_state: int
+ :raises ValueError: State is not a single bit state, or attempts to revert to NEW
+ :raises StateItemNotFound: Content key is not registered
+ :raises StateInvalid: Resulting state after addition of atomic state is unknown
+ :rtype: int
+ :returns: Resulting state
+ """
if not self.__is_pure(not_state):
raise ValueError('can only apply using single bit states')
@@ -373,70 +384,76 @@ class State:
return self.__move(key, current_state, to_state)
- # Return the current numeric state for the given content key.
- #
- # :param key: Key to return content for
- # :type key: str
- # :raises StateItemNotFound: Content key is unknown
- # :rtype: int
- # :returns: State
def state(self, key):
+ """Return the current numeric state for the given content key.
+
+ :param key: Key to return content for
+ :type key: str
+ :raises StateItemNotFound: Content key is unknown
+ :rtype: int
+ :returns: State
+ """
state = self.__keys_reverse.get(key)
if state == None:
raise StateItemNotFound(key)
return state
- # Retrieve the content for a content key.
- #
- # :param key: Content key to retrieve content for
- # :type key: str
- # :rtype: any
- # :returns: Content
def get(self, key):
+ """Retrieve the content for a content key.
+
+ :param key: Content key to retrieve content for
+ :type key: str
+ :rtype: any
+ :returns: Content
+ """
return self.__contents.get(key)
- # List all content keys matching a state.
- #
- # :param state: State to match
- # :type state: int
- # :rtype: list of str
- # :returns: Matching content keys
def list(self, state):
+ """List all content keys matching a state.
+
+ :param state: State to match
+ :type state: int
+ :rtype: list of str
+ :returns: Matching content keys
+ """
try:
return self.__keys[state]
except KeyError:
return []
- # Noop method for interface implementation providing sync to backend.
- #
- # :param state: State to sync.
- # :type state:
- # :todo: (for higher level implementer) if sync state is none, sync all
def sync(self, state):
+ """Noop method for interface implementation providing sync to backend.
+
+ :param state: State to sync.
+ :type state:
+ :todo: (for higher level implementer) if sync state is none, sync all
+ """
pass
- # In the memory-only class no persisted state is used, and this will return None.
- #
- # See shep.persist.PersistedState.path for more information.
def path(self, state, key=None):
+ """In the memory-only class no persisted state is used, and this will return None.
+
+ See shep.persist.PersistedState.path for more information.
+ """
return None
- # Return the next pure state.
- #
- # Will return the same result as the method next, but without advancing to the new state.
- #
- # :param key: Content key to inspect state for
- # :type key: str
- # :raises StateItemNotFound: Unknown content key
- # :raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
- # :rtype: int
- # :returns: Next state
def peek(self, key):
+ """Return the next pure state.
+
+ Will return the same result as the method next, but without advancing to the new state.
+
+ :param key: Content key to inspect state for
+ :type key: str
+ :raises StateItemNotFound: Unknown content key
+ :raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
+ :rtype: int
+ :returns: Next state
+ """
state = self.__keys_reverse.get(key)
if state == None:
raise StateItemNotFound(key)
@@ -453,27 +470,29 @@ class State:
return state
- # Advance to the next pure state.
- #
- # :param key: Content key to inspect state for
- # :type key: str
- # :raises StateItemNotFound: Unknown content key
- # :raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
- # :rtype: int
- # :returns: Next state
def next(self, key):
+ """Advance to the next pure state.
+
+ :param key: Content key to inspect state for
+ :type key: str
+ :raises StateItemNotFound: Unknown content key
+ :raises StateInvalid: Attempt to advance from an alias state, OR beyond the last known pure state.
+ :rtype: int
+ :returns: Next state
+ """
from_state = self.state(key)
new_state = self.peek(key)
return self.__move(key, from_state, new_state)
- # Replace contents associated by content key.
- #
- # :param key: Content key to replace for
- # :type key: str
- # :param contents: New contents
- # :type contents: any
- # :raises KeyError: Unknown content key
def replace(self, key, contents):
+ """Replace contents associated by content key.
+
+ :param key: Content key to replace for
+ :type key: str
+ :param contents: New contents
+ :type contents: any
+ :raises KeyError: Unknown content key
+ """
self.state(key)
self.__contents[key] = contents