shep

Multi-state key stores using bit masks for python3
git clone git://git.defalsify.org/shep.git
Log | Files | Refs | LICENSE

commit 10fdb77c9485445f9e981e6def7279e161acb107
parent 9ad005ae420aab43253df8611406cb18bc0a8657
Author: lash <dev@holbrook.no>
Date:   Fri, 11 Mar 2022 12:01:56 +0000

Add change method

Diffstat:
MCHANGELOG | 1+
Mshep/persist.py | 24++++++++++++++++++++++--
Mshep/state.py | 26++++++++++++++++++++++++--
Mtests/test_file.py | 36+++++++++++++++++++++++++++++++++++-
Mtests/test_state.py | 30++++++++++++++++++++++++++++++
5 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,5 +1,6 @@ - 0.1.1 * Add optional, pluggable verifier to protect state transition + * Add change method for atomic simultaneous set and unset - 0.1.0 * Release version bump - 0.0.19: diff --git a/shep/persist.py b/shep/persist.py @@ -14,8 +14,8 @@ class PersistedState(State): :type logger: object """ - def __init__(self, factory, bits, logger=None): - super(PersistedState, self).__init__(bits, logger=logger) + def __init__(self, factory, bits, logger=None, verifier=None): + super(PersistedState, self).__init__(bits, logger=logger, verifier=verifier) self.__store_factory = factory self.__stores = {} @@ -78,6 +78,26 @@ class PersistedState(State): return to_state + def change(self, key, bits_set, bits_unset): + """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) + + to_state = super(PersistedState, self).change(key, bits_set, bits_unset) + + k_to = self.name(to_state) + self.__ensure_store(k_to) + + contents = self.__stores[k_from].get(key) + self.__stores[k_to].add(key, contents) + self.__stores[k_from].remove(key) + + return to_state + + def move(self, key, to_state): """Persist a new state for a key or key/content. diff --git a/shep/state.py b/shep/state.py @@ -332,7 +332,7 @@ class State: if self.verifier != None: r = self.verifier(self, from_state, to_state) if r != None: - raise StateTransitionInvalid('{} -> {}: {}'.format(from_state, to_state, r)) + raise StateTransitionInvalid(r) self.__add_state_list(to_state, key) current_state_list.pop(idx) @@ -367,7 +367,7 @@ class State: return self.__move(key, current_state, to_state) - + def unset(self, key, not_state): """Unset a single bit, moving to a pure or alias state. @@ -404,6 +404,28 @@ class State: return self.__move(key, current_state, to_state) + def change(self, key, sets, unsets): + current_state = self.__keys_reverse.get(key) + if current_state == None: + raise StateItemNotFound(key) + to_state = current_state | sets + to_state &= ~unsets & self.__limit + + if sets == 0: + to_state = current_state & (~unsets) + if to_state == current_state: + raise ValueError('invalid change by unsets for state {}: {}'.format(key, unsets)) + + if to_state == getattr(self, self.base_state_name): + raise ValueError('State {} for {} cannot be reverted to {}'.format(current_state, key, self.base_state_name)) + + new_state = self.__reverse.get(to_state) + if new_state == None: + raise StateInvalid('resulting to state is unknown: {}'.format(to_state)) + + return self.__move(key, current_state, to_state) + + def state(self, key): """Return the current numeric state for the given content key. diff --git a/tests/test_file.py b/tests/test_file.py @@ -73,7 +73,41 @@ class TestStateReport(unittest.TestCase): with self.assertRaises(FileNotFoundError): os.stat(fp) - + + def test_change(self): + self.states.alias('inky', self.states.FOO | self.states.BAR) + self.states.put('abcd', state=self.states.FOO, contents='foo') + self.states.change('abcd', self.states.BAR, 0) + + fp = os.path.join(self.d, 'INKY', 'abcd') + f = open(fp, 'r') + v = f.read() + f.close() + + fp = os.path.join(self.d, 'FOO', 'abcd') + with self.assertRaises(FileNotFoundError): + os.stat(fp) + + fp = os.path.join(self.d, 'BAR', 'abcd') + with self.assertRaises(FileNotFoundError): + os.stat(fp) + + self.states.change('abcd', 0, self.states.BAR) + + fp = os.path.join(self.d, 'FOO', 'abcd') + f = open(fp, 'r') + v = f.read() + f.close() + + fp = os.path.join(self.d, 'INKY', 'abcd') + with self.assertRaises(FileNotFoundError): + os.stat(fp) + + fp = os.path.join(self.d, 'BAR', 'abcd') + with self.assertRaises(FileNotFoundError): + os.stat(fp) + + def test_set(self): self.states.alias('xyzzy', self.states.FOO | self.states.BAR) self.states.put('abcd', state=self.states.FOO, contents='foo') diff --git a/tests/test_state.py b/tests/test_state.py @@ -106,5 +106,35 @@ class TestState(unittest.TestCase): self.assertEqual(states.from_name('foo'), states.FOO) + + def test_change(self): + states = State(3) + states.add('foo') + states.add('bar') + states.add('baz') + states.alias('inky', states.FOO | states.BAR) + states.alias('pinky', states.FOO | states.BAZ) + states.put('abcd') + states.next('abcd') + states.set('abcd', states.BAR) + states.change('abcd', states.BAZ, states.BAR) + self.assertEqual(states.state('abcd'), states.PINKY) + + + def test_change_onezero(self): + states = State(3) + states.add('foo') + states.add('bar') + states.add('baz') + states.alias('inky', states.FOO | states.BAR) + states.alias('pinky', states.FOO | states.BAZ) + states.put('abcd') + states.next('abcd') + states.change('abcd', states.BAR, 0) + self.assertEqual(states.state('abcd'), states.INKY) + states.change('abcd', 0, states.BAR) + self.assertEqual(states.state('abcd'), states.FOO) + + if __name__ == '__main__': unittest.main()