diff --git a/doc/devices_classes_branch.txt b/doc/devices_classes_branch.txt index 7283d0d..cec2dba 100644 --- a/doc/devices_classes_branch.txt +++ b/doc/devices_classes_branch.txt @@ -406,15 +406,20 @@ Test cases for 5 discovery variants done and work monitor_mihome works with a synthetic join and toggles switches +KVS implementation completed and all tests pass + -------------------------------------------------------------------------------- TODO NEXT -KVS - key value store - ---- PERSISTENT REGISTRY -implement and test code (write a test harness) +Knit KVS into Registry +Re-test persistent use cases with Registry_test + + + + ---- NOTIFY, UPDATE, or DATA SEQUENCE? diff --git a/src/energenie/KVS.py b/src/energenie/KVS.py index 4b9746b..45e5d80 100644 --- a/src/energenie/KVS.py +++ b/src/energenie/KVS.py @@ -4,18 +4,21 @@ from lifecycle import * +class NotPersistableError(Exception): + pass + class KVS(): """A persistent key value store""" def __init__(self, filename=None): self.filename = filename self.store = {} - def load(self, filename=None, factory_cb=None): + def load(self, filename=None, create_fn=None): """Load the whole file into an in-memory cache""" # use new filename if provided, else use existing filename if filename == None: - filename == self.filename + filename = self.filename if filename == None: raise ValueError("No filename specified") @@ -51,44 +54,44 @@ k,v = line.split("=", 1) obj[k] = v else: # is blank - self.process(command, key, obj, factory_cb) + self.process(command, key, obj, create_fn) command = None is_cmd = True self.filename = filename # remember filename if it was provided - def process(self, command, key, obj, factory_cb): + def process(self, command, key, obj, create_fn): """Process the temporary object""" m = getattr(self, command) #If command is not found? get AttributeError - that's fine - m(key, obj, factory_cb) + m(key, obj, create_fn) - def ADD(self, key, obj, factory=None): + def ADD(self, key, obj, create_fn=None): """Add a new item to the kvs""" # The ADD command processes the next type= parameter as the class name in context # all other parameters are read as strings and passed to class constructor as kwargs - if factory != None: - print("*** calling factory to turn into a class instance") + if create_fn != None: + ##print("calling create_fn to turn into a class instance") type = obj["type"] del obj["type"] # don't pass to constructor - obj = factory.get(type, **obj) + obj = create_fn(type, **obj) # If this fails, then this is an error, so just let it bubble out else: - print("*** no factory configured, just storing kvp") + pass ##print("no create_fn configured, just storing kvp") - print("*** object: %s" % obj) + ##print("object: %s" % obj) # store kvp or class instance appropriately self.store[key] = obj - def IGN(self, key, obj=None, factory=None): + def IGN(self, key, obj=None, create_fn=None): """Ignore the whole record""" # The IGN command is the same length as ADD, allowing a seek/write to change any # command into IGN without changing the file size, effectively patching the file # so that the record is deleted. pass # There is nothing to do with this command - def DEL(self, key, obj=None, factory=None): + def DEL(self, key, obj=None, create_fn=None): """Delete the key from the store""" # The DEL command deletes the rec from the store. # This is useful to build temporary objects and delete them later. @@ -103,8 +106,10 @@ self.remove(key) # patches it to an IGN record self.store[key] = value - #TODO: If this fails, just add the kv pair map as the object - obj = value.get_config() # will fail with AttributeError if this method does not exist + try: + obj = value.get_config() # will fail with AttributeError if this method does not exist + except AttributeError: + raise NotPersistableError() self.append(key, obj) def __delitem__(self, key): diff --git a/src/energenie/KVS_test.py b/src/energenie/KVS_test.py index 508993b..1a3d730 100644 --- a/src/energenie/KVS_test.py +++ b/src/energenie/KVS_test.py @@ -4,7 +4,7 @@ import unittest from lifecycle import * -from KVS import KVS +from KVS import KVS, NotPersistableError # A dummy test class @@ -21,12 +21,21 @@ "id": self.id } +class FACTORY(): + @staticmethod + def get(name, **kwargs): + if name == "TV": return TV(**kwargs) + else: + raise ValueError("Unknown device name %s" % name) + def remove_file(filename): import os - os.unlink(filename) - + try: + os.unlink(filename) + except OSError: + pass # ignore def show_file(filename): """Show the contents of a file on screen""" @@ -35,16 +44,22 @@ l = l.strip() # remove nl print(l) +def write_file(filename, contents): + with open(filename, "w") as f: + lines = contents.split("\n") + for line in lines: + f.write(line + '\n') + class TestKVSMemory(unittest.TestCase): - @test_0 + @test_1 def test_create_blank(self): """Create a blank kvs, not bound to any external file""" kvs = KVS() # it should not fall over - @test_0 + @test_1 def test_add(self): """Add an object into the kvs store""" kvs = KVS() @@ -54,7 +69,7 @@ print(kvs.store) - @test_0 + @test_1 def test_change(self): """Change the value associated with an existing key""" kvs = KVS() @@ -63,7 +78,7 @@ print(kvs.store) - @test_0 + @test_1 def test_get(self): """Get the object associated with a key in the store""" kvs = KVS() @@ -71,7 +86,7 @@ t = kvs["tv1"] print(t) - @test_0 + @test_1 def test_delete(self): """Delete an existing key in the store, and a missing key for error""" kvs = KVS() @@ -85,7 +100,7 @@ except KeyError: pass # expected - @test_0 + @test_1 def test_size(self): """How big is the kvs""" kvs = KVS() @@ -94,7 +109,7 @@ kvs["tv2"] = TV(2) print(len(kvs)) - @test_0 + @test_1 def test_keys(self): """Get out all keys of the kvs""" kvs = KVS() @@ -108,7 +123,7 @@ KVS_FILENAME = "test.kvs" - @test_0 + @test_1 def test_write(self): """Write an in memory KVS to a file""" remove_file(self.KVS_FILENAME) @@ -118,7 +133,7 @@ show_file(self.KVS_FILENAME) - @test_0 + @test_1 def test_load_cache(self): """Load record from a kvs file into the kvs cache""" # create a file to test against @@ -138,7 +153,7 @@ # check state of the kvs file at end show_file(self.KVS_FILENAME) - @test_0 + @test_1 def test_add(self): """Add a new record to a persisted KVS""" remove_file(self.KVS_FILENAME) @@ -149,7 +164,7 @@ print(kvs.store) show_file(self.KVS_FILENAME) - @test_0 + @test_1 def test_delete(self): """Delete an existing key from the persistent version""" @@ -165,7 +180,7 @@ del kvs["tv1"] - @test_0 + @test_1 def test_change(self): """Change an existing record in a persisted KVS""" remove_file(self.KVS_FILENAME) @@ -177,9 +192,7 @@ kvs["tv1"] = TV(2) ####HERE### show_file(self.KVS_FILENAME) - #---- HERE ---- - - @test_0 + @test_1 def test_ADD_nofactory(self): #NOTE: This is an under the bonnet test of parsing an ADD record from the file @@ -194,7 +207,7 @@ # expected result: object described as a kvp becomes a kvp in the store if no factory callback print(kvs.store) - @test_0 + @test_1 def test_ADD_factory(self): #NOTE: This is an under the bonnet test of parsing an ADD record from the file obj = { @@ -202,21 +215,13 @@ "id": 1234 } kvs = KVS(self.KVS_FILENAME) - - class FACTORY(): - @staticmethod - def get(name, **kwargs): - if name == "TV": return TV(**kwargs) - else: - raise ValueError("Unknown device name %s" % name) - - kvs.ADD("tv1", obj, FACTORY) + kvs.ADD("tv1", obj, create_fn=FACTORY.get) # expected result: object described as a kvp becomes a configured object instance in store print(kvs.store) - @test_0 + @test_1 def test_IGN(self): #NOTE: This is an under the bonnet test of parsing an IGN record from the file obj = { @@ -229,7 +234,6 @@ # expected result: no change to the in memory data structures print(kvs.store) - #---- HERE ---- @test_1 def test_DEL(self): @@ -256,22 +260,45 @@ # expected result: error if it was not in the store in the first place print(kvs.store) - @test_0 + + @test_1 def test_load_process(self): """Load and process a file with lots of records in it""" - #including ADD, IGN, DEL - #make sure callback processing is working too for object creation - #as the callback will create the object that is stored in the cache - pass #TODO + CONTENTS = """\ +ADD tv +type=TV +id=1 +IGN fan +type=TV +id=2 -#TODO: Other tests - for integrating with the registry later -#pass in an object creator callback, should turn kvp into object instance -#when persisting, try to call get_config(), if it works, persist the kvp, -#if there is no get_config(), decide what to persist, if anything, -#or throw a NotPersistable error perhaps? -#Look to see if there is a pythonic way to do this, perhaps with one of -#the meta methods? +DEL tv + +ADD fridge +type=TV +id=99 +""" + write_file(self.KVS_FILENAME, CONTENTS) + + kvs = KVS(self.KVS_FILENAME) + kvs.load(create_fn=FACTORY.get) + + print(kvs.store) + + @test_1 + def test_not_persistable(self): + class NPC(): + pass + remove_file(self.KVS_FILENAME) + kvs = KVS(self.KVS_FILENAME) + + try: + kvs["npc"] = NPC() # should throw NotPersistableError + self.fail("Did not get expected NotPersistableError") + except NotPersistableError: + pass # expected + if __name__ == "__main__": unittest.main()