diff --git a/src/energenie/KVS.py b/src/energenie/KVS.py index 4d895c3..c603688 100644 --- a/src/energenie/KVS.py +++ b/src/energenie/KVS.py @@ -10,52 +10,68 @@ self.filename = filename self.store = {} - @unimplemented - def load(self, create_cb): + def load(self, filename=None, create_cb=None): """Load the whole file into an in-memory cache""" - # The 'callback' is called to process each record as it is read in. + + # use new filename if provided, else use existing filename + if filename == None: + filename == self.filename + if filename == None: + raise ValueError("No filename specified") + + #TODO: The 'callback' is called to process each record as it is read in?? # for the Registry, this is a way that it can create the class and also add receive routing - pass #TODO - # open file for read - # for each line read - # if in command mode - # if blank line, ignore it - # else not blank line - # split line, first word is command, second word is the key - # remember both - # change to data mode - # else in data mode - # if not blank line - # grab key=value - # add to temporary object - # else blank line - # process command,key,values - # now eof - # process command,key,values, if it command is not empty - # close file - @unimplemented - def process(self, command, key, values): + is_cmd = True + command = None + key = None + obj = None + + with open(filename) as f: + while True: + line = f.readline() + if line == "": # EOF + if command != None: + self.process(command, key, obj) + break # END + else: + line = line.strip() # remove nl + if is_cmd: + if len(line) == 0: # blank + pass # ignore extra blank lines + else: # not blank + # split line, first word is command, second word is the key + command, key = line.split(" ", 1) + obj = {} + is_cmd = False # now in data mode + + else: # in data mode + if len(line) > 0: # not blank + ##print("parsing %s" % line) + k,v = line.split("=", 1) + obj[k] = v + else: # is blank + self.process(command, key, obj) + command = None + is_cmd = True + + self.filename = filename # remember filename if it was provided + + def process(self, command, key, obj): """Process the temporary object""" - pass #TODO - # getattr method associated with the command name, error if no method - # pass the key,values to that method to let it be processed + m = getattr(self, command) + #If command is not found? get AttributeError - that's fine + m(key, obj) - @unimplemented - def ADD(self, key, values): + def ADD(self, key, obj): """Add a new item to the kvs""" # The ADD command process the next type= parameter as the class name in context # all other parameters are read as strings and passed to class constructor as kwargs - pass #TODO - # add key=values to the in memory object store - # open file for append - # write ADD command with key - # for all keys in value - # write k=v - # close file + self.store[key] = obj + self.append(key, obj) @unimplemented - def IGN(self, key, values=None): + def IGN(self, key, obj=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 @@ -63,7 +79,7 @@ pass # There is nothing to do with this command @unimplemented - def DEL(self, key, values=None): + def DEL(self, key, obj=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. @@ -75,8 +91,12 @@ return self.store[key] def __setitem__(self, key, value): + if key in self.store: + self.remove(key) # patches it to an IGN record + self.store[key] = value - self.append(key, value) + obj = value.get_config() # will fail with AttributeError if this method does not exist + self.append(key, obj) def __delitem__(self, key): del self.store[key] @@ -85,12 +105,13 @@ def __len__(self): return len(self.store) - @untested def keys(self): return self.store.keys() def append(self, key, values): """Append a new record to the persistent file""" + print("append:%s %s" % (key, values)) + if self.filename != None: with open(self.filename, 'a') as f: f.write("ADD %s\n" % key) @@ -99,11 +120,14 @@ f.write("%s=%s\n" % (k, v)) f.write("\n") - @untested + @unimplemented def remove(self, key): """Remove reference to this key in the file, and remove from in memory store""" if self.filename != None: pass #TODO + + ####HERE#### + # open file for read write # search line at a time, process each command # when we find the command 'ADD key' @@ -112,19 +136,32 @@ # keep going in case of duplicates # close file - @unimplemented def write(self, filename=None): """Rewrite the whole in memory cache over the top of the external file""" - # useful if you have updated the in memory copy only and want to completely regenerate - pass #TODO - # create file new, for write only - # for all objects in the store by key - # get value - # write ADD command key - # for all values - # write k=v - # write blank line - # close file + #or create a new file if one does not exist + + if filename == None: + filename = self.filename + if filename == None: + raise RuntimeError("No filename configured") + + with open(filename, "w") as f: + # create an ADD record in the file, for each object in the store + + for key in self.store: + obj = self.store[key] + # TODO: for this to work, we need to call the inner object get_config() to get a persistable version + # that the user of this class can recreate it from later + + f.write("ADD %s\n" % key) + state = obj.get_config() # will fail if object does not have this method + for k in state: + f.write("%s=%s\n" % (k, state[k])) + + # terminate with a blank line + f.write("\n") + + self.filename = filename # Remember that we are linked to this file # END diff --git a/src/energenie/KVS_test.py b/src/energenie/KVS_test.py index 935a7fb..a38d473 100644 --- a/src/energenie/KVS_test.py +++ b/src/energenie/KVS_test.py @@ -15,6 +15,25 @@ def __repr__(self): return "TV(%s)" % self.id + def get_config(self): + return { + "id": self.id + } + + + +def remove_file(filename): + import os + os.unlink(filename) + + +def show_file(filename): + """Show the contents of a file on screen""" + with open(filename) as f: + for l in f.readlines(): + l = l.strip() # remove nl + print(l) + class TestKVSMemory(unittest.TestCase): @@ -86,40 +105,99 @@ class TestKVSPersisted(unittest.TestCase): - @test_0 - def test_write(self): - pass #TODO: write an in memory kvs to a file - # write + KVS_FILENAME = "test.kvs" @test_0 - def test_load(self): - pass #TODO: load a blank kvs from an external file - # load - # callback for object creation needs to be passed in - # want to test that kvp's could be used to pass to kwargs to construct - # data must be passed in string format + def test_write(self): + """Write an in memory KVS to a file""" + remove_file(self.KVS_FILENAME) + kvs = KVS() + kvs["tv1"] = TV(1) + kvs.write(self.KVS_FILENAME) + + show_file(self.KVS_FILENAME) + + @test_0 + def test_load_cache(self): + """Load record from a kvs file into the kvs cache""" + # create a file to test against + remove_file(self.KVS_FILENAME) + kvs = KVS() + kvs["tv1"] = TV(1) + kvs.write(self.KVS_FILENAME) + + kvs = KVS() # clear it out again + + # load the file + kvs.load(self.KVS_FILENAME) + + # check the state of the kvs memory + print(kvs.store) + + # check state of the kvs file at end + show_file(self.KVS_FILENAME) @test_0 def test_add(self): - pass #TODO: does persistent version change as well? - # setitem + """Add a new record to a persisted KVS""" + remove_file(self.KVS_FILENAME) + kvs = KVS(self.KVS_FILENAME) + + kvs["tv1"] = TV(1) + + print(kvs.store) + show_file(self.KVS_FILENAME) + + #---- HERE ---- + + @test_1 + def test_delete(self): + """Delete an existing key from the persistent version""" + + remove_file(self.KVS_FILENAME) + kvs = KVS(self.KVS_FILENAME) + + kvs["tv1"] = TV(1) + show_file(self.KVS_FILENAME) + + del kvs["tv1"] ####FAIL remove() needs implementing + @test_0 def test_change(self): - pass #TODO: change the value associated with an existing key + """Change an existing record in a persisted KVS""" + remove_file(self.KVS_FILENAME) + kvs = KVS(self.KVS_FILENAME) + + kvs["tv1"] = TV(1) + show_file(self.KVS_FILENAME) + + kvs["tv1"] = TV(2) ####FAIL, need to implement kvs.remove() first + show_file(self.KVS_FILENAME) + + + @test_0 - def test_delete(self): - pass #TODO: does persistent version get an IGN update? - # delitem + def test_ADD(self): + pass #TODO: do ADD records get added when parsing the file? @test_0 def test_IGN(self): - pass #TODO: do IGN records get ignored? + pass #TODO: do IGN records get ignored when parsing the file? @test_0 def test_DEL(self): - pass #TODO: do DEL records get processed? + pass #TODO: do DEL records get processed when parsing the file? + + @test_0 + 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 + if __name__ == "__main__":