diff -r 5bf465160632 -r 3201ff1459dd mercurial/dagparser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/dagparser.py Thu Jun 10 11:48:15 2010 +0200 @@ -0,0 +1,473 @@ +# dagparser.py - parser and generator for concise description of DAGs +# +# Copyright 2010 Peter Arrenbrecht +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import re, string +import util + +def parsedag(desc): + '''parses a DAG from a concise textual description; generates events + + "+n" is a linear run of n nodes based on the current default parent + "." is a single node based on the current default parent + "$" resets the default parent to -1 (implied at the start); + otherwise the default parent is always the last node created + ">> len(list(parsedag(""" + ... + ... +3 # 3 nodes in linear run + ... :forkhere # a label for the last of the 3 nodes from above + ... +5 # 5 more nodes on one branch + ... :mergethis # label again + ... >> list(parsedag("")) + [] + + A simple linear run: + + >>> list(parsedag("+3")) + [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))] + + Some non-standard ways to define such runs: + + >>> list(parsedag("+1+2")) + [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))] + + >>> list(parsedag("+1*1*")) + [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))] + + >>> list(parsedag("*")) + [('n', (0, [-1]))] + + >>> list(parsedag("...")) + [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))] + + A fork and a join, using numeric back references: + + >>> list(parsedag("+2*2*/2")) + [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))] + + >>> list(parsedag("+2<2+1/2")) + [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))] + + Placing a label: + + >>> list(parsedag("+1 :mylabel +1")) + [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))] + + An empty label (silly, really): + + >>> list(parsedag("+1:+1")) + [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))] + + Fork and join, but with labels instead of numeric back references: + + >>> list(parsedag("+1:f +1:p2 *f */p2")) + [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')), + ('n', (2, [0])), ('n', (3, [2, 1]))] + + >>> list(parsedag("+1:f +1:p2 >> list(parsedag("+1 $ +1")) + [('n', (0, [-1])), ('n', (1, [-1]))] + + Annotations, which are meant to introduce sticky state for subsequent nodes: + + >>> list(parsedag("+1 @ann +1")) + [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))] + + >>> list(parsedag('+1 @"my annotation" +1')) + [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))] + + Commands, which are meant to operate on the most recently created node: + + >>> list(parsedag("+1 !cmd +1")) + [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))] + + >>> list(parsedag('+1 !"my command" +1')) + [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))] + + >>> list(parsedag('+1 !!my command line\\n +1')) + [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))] + + Comments, which extend to the end of the line: + + >>> list(parsedag('+1 # comment\\n+1')) + [('n', (0, [-1])), ('n', (1, [0]))] + + Error: + + >>> try: list(parsedag('+1 bad')) + ... except Exception, e: print e + invalid character in dag description: bad... + + ''' + if not desc: + return + + wordchars = string.ascii_letters + string.digits + + labels = {} + p1 = -1 + r = 0 + + def resolve(ref): + if not ref: + return p1 + elif ref[0] in string.digits: + return r - int(ref) + else: + return labels[ref] + + chiter = (c for c in desc) + + def nextch(): + try: + return chiter.next() + except StopIteration: + return '\0' + + def nextrun(c, allow): + s = '' + while c in allow: + s += c + c = nextch() + return c, s + + def nextdelimited(c, limit, escape): + s = '' + while c != limit: + if c == escape: + c = nextch() + s += c + c = nextch() + return nextch(), s + + def nextstring(c): + if c == '"': + return nextdelimited(nextch(), '"', '\\') + else: + return nextrun(c, wordchars) + + c = nextch() + while c != '\0': + while c in string.whitespace: + c = nextch() + if c == '.': + yield 'n', (r, [p1]) + p1 = r + r += 1 + c = nextch() + elif c == '+': + c, digs = nextrun(nextch(), string.digits) + n = int(digs) + for i in xrange(0, n): + yield 'n', (r, [p1]) + p1 = r + r += 1 + elif c == '*' or c == '/': + if c == '*': + c = nextch() + c, pref = nextstring(c) + prefs = [pref] + while c == '/': + c, pref = nextstring(nextch()) + prefs.append(pref) + ps = [resolve(ref) for ref in prefs] + yield 'n', (r, ps) + p1 = r + r += 1 + elif c == '<': + c, ref = nextstring(nextch()) + p1 = resolve(ref) + elif c == ':': + c, name = nextstring(nextch()) + labels[name] = p1 + yield 'l', (p1, name) + elif c == '@': + c, text = nextstring(nextch()) + yield 'a', text + elif c == '!': + c = nextch() + if c == '!': + cmd = '' + c = nextch() + while c not in '\n\r\0': + cmd += c + c = nextch() + yield 'C', cmd + else: + c, cmd = nextstring(c) + yield 'c', cmd + elif c == '#': + while c not in '\n\r\0': + c = nextch() + elif c == '$': + p1 = -1 + c = nextch() + elif c == '\0': + return # in case it was preceded by whitespace + else: + s = '' + i = 0 + while c != '\0' and i < 10: + s += c + i += 1 + c = nextch() + raise util.Abort("invalid character in dag description: %s..." % s) + +def dagtextlines(events, + addspaces=True, + wraplabels=False, + wrapannotations=False, + wrapcommands=False, + wrapnonlinear=False, + usedots=False, + maxlinewidth=70): + '''generates single lines for dagtext()''' + + def wrapstring(text): + if re.match("^[0-9a-z]*$", text): + return text + return '"' + text.replace('\\', '\\\\').replace('"', '\"') + '"' + + def gen(): + labels = {} + run = 0 + wantr = 0 + needroot = False + for kind, data in events: + if kind == 'n': + r, ps = data + + # sanity check + if r != wantr: + raise util.Abort("Expected id %i, got %i" % (wantr, r)) + if not ps: + ps = [-1] + else: + for p in ps: + if p >= r: + raise util.Abort("Parent id %i is larger than " + "current id %i" % (p, r)) + wantr += 1 + + # new root? + p1 = r - 1 + if len(ps) == 1 and ps[0] == -1: + if needroot: + if run: + yield '+' + str(run) + run = 0 + if wrapnonlinear: + yield '\n' + yield '$' + p1 = -1 + else: + needroot = True + if len(ps) == 1 and ps[0] == p1: + if usedots: + yield "." + else: + run += 1 + else: + if run: + yield '+' + str(run) + run = 0 + if wrapnonlinear: + yield '\n' + prefs = [] + for p in ps: + if p == p1: + prefs.append('') + elif p in labels: + prefs.append(labels[p]) + else: + prefs.append(str(r - p)) + yield '*' + '/'.join(prefs) + else: + if run: + yield '+' + str(run) + run = 0 + if kind == 'l': + rid, name = data + labels[rid] = name + yield ':' + name + if wraplabels: + yield '\n' + elif kind == 'c': + yield '!' + wrapstring(data) + if wrapcommands: + yield '\n' + elif kind == 'C': + yield '!!' + data + yield '\n' + elif kind == 'a': + if wrapannotations: + yield '\n' + yield '@' + wrapstring(data) + elif kind == '#': + yield '#' + data + yield '\n' + else: + raise util.Abort("invalid event type in dag: " + + format((type, data))) + if run: + yield '+' + str(run) + + line = '' + for part in gen(): + if part == '\n': + if line: + yield line + line = '' + else: + if len(line) + len(part) >= maxlinewidth: + yield line + line = '' + elif addspaces and line and part != '.': + line += ' ' + line += part + if line: + yield line + +def dagtext(dag, + addspaces=True, + wraplabels=False, + wrapannotations=False, + wrapcommands=False, + wrapnonlinear=False, + usedots=False, + maxlinewidth=70): + '''generates lines of a textual representation for a dag event stream + + events should generate what parsedag() does, so: + + ('n', (id, [parentids])) for node creation + ('l', (id, labelname)) for labels on nodes + ('a', text) for annotations + ('c', text) for commands + ('C', text) for line commands ('!!') + ('#', text) for comment lines + + Parent nodes must come before child nodes. + + Examples + -------- + + Linear run: + + >>> dagtext([('n', (0, [-1])), ('n', (1, [0]))]) + '+2' + + Two roots: + + >>> dagtext([('n', (0, [-1])), ('n', (1, [-1]))]) + '+1 $ +1' + + Fork and join: + + >>> dagtext([('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), + ... ('n', (3, [2, 1]))]) + '+2 *2 */2' + + Fork and join with labels: + + >>> dagtext([('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), + ... ('l', (1, 'p2')), ('n', (2, [0])), ('n', (3, [2, 1]))]) + '+1 :f +1 :p2 *f */p2' + + Annotations: + + >>> dagtext([('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]) + '+1 @ann +1' + + >>> dagtext([('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]) + '+1 @"my annotation" +1' + + Commands: + + >>> dagtext([('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]) + '+1 !cmd +1' + + >>> dagtext([('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]) + '+1 !"my command" +1' + + >>> dagtext([('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]) + '+1 !!my command line\\n+1' + + Comments: + + >>> dagtext([('n', (0, [-1])), ('#', ' comment'), ('n', (1, [0]))]) + '+1 # comment\\n+1' + + >>> dagtext([]) + '' + + Combining parsedag and dagtext: + + >>> dagtext(parsedag('+1 :f +1 :p2 *f */p2')) + '+1 :f +1 :p2 *f */p2' + + ''' + return "\n".join(dagtextlines(dag, + addspaces, + wraplabels, + wrapannotations, + wrapcommands, + wrapnonlinear, + usedots, + maxlinewidth))