]> mj.ucw.cz Git - moe.git/blobdiff - t/moe/status.py
Minor updates (docs, status.py)
[moe.git] / t / moe / status.py
index 76681c7b4739fb2f499e92259002234ae9147786..144bf486f66afffaf6e272d79a74ebc7b727e43a 100644 (file)
@@ -1,64 +1,82 @@
-#!/usr/bin/env python
-
 import sys
 import types
 import re
 
-key_pattern = re.compile("^[A-Za-z0-9_-]+$")
+key_pattern_str = "\A[A-Za-z0-9_-]+\Z"
+key_pattern = re.compile(key_pattern_str)
 
-class MoeStatusInvalid(Exception):
+class InvalidStatusFile(StandardError):
     pass
 
-class Status:
-    """Moe status file."""
+class Status(object):
+    """
+    (One subtree of) a status file.
+
+    Each Status is a dictionary with string keys matching the specs. and 
+    values either strings or nested Status subtrees.
+
+    The class defines `__getitem__`, `__setitem__`, `__eq__` and `keys`.
+    """
 
     def __init__(self):
-        self.stat = {}
+        self.d = {}
 
     def __getitem__(self, k):
-       if not self.stat.has_key(k): return None
-        v = self.stat[k]
-       if type(v) == types.ListType:
-           if len(v) > 0: return v[0]
-           else: return None
-       else: return v
+       return self.d[k]
 
     def __setitem__(self, k, v):
-       self.stat[k] = v
+       self.d[k] = v
+
+    def __eq__(self, s):
+       return self.d == s.d
 
     def keys(self):
-       return self.stat.keys()
+       return self.d.keys()
 
-    def get_list(self, k):
-       m = self.stat
-       if not m.has_key(k):
-           m[k] = []
-       elif type(m[k]) != types.ListType:
-           m[k] = [m[k]]
-       return m[k]
+    def update(self, stat2):
+       """
+       Updates values of `self` with values of `stat2`, recursively.
 
+       Directly references objects (values and subtrees) of `stat2`, so making a deep copy of `stat2`
+       may be necessary if you intend to modify `stat2` afterwards.
+       """
+       
+       for k,v2 in stat2.d.items():
+           if k not in self.d:
+               self[k] = v2
+           else:
+               v = self[k]
+               if isinstance(v, Status) != isinstance(v2, Status):
+                   raise TypeError("Mixing Status and value while updating key %r"%k)
+               if isinstance(v, Status):
+                   v.update(v2)
+               else:
+                   self[k] = v2                
+       
     def dump(self, prefix=""):
        """
-       Dump status in metafile format.
-       Return a list of lines, `prefix` is indent prefix.
+       Dump Status in status file format.
+       Returns a list of lines, ``prefix`` is indentation prefix.
        """
+
        l = []
-        for k,v in self.stat.items():
-           if type(v) == types.ListType: vals = v
-           else: vals = [v]
-           for w in vals:
-               if isinstance(w, Status):
-                   l.append(prefix + k + "(\n")
-                   l.extend(self.str_lines(prefix+"  "))
-                   l.append(prefix + ")\n")
-               else:
-                   l.append(prefix + k + ":" + str(w) + "\n")
+        for k,v in self.d.items():
+           if isinstance(v, Status):
+               l.append(prefix + k + " (")
+               l.extend(v.dump(prefix+"  "))
+               l.append(prefix + ")")
+           else:
+               d = str(v).split('\n')
+               l.append(prefix + k + ":" + d[0])
+               for i in d[1:]:
+                   l.append(prefix + ' '*len(k) + ':' + i)
        return l
        
     def write(self, f=None, name=None):
        """
-       Write status (as a metafile) to `f` or file `name` or `stdout`
+       Write Status to File ``f`` or overwrite file ``name`` or write to ``stdout`` (otherwise).
        """
+
        if not f and name is not None:
            with open(name, "w") as f:
                for l in self.dump():
@@ -69,47 +87,75 @@ class Status:
            for l in self.dump():
                f.write(l+"\n")
 
-    def read(self, file=None, name=None):
-       if file is None:
-           if name is not None:
-               file = open(name, "r")
-           else:
-               file = sys.stdin
-       self.stat = {}
-       self.do_read(file)
+    def read(self, f=None, name=None, lines=None):
+       """
+       Parse Status file
+       * from File ``f`` 
+       * or from file ``name`` opened for reading 8-bit ASCII
+       * or from ``lines`` (a list/iterator of lines)
+
+       Deletes all previous contents of the Status.
+       """
+
+       self.d = {}
+       if f is not None:
+           return self.do_read(f.readlines())
+       if name is not None:
+           with open(name, 'r') as f:
+               return self.do_read(f.readlines())
+       if lines is not None:
+           return self.do_read(lines)
+       raise ValueError('Provide at least one parameter to Status.read()')
 
     def read_val(self, k, v):
+       """
+       Internal: Safely add a new value to Status
+       """
+
        if not key_pattern.match(k):
-           raise MoeStatusInvalid, "Parse error: invalid key syntax"
-       m = self.stat
-       if not m.has_key(k):
-           m[k] = v
-       else:
-           self.get_list(k).append(v)
+           raise InvalidStatusFile("Parse error: invalid key %r"%k)
+       if k in self.d:
+           raise InvalidStatusFile("Multiple occurences of key %r"%k)
+       self.d[k]=v
+
+    def do_read(self, lines):
+       """
+       Internal: Parse a status file given as list/iterator of lines
+       """
 
-    def do_read(self, file):
-       stk = []
-       this = self
-       for x in file.readlines():
+       stk = [] # stack
+       this = self # currently read nested Status
+       lastk = None # for multiline appending
+       for x in lines:
            x = x.rstrip("\n").lstrip(" \t")
            if x=="" or x.startswith("#"):
-               pass
+               lastk = None
            else:
                sep = x.find(":")
-               if sep >= 0:
-                   k = x[:sep]
+               if sep > 0: # key:value
+                   k = x[:sep].rstrip(" \t")
                    v = x[sep+1:]
                    this.read_val(k, v)
-               elif x.endswith("("):
-                   k = x[:-1]
+                   lastk = k
+               elif sep == 0: # continuation of multiline :value
+                   if not lastk:
+                       raise InvalidStatusFile("Parse error: key expected before ':'")
+                   v = x[sep+1:]
+                   this[lastk] += '\n' + v
+               elif x.endswith("("): # close subtree
+                   k = x[:-1].rstrip(" \t")
                    new = Status()
                    this.read_val(k, new)
                    stk.append(this)
                    this = new
+                   lastk = None
                elif x == ")":
+                   lastk = None
                    if len(stk) == 0:
-                       raise MoeStatusInvalid, "Parse error: incorrect nesting"
+                       raise InvalidStatusFile("Parse error: incorrect nesting")
                    else:
                        this = stk.pop()
                else:
-                   raise MoeStatusInvalid, "Parse error: malformed line"
+                   raise InvalidStatusFile("Parse error: malformed line")
+       if stk:
+           raise InvalidStatusFile("Parse error: not all subtrees closed")