-#!/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 MoeStatus:
- """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
-
- 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 write(self, file=None, name=None):
- if file is None:
- if name is not None:
- file = open(name, "w")
+ self.d[k] = v
+
+ def __eq__(self, s):
+ return self.d == s.d
+
+ def keys(self):
+ return self.d.keys()
+
+ 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:
- file = sys.stdout
- self.write_nested(file, 0)
-
- def write_nested(self, file, indent):
- for k,v in self.stat.items():
- if type(v) == types.ListType: vals = v
- else: vals = [v]
- for w in vals:
- if isinstance(w, MoeStatus):
- file.write("\t" * indent + k + "(\n")
- w.write_nested(file, indent+1);
- file.write("\t" * indent + ")\n")
+ 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:
- file.write("\t" * indent + k + ":" + str(w) + "\n")
+ self[k] = v2
+
+ def dump(self, prefix=""):
+ """
+ Dump Status in status file format.
+ Returns a list of lines, ``prefix`` is indentation prefix.
+ """
- def read(self, file=None, name=None):
- if file is None:
- if name is not None:
- file = open(name, "r")
+ l = []
+ for k,v in self.d.items():
+ if isinstance(v, Status):
+ l.append(prefix + k + " (")
+ l.extend(v.dump(prefix+" "))
+ l.append(prefix + ")")
else:
- file = sys.stdin
- self.stat = {}
- self.do_read(file)
+ 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 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():
+ f.write(l+"\n")
+ else:
+ if not f:
+ f = sys.stdout
+ for l in self.dump():
+ f.write(l+"\n")
+
+ 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, file):
- stk = []
- this = self
- for x in file.readlines():
+ def do_read(self, lines):
+ """
+ Internal: Parse a status file given as list/iterator of lines
+ """
+
+ 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]
- new = MoeStatus()
+ 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")