##// END OF EJS Templates
localrepo: iteratively derive local repository type...
localrepo: iteratively derive local repository type This commit implements the dynamic local repository type derivation that was explained in the recent commit bfeab472e3c0 "localrepo: create new function for instantiating a local repo object." Instead of a static localrepository class/type which must be customized after construction, we now dynamically construct a type by building up base classes/types to represent specific repository interfaces. Conceptually, the end state is similar to what was happening when various extensions would monkeypatch the __class__ of newly-constructed repo instances. However, the approach is inverted. Instead of making the instance then customizing it, we do the customization up front by influencing the behavior of the type then we instantiate that custom type. This approach gives us much more flexibility. For example, we can use completely separate classes for implementing different aspects of the repository. For example, we could have one class representing revlog-based file storage and another representing non-revlog based file storage. When then choose which implementation to use based on the presence of repo requirements. A concern with this approach is that it creates a lot more types and complexity and that complexity adds overhead. Yes, it is true that this approach will result in more types being created. Yes, this is more complicated than traditional "instantiate a static type." However, I believe the alternatives to supporting alternate storage backends are just as complicated. (Before I arrived at this solution, I had patches storing factory functions on local repo instances for e.g. constructing a file storage instance. We ended up having a handful of these. And this was logically identical to assigning custom methods. Since we were logically changing the type of the instance, I figured it would be better to just use specialized types instead of introducing levels of abstraction at run-time.) On the performance front, I don't believe that having N base classes has any significant performance overhead compared to just a single base class. Intuition says that Python will need to iterate the base classes to find an attribute. However, CPython caches method lookups: as long as the __class__ or MRO isn't changing, method attribute lookup should be constant time after first access. And non-method attributes are stored in __dict__, of which there is only 1 per object, so the number of base classes for __dict__ is irrelevant. Anyway, this commit splits up the monolithic completelocalrepository interface into sub-interfaces: 1 for file storage and 1 representing everything else. We've taught ``makelocalrepository()`` to call a series of factory functions which will produce types implementing specific interfaces. It then calls type() to create a new type from the built-up list of base types. This commit should be considered a start and not the end state. I suspect we'll hit a number of problems as we start to implement alternate storage backends: * Passing custom arguments to __init__ and setting custom attributes on __dict__. * Customizing the set of interfaces that are needed. e.g. the "readonly" intent could translate to not requesting an interface providing methods related to writing. * More ergonomic way for extensions to insert themselves so their callbacks aren't unconditionally called. * Wanting to modify vfs instances, other arguments passed to __init__. That being said, this code is usable in its current state and I'm convinced future commits will demonstrate the value in this approach. Differential Revision: https://phab.mercurial-scm.org/D4642

File last commit:

r38806:e7aa113b default
r39800:e4e88157 default
Show More
dagparser.py
490 lines | 14.8 KiB | text/x-python | PythonLexer
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 # dagparser.py - parser and generator for concise description of DAGs
#
# Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
Gregory Szorc
dagparser: use absolute_import
r25941 from __future__ import absolute_import
import re
import string
from .i18n import _
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 from . import (
error,
Yuya Nishihara
py3: wrap string constants in dagparser.py with bytestr()
r34208 pycompat,
Yuya Nishihara
stringutil: bulk-replace call sites to point to new module...
r37102 )
from .utils import (
stringutil,
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 )
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335
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
"<p" sets the default parent to the backref p
"*p" is a fork at parent p, where p is a backref
"*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
"/p2/.../pn" is a merge of the preceding node and p2..pn
":name" defines a label for the preceding node; labels can be redefined
"@text" emits an annotation event for text
"!command" emits an action event for the current node
"!!my command\n" is like "!", but to the end of the line
"#...\n" is a comment up to the end of the line
Whitespace between the above elements is ignored.
A backref is either
* a number n, which references the node curr-n, where curr is the current
node, or
* the name of a label you placed earlier using ":name", or
* empty to denote the default parent.
All string valued-elements are either strictly alphanumeric, or must
be enclosed in double quotes ("..."), with "\" as escape character.
Generates sequence of
('n', (id, [parentids])) for node creation
('l', (id, labelname)) for labels on nodes
('a', text) for annotations
('c', command) for actions (!)
('C', command) for line actions (!!)
Examples
--------
Example of a complex graph (output not shown for brevity):
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> len(list(parsedag(b"""
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 ...
... +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
timeless@mozdev.org
en-us: labeled
r17500 ... <forkhere # set default parent to labeled fork node
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 ... +10 # 10 more nodes on a parallel branch
... @stable # following nodes will be annotated as "stable"
... +5 # 5 nodes in stable
... !addfile # custom command; could trigger new file in next node
... +2 # two more nodes
timeless@mozdev.org
en-us: labeled
r17500 ... /mergethis # merge last node with labeled node
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 ... +4 # 4 more nodes descending from merge node
...
... """)))
34
Empty list:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b""))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 []
A simple linear run:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+3"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
Some non-standard ways to define such runs:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1+2"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1*1*"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"*"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"..."))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
A fork and a join, using numeric back references:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+2*2*/2"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+2<2+1/2"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
Placing a label:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1 :mylabel +1"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
An empty label (silly, really):
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1:+1"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
Fork and join, but with labels instead of numeric back references:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1:f +1:p2 *f */p2"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
('n', (2, [0])), ('n', (3, [2, 1]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1:f +1:p2 <f +1 /p2"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
('n', (2, [0])), ('n', (3, [2, 1]))]
Restarting from the root:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1 $ +1"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [-1]))]
Annotations, which are meant to introduce sticky state for subsequent nodes:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1 @ann +1"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b'+1 @"my annotation" +1'))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
Commands, which are meant to operate on the most recently created node:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b"+1 !cmd +1"))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b'+1 !"my command" +1'))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b'+1 !!my command line\\n +1'))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
Comments, which extend to the end of the line:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> list(parsedag(b'+1 # comment\\n+1'))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 [('n', (0, [-1])), ('n', (1, [0]))]
Error:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> try: list(parsedag(b'+1 bad'))
Yuya Nishihara
doctest: upgrade old-style "except" clause
r34140 ... except Exception as e: print(pycompat.sysstr(bytes(e)))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 invalid character in dag description: bad...
'''
if not desc:
return
Yuya Nishihara
py3: wrap string constants in dagparser.py with bytestr()
r34208 wordchars = pycompat.bytestr(string.ascii_letters + string.digits)
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335
labels = {}
p1 = -1
r = 0
def resolve(ref):
if not ref:
return p1
Yuya Nishihara
py3: wrap string constants in dagparser.py with bytestr()
r34208 elif ref[0] in pycompat.bytestr(string.digits):
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 return r - int(ref)
else:
return labels[ref]
Yuya Nishihara
py3: iterate bytes as a byte string in dagparser.py
r34209 chiter = pycompat.iterbytestr(desc)
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335
def nextch():
Pierre-Yves David
dagparser: use 'next' instead of try/except for default value...
r25170 return next(chiter, '\0')
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335
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':
Yuya Nishihara
py3: wrap string constants in dagparser.py with bytestr()
r34208 while c in pycompat.bytestr(string.whitespace):
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 c = nextch()
if c == '.':
yield 'n', (r, [p1])
p1 = r
r += 1
c = nextch()
elif c == '+':
Yuya Nishihara
py3: wrap string constants in dagparser.py with bytestr()
r34208 c, digs = nextrun(nextch(), pycompat.bytestr(string.digits))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 n = int(digs)
Gregory Szorc
global: use pycompat.xrange()...
r38806 for i in pycompat.xrange(0, n):
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 yield 'n', (r, [p1])
p1 = r
r += 1
Brodie Rao
cleanup: use x in (a, b) instead of x == a or x == b
r12387 elif c in '*/':
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 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()
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_('invalid character in dag description: '
Brodie Rao
cleanup: eradicate long lines
r16683 '%s...') % s)
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335
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:
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_("expected id %i, got %i") % (wantr, r))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 if not ps:
ps = [-1]
else:
for p in ps:
if p >= r:
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_("parent id %i is larger than "
Martin Geisler
dagparser: translate and lowercase error messages
r12134 "current id %i") % (p, r))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 wantr += 1
# new root?
p1 = r - 1
if len(ps) == 1 and ps[0] == -1:
if needroot:
if run:
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 yield '+%d' % run
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 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:
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 yield '+%d' % run
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 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:
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 prefs.append('%d' % (r - p))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 yield '*' + '/'.join(prefs)
else:
if run:
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 yield '+%d' % run
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 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:
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 raise error.Abort(_("invalid event type in dag: "
"('%s', '%s')")
Yuya Nishihara
stringutil: bulk-replace call sites to point to new module...
r37102 % (stringutil.escapestr(kind),
stringutil.escapestr(data)))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 if run:
Yuya Nishihara
py3: drop use of str() in dagparser.py
r34207 yield '+%d' % run
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335
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:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+2'
Two roots:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [-1]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 $ +1'
Fork and join:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0])), (b'n', (2, [0])),
... (b'n', (3, [2, 1]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+2 *2 */2'
Fork and join with labels:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'l', (0, b'f')), (b'n', (1, [0])),
... (b'l', (1, b'p2')), (b'n', (2, [0])), (b'n', (3, [2, 1]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 :f +1 :p2 *f */p2'
Annotations:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'a', b'ann'), (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 @ann +1'
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])),
... (b'a', b'my annotation'),
... (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 @"my annotation" +1'
Commands:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'c', b'cmd'), (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 !cmd +1'
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])),
... (b'c', b'my command'),
... (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 !"my command" +1'
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])),
... (b'C', b'my command line'),
... (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 !!my command line\\n+1'
Comments:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext([(b'n', (0, [-1])), (b'#', b' comment'), (b'n', (1, [0]))])
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 # comment\\n+1'
>>> dagtext([])
''
Combining parsedag and dagtext:
Yuya Nishihara
doctest: bulk-replace string literals with b'' for Python 3...
r34133 >>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2'))
Peter Arrenbrecht
dagparser: parses and formats DAGs as concise text...
r11335 '+1 :f +1 :p2 *f */p2'
'''
return "\n".join(dagtextlines(dag,
addspaces,
wraplabels,
wrapannotations,
wrapcommands,
wrapnonlinear,
usedots,
maxlinewidth))