From 9f71401bb5073e0c610ff20bdb594b4eb7e0bde0 Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 9 Feb 2022 19:20:59 +0000 Subject: [PATCH 01/18] Kanban example --- example/kanban.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++ shep/state.py | 31 +++++++++++++++-------- 2 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 example/kanban.py diff --git a/example/kanban.py b/example/kanban.py new file mode 100644 index 0000000..a6f1026 --- /dev/null +++ b/example/kanban.py @@ -0,0 +1,64 @@ +from shep.state import State + + +# we don't like "NEW" as the default label for a new item in the queue, so we change it to BACKLOG +State.set_default_state('backlog') + +# define all the valid states +st = State(5) +st.add('pending') +st.add('blocked') +st.add('doing') +st.add('review') +st.add('finished') + +# define a couple of states that give a bit more context to progress; something is blocked before starting development or something is blocked during development... +st.alias('startblock', st.BLOCKED, st.PENDING) +st.alias('doingblock', st.BLOCKED, st.DOING) + + +# create the foo key which will forever languish in backlog +k = 'foo' +st.put(k) +foo_state = st.state(k) +foo_state_name = st.name(foo_state) +foo_contents_r = st.get('foo') +print('{} {} {}'.format(k, foo_state_name, foo_contents_r)) + + +# Create bar->baz and advance it from backlog to pending +k = 'bar' +bar_contents = 'baz' +st.put(k, contents=bar_contents) + +st.next(k) +bar_state = st.state(k) +bar_state_name = st.name(bar_state) +bar_contents_r = st.get('bar') +print('{} {} {}'.format(k, bar_state_name, bar_contents_r)) + +# Create inky->pinky and move to doing then doing-blocked +k = 'inky' +inky_contents = 'pinky' +st.put(k, contents=inky_contents) +inky_state = st.state(k) +st.move(k, st.DOING) +st.set(k, st.BLOCKED) +inky_state = st.state(k) +inky_state_name = st.name(inky_state) +inky_contents_r = st.get('inky') +print('{} {} {}'.format(k, inky_state_name, bar_contents_r)) + +# then replace the content +# note that replace could potentially mean some VCS below +inky_new_contents = 'blinky' +st.replace(k, inky_new_contents) +inky_contents_r = st.get('inky') +print('{} {} {}'.format(k, inky_state_name, inky_contents_r)) + +# so now move to review +st.move(k, st.REVIEW) +inky_state = st.state(k) +inky_state_name = st.name(inky_state) +print('{} {} {}'.format(k, inky_state_name, inky_contents_r)) + diff --git a/shep/state.py b/shep/state.py index c9c0ccd..23d599e 100644 --- a/shep/state.py +++ b/shep/state.py @@ -19,18 +19,27 @@ class State: :param logger: Standard library logging instance to output to :type logger: logging.Logger """ + + base_state_name = 'NEW' + def __init__(self, bits, logger=None): self.__bits = bits self.__limit = (1 << bits) - 1 self.__c = 0 - self.NEW = 0 + setattr(self, self.base_state_name, 0) + #self.NEW = 0 - self.__reverse = {0: self.NEW} - self.__keys = {self.NEW: []} + self.__reverse = {0: getattr(self, self.base_state_name)} + self.__keys = {getattr(self, self.base_state_name): []} self.__keys_reverse = {} self.__contents = {} + @classmethod + def set_default_state(cls, state_name): + cls.base_state_name = state_name.upper() + + # return true if v is a single-bit state def __is_pure(self, v): if v == 0: @@ -197,7 +206,7 @@ class State: :return: State name """ if v == None or v == 0: - return 'NEW' + return self.base_state_name k = self.__reverse.get(v) if k == None: raise StateInvalid(v) @@ -252,13 +261,13 @@ class 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. + If no state it specified, the default state attribute State.base_state_name 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 + :param state: Initial state for the put. If not given, initial state will be State.base_state_name :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 @@ -268,7 +277,7 @@ class State: :return: Resulting state that key is put under (should match the input state) """ if state == None: - state = self.NEW + state = getattr(self, self.base_state_name) elif self.__reverse.get(state) == None: raise StateInvalid(state) self.__check_key(key) @@ -351,13 +360,13 @@ class 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). + The resulting state cannot be State.base_state_name (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 ValueError: State is not a single bit state, or attempts to revert to State.base_state_name :raises StateItemNotFound: Content key is not registered :raises StateInvalid: Resulting state after addition of atomic state is unknown :rtype: int @@ -374,8 +383,8 @@ class State: if to_state == current_state: raise ValueError('invalid change for state {}: {}'.format(key, not_state)) - if to_state == self.NEW: - raise ValueError('State {} for {} cannot be reverted to NEW'.format(current_state, key)) + 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: From 9ad005ae420aab43253df8611406cb18bc0a8657 Mon Sep 17 00:00:00 2001 From: lash Date: Fri, 11 Mar 2022 10:31:08 +0000 Subject: [PATCH 02/18] Add verifier --- CHANGELOG | 4 ++++ setup.cfg | 2 +- shep/error.py | 6 ++++++ shep/state.py | 19 +++++++++++++++---- shep/verify.py | 2 ++ tests/test_state.py | 1 - 6 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 shep/verify.py diff --git a/CHANGELOG b/CHANGELOG index 038b1d8..ab52a6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +- 0.1.1 + * Add optional, pluggable verifier to protect state transition +- 0.1.0 + * Release version bump - 0.0.19: * Enable alias with comma separated values - 0.0.18 diff --git a/setup.cfg b/setup.cfg index 43b0318..663c5ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = shep -version = 0.1.0rc1 +version = 0.1.1rc1 description = Multi-state key stores using bit masks author = Louis Holbrook author_email = dev@holbrook.no diff --git a/shep/error.py b/shep/error.py index bd4beb9..74223bf 100644 --- a/shep/error.py +++ b/shep/error.py @@ -26,3 +26,9 @@ class StateCorruptionError(RuntimeError): """An irrecoverable discrepancy between persisted state and memory state has occurred. """ pass + + +class StateTransitionInvalid(Exception): + """Raised if state transition verification fails + """ + pass diff --git a/shep/state.py b/shep/state.py index 23d599e..1e00ede 100644 --- a/shep/state.py +++ b/shep/state.py @@ -1,12 +1,18 @@ +# standard imports +import re + # local imports from shep.error import ( StateExists, StateInvalid, StateItemExists, StateItemNotFound, + StateTransitionInvalid, ) +re_name = r'^[a-zA-Z_]+$' + class State: """State is an in-memory bitmasked state store for key-value pairs, or even just keys alone. @@ -22,17 +28,17 @@ class State: base_state_name = 'NEW' - def __init__(self, bits, logger=None): + def __init__(self, bits, logger=None, verifier=None): self.__bits = bits self.__limit = (1 << bits) - 1 self.__c = 0 setattr(self, self.base_state_name, 0) - #self.NEW = 0 self.__reverse = {0: getattr(self, self.base_state_name)} self.__keys = {getattr(self, self.base_state_name): []} self.__keys_reverse = {} self.__contents = {} + self.verifier = verifier @classmethod @@ -54,8 +60,8 @@ class State: # validates a state name and return its canonical representation def __check_name_valid(self, k): - if not k.isalpha(): - raise ValueError('only alpha') + if not re.match(re_name, k): + raise ValueError('only alpha and underscore') return k.upper() @@ -323,6 +329,11 @@ class State: if current_state_list == None: raise StateCorruptionError(to_state) + if self.verifier != None: + r = self.verifier(self, from_state, to_state) + if r != None: + raise StateTransitionInvalid('{} -> {}: {}'.format(from_state, to_state, r)) + self.__add_state_list(to_state, key) current_state_list.pop(idx) diff --git a/shep/verify.py b/shep/verify.py new file mode 100644 index 0000000..a238932 --- /dev/null +++ b/shep/verify.py @@ -0,0 +1,2 @@ +def default_checker(statestore, old, new): + return None diff --git a/tests/test_state.py b/tests/test_state.py index da7a06c..b5de3c6 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -18,7 +18,6 @@ class TestState(unittest.TestCase): for k in [ 'f0o', 'f oo', - 'f_oo', ]: with self.assertRaises(ValueError): states.add(k) From 10fdb77c9485445f9e981e6def7279e161acb107 Mon Sep 17 00:00:00 2001 From: lash Date: Fri, 11 Mar 2022 12:01:56 +0000 Subject: [PATCH 03/18] Add change method --- CHANGELOG | 1 + shep/persist.py | 24 ++++++++++++++++++++++-- shep/state.py | 26 ++++++++++++++++++++++++-- tests/test_file.py | 36 +++++++++++++++++++++++++++++++++++- tests/test_state.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ab52a6f..4fdbd30 100644 --- 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 index 60a5b92..9358bfb 100644 --- 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 index 1e00ede..02e40ea 100644 --- 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 index 71388ed..f0e1c24 100644 --- 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 index b5de3c6..f4a3acf 100644 --- 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() From b92a4e64583002a13ad9d30e7c21f7a0a5f49546 Mon Sep 17 00:00:00 2001 From: lash Date: Fri, 11 Mar 2022 19:36:24 +0000 Subject: [PATCH 04/18] Add persistent pure state indexes --- shep/state.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/shep/state.py b/shep/state.py index 02e40ea..a355c93 100644 --- a/shep/state.py +++ b/shep/state.py @@ -8,6 +8,7 @@ from shep.error import ( StateItemExists, StateItemNotFound, StateTransitionInvalid, + StateCorruptionError, ) @@ -129,7 +130,17 @@ class State: def __add_state_list(self, state, item): if self.__keys.get(state) == None: self.__keys[state] = [] - self.__keys[state].append(item) + if not self.__is_pure(state) or state == 0: + self.__keys[state].append(item) + c = 1 + import sys + for i in range(self.__bits): + part = c & state + if part > 0: + if self.__keys.get(part) == None: + self.__keys[part] = [] + self.__keys[part].append(item) + c <<= 1 self.__keys_reverse[item] = state @@ -334,8 +345,8 @@ class State: if r != None: raise StateTransitionInvalid(r) - self.__add_state_list(to_state, key) current_state_list.pop(idx) + self.__add_state_list(to_state, key) return to_state From d68286ee6ca7366ba80fa7af95198016369ea9bb Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 13 Mar 2022 16:36:17 +0000 Subject: [PATCH 05/18] Add modify dates handler --- shep/state.py | 14 ++++++++++++++ shep/store/file.py | 10 ++++++++++ tests/test_state.py | 16 ++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/shep/state.py b/shep/state.py index a355c93..382ba07 100644 --- a/shep/state.py +++ b/shep/state.py @@ -1,5 +1,6 @@ # standard imports import re +import datetime # local imports from shep.error import ( @@ -39,6 +40,7 @@ class State: self.__keys = {getattr(self, self.base_state_name): []} self.__keys_reverse = {} self.__contents = {} + self.__change = {} self.verifier = verifier @@ -302,6 +304,8 @@ class State: if contents != None: self.__contents[key] = contents + self.register_modify(key) + return state @@ -348,6 +352,8 @@ class State: current_state_list.pop(idx) self.__add_state_list(to_state, key) + self.register_modify(key) + return to_state @@ -549,3 +555,11 @@ class State: """ self.state(key) self.__contents[key] = contents + + + def modified(self, key): + return self.__change[key] + + + def register_modify(self, key): + self.__change[key] = datetime.datetime.now().timestamp() diff --git a/shep/store/file.py b/shep/store/file.py index f9b84d9..9dce8af 100644 --- a/shep/store/file.py +++ b/shep/store/file.py @@ -103,6 +103,16 @@ class SimpleFileStore: f.close() + def modified(self, k): + path = self.path(k) + st = os.stat(path) + return float(st.st_ctime()) + + + def register_modify(self, k): + pass + + class SimpleFileStoreFactory: """Provide a method to instantiate SimpleFileStore instances that provide persistence for individual states. diff --git a/tests/test_state.py b/tests/test_state.py index f4a3acf..36e1e05 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -136,5 +136,21 @@ class TestState(unittest.TestCase): self.assertEqual(states.state('abcd'), states.FOO) + def test_change_dates(self): + states = State(3) + states.add('foo') + states.put('abcd') + states.put('bcde') + + a = states.modified('abcd') + b = states.modified('bcde') + self.assertGreater(b, a) + + states.set('abcd', states.FOO) + a = states.modified('abcd') + b = states.modified('bcde') + self.assertGreater(a, b) + + if __name__ == '__main__': unittest.main() From af8ce95e22407bc22082fffecd3cf125dd833abc Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 16 Mar 2022 16:49:00 +0000 Subject: [PATCH 06/18] Optional allow undefined alias states --- CHANGELOG | 5 +++-- shep/persist.py | 13 +++++++++++++ shep/state.py | 33 ++++++++++++++++++++++++--------- shep/store/file.py | 2 +- tests/test_state.py | 29 +++++++++++++++++++++++++++-- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4fdbd30..9068181 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ - 0.1.1 - * Add optional, pluggable verifier to protect state transition - * Add change method for atomic simultaneous set and unset + * Optional, pluggable verifier to protect state transition + * Change method for atomic simultaneous set and unset + * Optionally allow undefined composite states - 0.1.0 * Release version bump - 0.0.19: diff --git a/shep/persist.py b/shep/persist.py index 9358bfb..6765a45 100644 --- a/shep/persist.py +++ b/shep/persist.py @@ -1,3 +1,6 @@ +# standard imports +import datetime + # local imports from .state import State from .error import StateItemExists @@ -95,6 +98,8 @@ class PersistedState(State): self.__stores[k_to].add(key, contents) self.__stores[k_from].remove(key) + self.register_modify(key) + return to_state @@ -119,6 +124,8 @@ class PersistedState(State): self.__stores[k_to].add(key, contents) self.__stores[k_from].remove(key) + self.register_modify(key) + return to_state @@ -192,3 +199,9 @@ class PersistedState(State): state = self.state(key) k = self.name(state) return self.__stores[k].replace(key, contents) + + + def modified(self, key): + state = self.state(key) + k = self.name(state) + return self.__stores[k].modified(key) diff --git a/shep/state.py b/shep/state.py index 382ba07..19dceeb 100644 --- a/shep/state.py +++ b/shep/state.py @@ -30,7 +30,7 @@ class State: base_state_name = 'NEW' - def __init__(self, bits, logger=None, verifier=None): + def __init__(self, bits, logger=None, verifier=None, check_alias=True): self.__bits = bits self.__limit = (1 << bits) - 1 self.__c = 0 @@ -40,8 +40,9 @@ class State: self.__keys = {getattr(self, self.base_state_name): []} self.__keys_reverse = {} self.__contents = {} - self.__change = {} + self.modified_last = {} self.verifier = verifier + self.check_alias = check_alias @classmethod @@ -135,7 +136,6 @@ class State: if not self.__is_pure(state) or state == 0: self.__keys[state].append(item) c = 1 - import sys for i in range(self.__bits): part = c & state if part > 0: @@ -215,6 +215,18 @@ class State: return l + def elements(self, v): + r = [] + if v == None or v == 0: + return self.base_state_name + c = 1 + for i in range(1, self.__bits): + if v & c > 0: + r.append(self.name(c)) + c <<= 1 + return '*' + ','.join(r) + + def name(self, v): """Retrieve that string representation of the state attribute represented by the given state integer value. @@ -224,11 +236,14 @@ class State: :rtype: str :return: State name """ - if v == None or v == 0: - return self.base_state_name k = self.__reverse.get(v) if k == None: - raise StateInvalid(v) + if self.check_alias: + raise StateInvalid(v) + else: + k = self.elements(v) + elif v == None or v == 0: + return self.base_state_name return k @@ -379,7 +394,7 @@ class State: to_state = current_state | or_state new_state = self.__reverse.get(to_state) - if new_state == None: + if new_state == None and self.check_alias: raise StateInvalid('resulting to state is unknown: {}'.format(to_state)) return self.__move(key, current_state, to_state) @@ -558,8 +573,8 @@ class State: def modified(self, key): - return self.__change[key] + return self.modified_last[key] def register_modify(self, key): - self.__change[key] = datetime.datetime.now().timestamp() + self.modified_last[key] = datetime.datetime.now().timestamp() diff --git a/shep/store/file.py b/shep/store/file.py index 9dce8af..fc971a8 100644 --- a/shep/store/file.py +++ b/shep/store/file.py @@ -106,7 +106,7 @@ class SimpleFileStore: def modified(self, k): path = self.path(k) st = os.stat(path) - return float(st.st_ctime()) + return st.st_ctime def register_modify(self, k): diff --git a/tests/test_state.py b/tests/test_state.py index 36e1e05..b2f4693 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,5 +1,6 @@ # standard imports import unittest +import logging # local imports from shep import State @@ -8,6 +9,9 @@ from shep.error import ( StateInvalid, ) +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + class TestState(unittest.TestCase): @@ -81,7 +85,29 @@ class TestState(unittest.TestCase): states.add('bar') with self.assertRaises(StateInvalid): states.alias('baz', 5) - + + + def test_alias_invalid(self): + states = State(3) + states.add('foo') + states.add('bar') + states.put('abcd') + states.set('abcd', states.FOO) + with self.assertRaises(StateInvalid): + states.set('abcd', states.BAR) + + + def test_alias_invalid_ignore(self): + states = State(3, check_alias=False) + states.add('foo') + states.add('bar') + states.put('abcd') + states.set('abcd', states.FOO) + states.set('abcd', states.BAR) + v = states.state('abcd') + s = states.name(v) + self.assertEqual(s, '*FOO,BAR') + def test_peek(self): states = State(3) @@ -106,7 +132,6 @@ class TestState(unittest.TestCase): self.assertEqual(states.from_name('foo'), states.FOO) - def test_change(self): states = State(3) states.add('foo') From 798262f00f3af1cd59320ae489274f1cf999d099 Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 16 Mar 2022 17:13:05 +0000 Subject: [PATCH 07/18] State change event emitter --- shep/state.py | 9 ++++++++- tests/test_state.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/shep/state.py b/shep/state.py index 19dceeb..e4ae551 100644 --- a/shep/state.py +++ b/shep/state.py @@ -30,7 +30,7 @@ class State: base_state_name = 'NEW' - def __init__(self, bits, logger=None, verifier=None, check_alias=True): + def __init__(self, bits, logger=None, verifier=None, check_alias=True, event_callback=None): self.__bits = bits self.__limit = (1 << bits) - 1 self.__c = 0 @@ -43,6 +43,7 @@ class State: self.modified_last = {} self.verifier = verifier self.check_alias = check_alias + self.event_callback = event_callback @classmethod @@ -320,6 +321,9 @@ class State: self.__contents[key] = contents self.register_modify(key) + + if self.event_callback != None: + self.event_callback(key, state) return state @@ -369,6 +373,9 @@ class State: self.register_modify(key) + if self.event_callback != None: + self.event_callback(key, to_state) + return to_state diff --git a/tests/test_state.py b/tests/test_state.py index b2f4693..d89470f 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -13,6 +13,18 @@ logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() +class MockCallback: + + def __init__(self): + self.items = {} + + + def add(self, k, v): + if self.items.get(k) == None: + self.items[k] = [] + self.items[k].append(v) + + class TestState(unittest.TestCase): def test_key_check(self): @@ -177,5 +189,24 @@ class TestState(unittest.TestCase): self.assertGreater(a, b) + def test_event_callback(self): + cb = MockCallback() + states = State(3, event_callback=cb.add) + states.add('foo') + states.add('bar') + states.add('baz') + states.alias('xyzzy', states.FOO | states.BAR) + states.put('abcd') + states.set('abcd', states.FOO) + states.set('abcd', states.BAR) + states.change('abcd', states.BAZ, states.XYZZY) + events = cb.items['abcd'] + self.assertEqual(len(events), 4) + self.assertEqual(events[0], states.NEW) + self.assertEqual(events[1], states.FOO) + self.assertEqual(events[2], states.XYZZY) + self.assertEqual(events[3], states.BAZ) + + if __name__ == '__main__': unittest.main() From 57a9ea44ff34fc5a58aecfff1f8e86e7415748ca Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 16 Mar 2022 19:25:29 +0000 Subject: [PATCH 08/18] Dynamic state --- CHANGELOG | 1 + shep/state.py | 4 ++++ tests/test_state.py | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9068181..48358b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ * Optional, pluggable verifier to protect state transition * Change method for atomic simultaneous set and unset * Optionally allow undefined composite states + * Dynamic bits - 0.1.0 * Release version bump - 0.0.19: diff --git a/shep/state.py b/shep/state.py index e4ae551..86a9b93 100644 --- a/shep/state.py +++ b/shep/state.py @@ -31,6 +31,7 @@ class State: base_state_name = 'NEW' def __init__(self, bits, logger=None, verifier=None, check_alias=True, event_callback=None): + self.__initial_bits = bits self.__bits = bits self.__limit = (1 << bits) - 1 self.__c = 0 @@ -92,6 +93,9 @@ class State: # enforces state value within bit limit of instantiation def __check_limit(self, v): + if self.__initial_bits == 0: + self.__bits += 1 + self.__limit = (1 << self.__bits) - 1 if v > self.__limit: raise OverflowError(v) return v diff --git a/tests/test_state.py b/tests/test_state.py index d89470f..e80db73 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -208,5 +208,12 @@ class TestState(unittest.TestCase): self.assertEqual(events[3], states.BAZ) + def test_dynamic(self): + states = State(0) + states.add('foo') + states.add('bar') + states.alias('baz', states.FOO | states.BAR) + + if __name__ == '__main__': unittest.main() From 8ccc89b4a538bc351fe25edea74158ed06fc0149 Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 17 Mar 2022 19:16:33 +0000 Subject: [PATCH 09/18] Binary content option, sync all option --- CHANGELOG | 2 ++ shep/persist.py | 24 ++++++++++++++++-------- shep/state.py | 2 +- shep/store/file.py | 21 +++++++++++++-------- tests/test_file.py | 21 ++++++++++++++++++++- tests/test_store.py | 1 + 6 files changed, 53 insertions(+), 18 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 48358b4..7fa5ba1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,8 @@ * Change method for atomic simultaneous set and unset * Optionally allow undefined composite states * Dynamic bits + * Optional binary contents + * Sync all if no state passed as argument - 0.1.0 * Release version bump - 0.0.19: diff --git a/shep/persist.py b/shep/persist.py index 6765a45..ac82d55 100644 --- a/shep/persist.py +++ b/shep/persist.py @@ -129,7 +129,7 @@ class PersistedState(State): return to_state - def sync(self, state): + def sync(self, state=None): """Reload resources for a single state in memory from the persisted state store. :param state: State to load @@ -137,16 +137,24 @@ class PersistedState(State): :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) + states = [] + if state == None: + states = list(self.all()) + else: + states = [self.name(state)] - self.__ensure_store(k) + ks = [] + for k in states: + ks.append(k) - for o in self.__stores[k].list(): + for k in ks: self.__ensure_store(k) - try: - super(PersistedState, self).put(o[0], state=state, contents=o[1]) - except StateItemExists: - pass + for o in self.__stores[k].list(): + state = self.from_name(k) + try: + super(PersistedState, self).put(o[0], state=state, contents=o[1]) + except StateItemExists as e: + pass def list(self, state): diff --git a/shep/state.py b/shep/state.py index 86a9b93..8b216f1 100644 --- a/shep/state.py +++ b/shep/state.py @@ -317,7 +317,7 @@ class State: """ if state == None: state = getattr(self, self.base_state_name) - elif self.__reverse.get(state) == None: + elif self.__reverse.get(state) == None and self.check_alias: raise StateInvalid(state) self.__check_key(key) self.__add_state_list(state, key) diff --git a/shep/store/file.py b/shep/store/file.py index fc971a8..854dc70 100644 --- a/shep/store/file.py +++ b/shep/store/file.py @@ -8,10 +8,14 @@ class SimpleFileStore: :param path: Filesystem base path for all state directory :type path: str """ - def __init__(self, path): + def __init__(self, path, binary=False): self.__path = path os.makedirs(self.__path, exist_ok=True) - + if binary: + self.__m = ['rb', 'wb'] + else: + self.__m = ['r', 'w'] + def add(self, k, contents=None): """Add a new key and optional contents @@ -25,7 +29,7 @@ class SimpleFileStore: if contents == None: contents = '' - f = open(fp, 'w') + f = open(fp, self.__m[1]) f.write(contents) f.close() @@ -51,7 +55,7 @@ class SimpleFileStore: :return: Contents """ fp = os.path.join(self.__path, k) - f = open(fp, 'r') + f = open(fp, self.__m[0]) r = f.read() f.close() return r @@ -66,7 +70,7 @@ class SimpleFileStore: files = [] for p in os.listdir(self.__path): fp = os.path.join(self.__path, p) - f = open(fp, 'r') + f = open(fp, self.__m[0]) r = f.read() f.close() if len(r) == 0: @@ -98,7 +102,7 @@ class SimpleFileStore: """ fp = os.path.join(self.__path, k) os.stat(fp) - f = open(fp, 'w') + f = open(fp, self.__m[1]) r = f.write(contents) f.close() @@ -119,8 +123,9 @@ class SimpleFileStoreFactory: :param path: Filesystem path as base path for states :type path: str """ - def __init__(self, path): + def __init__(self, path, binary=False): self.__path = path + self.__binary = binary def add(self, k): @@ -133,4 +138,4 @@ class SimpleFileStoreFactory: """ k = str(k) store_path = os.path.join(self.__path, k) - return SimpleFileStore(store_path) + return SimpleFileStore(store_path, binary=self.__binary) diff --git a/tests/test_file.py b/tests/test_file.py index f0e1c24..79b900f 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -142,7 +142,7 @@ class TestStateReport(unittest.TestCase): os.stat(fp) - def test_sync(self): + def test_sync_one(self): self.states.put('abcd', state=self.states.FOO, contents='foo') self.states.put('xxx', state=self.states.FOO) self.states.put('yyy', state=self.states.FOO) @@ -162,6 +162,25 @@ class TestStateReport(unittest.TestCase): self.assertEqual(self.states.get('zzzz'), 'xyzzy') + def test_sync_all(self): + self.states.put('abcd', state=self.states.FOO) + self.states.put('xxx', state=self.states.BAR) + + fp = os.path.join(self.d, 'FOO', 'abcd') + f = open(fp, 'w') + f.write('foofoo') + f.close() + + fp = os.path.join(self.d, 'BAR', 'zzzz') + f = open(fp, 'w') + f.write('barbar') + f.close() + + self.states.sync() + self.assertEqual(self.states.get('abcd'), None) + self.assertEqual(self.states.get('zzzz'), 'barbar') + + def test_path(self): self.states.put('yyy', state=self.states.FOO) diff --git a/tests/test_store.py b/tests/test_store.py index 182b639..185f25c 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -80,5 +80,6 @@ class TestStateItems(unittest.TestCase): self.assertIsNone(self.mockstore.v.get(item)) + if __name__ == '__main__': unittest.main() From 2356ebc08f221d87131085941d807f17594ed6fd Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 17 Mar 2022 21:36:07 +0000 Subject: [PATCH 10/18] Pure-only all, faulty peek check, update persist init --- shep/persist.py | 4 ++-- shep/state.py | 10 +++++++--- shep/store/file.py | 5 ++++- tests/test_file.py | 5 ++++- tests/test_state.py | 9 +++++---- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/shep/persist.py b/shep/persist.py index ac82d55..b814f18 100644 --- a/shep/persist.py +++ b/shep/persist.py @@ -17,8 +17,8 @@ class PersistedState(State): :type logger: object """ - def __init__(self, factory, bits, logger=None, verifier=None): - super(PersistedState, self).__init__(bits, logger=logger, verifier=verifier) + def __init__(self, factory, bits, logger=None, verifier=None, check_alias=True, event_callback=None): + super(PersistedState, self).__init__(bits, logger=logger, verifier=verifier, check_alias=check_alias, event_callback=event_callback) self.__store_factory = factory self.__stores = {} diff --git a/shep/state.py b/shep/state.py index 8b216f1..8879eff 100644 --- a/shep/state.py +++ b/shep/state.py @@ -203,7 +203,7 @@ class State: self.__set(k, v) - def all(self): + def all(self, pure=False): """Return list of all unique atomic and alias states. :rtype: list of ints @@ -215,6 +215,10 @@ class State: continue if k.upper() != k: continue + if pure: + state = self.from_name(k) + if not self.__is_pure(state): + continue l.append(k) l.sort() return l @@ -349,7 +353,7 @@ class State: raise StateItemNotFound(key) new_state = self.__reverse.get(to_state) - if new_state == None: + if new_state == None and self.check_alias: raise StateInvalid(to_state) return self.__move(key, current_state, to_state) @@ -549,7 +553,7 @@ class State: state = 1 else: state <<= 1 - if state > self.__c: + if state > self.__limit: raise StateInvalid('unknown state {}'.format(state)) return state diff --git a/shep/store/file.py b/shep/store/file.py index 854dc70..4bc99a9 100644 --- a/shep/store/file.py +++ b/shep/store/file.py @@ -27,7 +27,10 @@ class SimpleFileStore: """ fp = os.path.join(self.__path, k) if contents == None: - contents = '' + if self.__m[1] == 'wb': + contents = b'' + else: + contents = '' f = open(fp, self.__m[1]) f.write(contents) diff --git a/tests/test_file.py b/tests/test_file.py index 79b900f..025e6da 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -200,6 +200,9 @@ class TestStateReport(unittest.TestCase): self.states.next('abcd') self.assertEqual(self.states.state('abcd'), self.states.BAR) + self.states.next('abcd') + self.assertEqual(self.states.state('abcd'), self.states.BAZ) + with self.assertRaises(StateInvalid): self.states.next('abcd') @@ -207,7 +210,7 @@ class TestStateReport(unittest.TestCase): with self.assertRaises(FileNotFoundError): os.stat(fp) - fp = os.path.join(self.d, 'BAR', 'abcd') + fp = os.path.join(self.d, 'BAZ', 'abcd') os.stat(fp) diff --git a/tests/test_state.py b/tests/test_state.py index e80db73..cf24c37 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -48,11 +48,12 @@ class TestState(unittest.TestCase): def test_limit(self): - states = State(2) + states = State(3) states.add('foo') states.add('bar') + states.add('baz') with self.assertRaises(OverflowError): - states.add('baz') + states.add('gaz') def test_dup(self): @@ -122,7 +123,7 @@ class TestState(unittest.TestCase): def test_peek(self): - states = State(3) + states = State(2) states.add('foo') states.add('bar') @@ -135,7 +136,7 @@ class TestState(unittest.TestCase): states.move('abcd', states.BAR) with self.assertRaises(StateInvalid): - self.assertEqual(states.peek('abcd')) + states.peek('abcd') def test_from_name(self): From 6680b897b3c7c3c69e9408748a3021353dee74d3 Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 17 Mar 2022 22:01:51 +0000 Subject: [PATCH 11/18] Optional sync state on base state object --- shep/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shep/state.py b/shep/state.py index 8879eff..c4bbaf2 100644 --- a/shep/state.py +++ b/shep/state.py @@ -513,7 +513,7 @@ class State: return [] - def sync(self, state): + def sync(self, state=None): """Noop method for interface implementation providing sync to backend. :param state: State to sync. From 2beeb4c725d2d6eedd93c09502c52f475f21a166 Mon Sep 17 00:00:00 2001 From: lash Date: Fri, 18 Mar 2022 18:34:49 +0000 Subject: [PATCH 12/18] Add mask --- CHANGELOG | 1 + shep/state.py | 19 ++++++++++++++----- tests/test_state.py | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7fa5ba1..6f82d60 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ * Dynamic bits * Optional binary contents * Sync all if no state passed as argument + * Mask method for client-side state manipulation - 0.1.0 * Release version bump - 0.0.19: diff --git a/shep/state.py b/shep/state.py index c4bbaf2..0ff02df 100644 --- a/shep/state.py +++ b/shep/state.py @@ -92,10 +92,11 @@ class State: # enforces state value within bit limit of instantiation - def __check_limit(self, v): - if self.__initial_bits == 0: - self.__bits += 1 - self.__limit = (1 << self.__bits) - 1 + def __check_limit(self, v, pure=True): + if pure: + if self.__initial_bits == 0: + self.__bits += 1 + self.__limit = (1 << self.__bits) - 1 if v > self.__limit: raise OverflowError(v) return v @@ -197,7 +198,7 @@ class State: v = 0 for a in args: a = self.__check_value_cursor(a) - v = self.__check_limit(v | a) + v = self.__check_limit(v | a, pure=False) if self.__is_pure(v): raise ValueError('use add to add pure values') self.__set(k, v) @@ -593,3 +594,11 @@ class State: def register_modify(self, key): self.modified_last[key] = datetime.datetime.now().timestamp() + + + def mask(self, key, states): + statemask = self.__limit + 1 + statemask |= states + statemask = ~statemask + statemask &= self.__limit + return statemask diff --git a/tests/test_state.py b/tests/test_state.py index cf24c37..eefe6ff 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -216,5 +216,26 @@ class TestState(unittest.TestCase): states.alias('baz', states.FOO | states.BAR) + + def test_mask(self): + states = State(3) + states.add('foo') + states.add('bar') + states.add('baz') + states.alias('all', states.FOO | states.BAR | states.BAZ) + mask = states.mask('xyzzy', states.FOO | states.BAZ) + self.assertEqual(mask, states.BAR) + + + def test_mask_dynamic(self): + states = State(0) + states.add('foo') + states.add('bar') + states.add('baz') + states.alias('all', states.FOO | states.BAR | states.BAZ) + mask = states.mask('xyzzy', states.FOO | states.BAZ) + self.assertEqual(mask, states.BAR) + + if __name__ == '__main__': unittest.main() From 0c76507f5fb18c1dcaee880cbcb63b1b66916055 Mon Sep 17 00:00:00 2001 From: lash Date: Wed, 23 Mar 2022 23:34:13 +0000 Subject: [PATCH 13/18] Sync unknown states in persistent store --- shep/persist.py | 4 ++++ shep/state.py | 32 ++++++++++++++++++++++++-------- shep/store/file.py | 2 +- tests/test_state.py | 10 ++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/shep/persist.py b/shep/persist.py index b814f18..466293b 100644 --- a/shep/persist.py +++ b/shep/persist.py @@ -58,6 +58,8 @@ class PersistedState(State): self.__stores[k_to].add(key, contents) self.__stores[k_from].remove(key) + self.sync(to_state) + return to_state @@ -126,6 +128,8 @@ class PersistedState(State): self.register_modify(key) + self.sync(to_state) + return to_state diff --git a/shep/state.py b/shep/state.py index 0ff02df..ebd8e68 100644 --- a/shep/state.py +++ b/shep/state.py @@ -150,6 +150,9 @@ class State: self.__keys[part].append(item) c <<= 1 self.__keys_reverse[item] = state + if self.__reverse.get(state) == None: + s = self.elements(state) + self.alias(s, state) def __state_list_index(self, item, state_list): @@ -234,7 +237,16 @@ class State: if v & c > 0: r.append(self.name(c)) c <<= 1 - return '*' + ','.join(r) + return '_' + '_'.join(r) + + + def from_elements(self, k): + r = 0 + if k[0] != '_': + raise ValueError('elements string must start with underscore (_), got {}'.format(k)) + for v in k[1:].split('_'): + r |= self.from_name(v) + return r def name(self, v): @@ -325,14 +337,16 @@ class State: elif self.__reverse.get(state) == None and self.check_alias: raise StateInvalid(state) self.__check_key(key) + + if self.event_callback != None: + old_state = self.__keys_reverse.get(key) + self.event_callback(key, 'nonexistent', self.name(state)) + self.__add_state_list(state, key) if contents != None: self.__contents[key] = contents self.register_modify(key) - - if self.event_callback != None: - self.event_callback(key, state) return state @@ -378,13 +392,15 @@ class State: raise StateTransitionInvalid(r) current_state_list.pop(idx) + + if self.event_callback != None: + old_state = self.__keys_reverse.get(key) + self.event_callback(key, self.name(old_state), self.name(to_state)) + self.__add_state_list(to_state, key) self.register_modify(key) - if self.event_callback != None: - self.event_callback(key, to_state) - return to_state @@ -596,7 +612,7 @@ class State: self.modified_last[key] = datetime.datetime.now().timestamp() - def mask(self, key, states): + def mask(self, key, states=0): statemask = self.__limit + 1 statemask |= states statemask = ~statemask diff --git a/shep/store/file.py b/shep/store/file.py index 4bc99a9..0f1b60f 100644 --- a/shep/store/file.py +++ b/shep/store/file.py @@ -15,7 +15,7 @@ class SimpleFileStore: self.__m = ['rb', 'wb'] else: self.__m = ['r', 'w'] - + def add(self, k, contents=None): """Add a new key and optional contents diff --git a/tests/test_state.py b/tests/test_state.py index eefe6ff..a55c192 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -237,5 +237,15 @@ class TestState(unittest.TestCase): self.assertEqual(mask, states.BAR) + def test_mask_zero(self): + states = State(0) + states.add('foo') + states.add('bar') + states.add('baz') + states.alias('all', states.FOO | states.BAR | states.BAZ) + mask = states.mask('xyzzy') + self.assertEqual(mask, states.ALL) + + if __name__ == '__main__': unittest.main() From bb87ba1e346829e025f3de304ee3bfd19b5101ea Mon Sep 17 00:00:00 2001 From: lash Date: Tue, 29 Mar 2022 06:22:54 +0000 Subject: [PATCH 14/18] Remove deadline --- shep/persist.py | 1 - 1 file changed, 1 deletion(-) diff --git a/shep/persist.py b/shep/persist.py index 466293b..a094a13 100644 --- a/shep/persist.py +++ b/shep/persist.py @@ -170,7 +170,6 @@ class PersistedState(State): """ k = self.name(state) self.__ensure_store(k) - #return self.__stores[k].list(state) return super(PersistedState, self).list(state) From bddf335a53858254e94e27f1d8d8964440a49361 Mon Sep 17 00:00:00 2001 From: lash Date: Tue, 29 Mar 2022 09:24:57 +0000 Subject: [PATCH 15/18] Rehabilitate broken tests --- shep/state.py | 4 ++-- tests/test_state.py | 18 ++++++++++-------- tests/test_store.py | 4 ++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/shep/state.py b/shep/state.py index ebd8e68..ea7018d 100644 --- a/shep/state.py +++ b/shep/state.py @@ -150,7 +150,7 @@ class State: self.__keys[part].append(item) c <<= 1 self.__keys_reverse[item] = state - if self.__reverse.get(state) == None: + if self.__reverse.get(state) == None and not self.check_alias: s = self.elements(state) self.alias(s, state) @@ -340,7 +340,7 @@ class State: if self.event_callback != None: old_state = self.__keys_reverse.get(key) - self.event_callback(key, 'nonexistent', self.name(state)) + self.event_callback(key, None, self.name(state)) self.__add_state_list(state, key) if contents != None: diff --git a/tests/test_state.py b/tests/test_state.py index a55c192..863aa8d 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -17,12 +17,15 @@ class MockCallback: def __init__(self): self.items = {} + self.items_from = {} - def add(self, k, v): + def add(self, k, v_from, v_to): if self.items.get(k) == None: self.items[k] = [] - self.items[k].append(v) + self.items_from[k] = [] + self.items[k].append(v_to) + self.items_from[k].append(v_from) class TestState(unittest.TestCase): @@ -119,7 +122,7 @@ class TestState(unittest.TestCase): states.set('abcd', states.BAR) v = states.state('abcd') s = states.name(v) - self.assertEqual(s, '*FOO,BAR') + self.assertEqual(s, '_FOO_BAR') def test_peek(self): @@ -203,10 +206,10 @@ class TestState(unittest.TestCase): states.change('abcd', states.BAZ, states.XYZZY) events = cb.items['abcd'] self.assertEqual(len(events), 4) - self.assertEqual(events[0], states.NEW) - self.assertEqual(events[1], states.FOO) - self.assertEqual(events[2], states.XYZZY) - self.assertEqual(events[3], states.BAZ) + self.assertEqual(states.from_name(events[0]), states.NEW) + self.assertEqual(states.from_name(events[1]), states.FOO) + self.assertEqual(states.from_name(events[2]), states.XYZZY) + self.assertEqual(states.from_name(events[3]), states.BAZ) def test_dynamic(self): @@ -216,7 +219,6 @@ class TestState(unittest.TestCase): states.alias('baz', states.FOO | states.BAR) - def test_mask(self): states = State(3) states.add('foo') diff --git a/tests/test_store.py b/tests/test_store.py index 185f25c..f062bae 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -33,6 +33,10 @@ class MockStore: return self.v[k] + def list(self): + return list(self.v.keys()) + + class TestStateItems(unittest.TestCase): def setUp(self): From 2b614b00ef2d4c48b26082e18f6996f19f45bbb0 Mon Sep 17 00:00:00 2001 From: lash Date: Tue, 29 Mar 2022 11:29:10 +0000 Subject: [PATCH 16/18] Include leftmost bit in elements generation --- shep/state.py | 2 +- tests/test_state.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/shep/state.py b/shep/state.py index ea7018d..222e59a 100644 --- a/shep/state.py +++ b/shep/state.py @@ -233,7 +233,7 @@ class State: if v == None or v == 0: return self.base_state_name c = 1 - for i in range(1, self.__bits): + for i in range(self.__bits): if v & c > 0: r.append(self.name(c)) c <<= 1 diff --git a/tests/test_state.py b/tests/test_state.py index 863aa8d..95f7627 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -117,12 +117,13 @@ class TestState(unittest.TestCase): states = State(3, check_alias=False) states.add('foo') states.add('bar') + states.add('baz') states.put('abcd') states.set('abcd', states.FOO) - states.set('abcd', states.BAR) + states.set('abcd', states.BAZ) v = states.state('abcd') s = states.name(v) - self.assertEqual(s, '_FOO_BAR') + self.assertEqual(s, '_FOO_BAZ') def test_peek(self): From 4dcd6dee39abc61d4413b7f449054b0dfa78714b Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 31 Mar 2022 08:34:51 +0000 Subject: [PATCH 17/18] Replace composite state name concat with dots --- shep/state.py | 26 +++++++++++++++----------- tests/test_state.py | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/shep/state.py b/shep/state.py index 222e59a..e6c38dd 100644 --- a/shep/state.py +++ b/shep/state.py @@ -152,7 +152,7 @@ class State: self.__keys_reverse[item] = state if self.__reverse.get(state) == None and not self.check_alias: s = self.elements(state) - self.alias(s, state) + self.__alias(s, state) def __state_list_index(self, item, state_list): @@ -183,7 +183,17 @@ class State: k = self.__check_name(k) v = self.__check_value(v) self.__set(k, v) - + + + def __alias(self, k, *args): + v = 0 + for a in args: + a = self.__check_value_cursor(a) + v = self.__check_limit(v | a, pure=False) + if self.__is_pure(v): + raise ValueError('use add to add pure values') + return self.__set(k, v) + def alias(self, k, *args): """Add an alias for a combination of states in the store. @@ -198,13 +208,7 @@ class State: :raises ValueError: Attempt to use bit value as alias """ k = self.__check_name(k) - v = 0 - for a in args: - a = self.__check_value_cursor(a) - v = self.__check_limit(v | a, pure=False) - if self.__is_pure(v): - raise ValueError('use add to add pure values') - self.__set(k, v) + return self.__alias(k, *args) def all(self, pure=False): @@ -237,14 +241,14 @@ class State: if v & c > 0: r.append(self.name(c)) c <<= 1 - return '_' + '_'.join(r) + return '_' + '.'.join(r) def from_elements(self, k): r = 0 if k[0] != '_': raise ValueError('elements string must start with underscore (_), got {}'.format(k)) - for v in k[1:].split('_'): + for v in k[1:].split('.'): r |= self.from_name(v) return r diff --git a/tests/test_state.py b/tests/test_state.py index 95f7627..f7d1f10 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -123,7 +123,7 @@ class TestState(unittest.TestCase): states.set('abcd', states.BAZ) v = states.state('abcd') s = states.name(v) - self.assertEqual(s, '_FOO_BAZ') + self.assertEqual(s, '_FOO.BAZ') def test_peek(self): From 74fcf3c959abed48cf63725a1962a1477f62644e Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 31 Mar 2022 16:00:27 +0000 Subject: [PATCH 18/18] Add dot to name check --- shep/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shep/state.py b/shep/state.py index e6c38dd..df770dc 100644 --- a/shep/state.py +++ b/shep/state.py @@ -13,7 +13,7 @@ from shep.error import ( ) -re_name = r'^[a-zA-Z_]+$' +re_name = r'^[a-zA-Z_\.]+$' class State: """State is an in-memory bitmasked state store for key-value pairs, or even just keys alone.