##// END OF EJS Templates
wireproto: add streams to frame-based protocol...
wireproto: add streams to frame-based protocol Previously, the frame-based protocol was just a series of frames, with each frame associated with a request ID. In order to scale the protocol, we'll want to enable the use of compression. While it is possible to enable compression at the socket/pipe level, this has its disadvantages. The big one is it undermines the point of frames being standalone, atomic units that can be read and written: if you add compression above the framing protocol, you are back to having a stream-based protocol as opposed to something frame-based. So in order to preserve frames, compression needs to occur at the frame payload level. Compressing each frame's payload individually will limit compression ratios because the window size of the compressor will be limited by the max frame size, which is 32-64kb as currently defined. It will also add CPU overhead, as it is more efficient for compressors to operate on fewer, larger blocks of data than more, smaller blocks. So compressing each frame independently is out. This means we need to compress each frame's payload as if it is part of a larger stream. The simplest approach is to have 1 stream per connection. This could certainly work. However, it has disadvantages (documented below). We could also have 1 stream per RPC/command invocation. (This is the model HTTP/2 goes with.) This also has disadvantages. The main disadvantage to one global stream is that it has the very real potential to create CPU bottlenecks doing compression. Networks are only getting faster and the performance of single CPU cores has been relatively flat. Newer compression formats like zstandard offer better CPU cycle efficiency than predecessors like zlib. But it still all too common to saturate your CPU with compression overhead long before you saturate the network pipe. The main disadvantage with streams per request is that you can't reap the benefits of the compression context for multiple requests. For example, if you send 1000 RPC requests (or HTTP/2 requests for that matter), the response to each would have its own compression context. The overall size of the raw responses would be larger because compression contexts wouldn't be able to reference data from another request or response. The approach for streams as implemented in this commit is to support N streams per connection and for streams to potentially span requests and responses. As explained by the added internals docs, this facilitates servers and clients delegating independent streams and compression to independent threads / CPU cores. This helps alleviate the CPU bottleneck of compression. This design also allows compression contexts to be reused across requests/responses. This can result in improved compression ratios and less overhead for compressors and decompressors having to build new contexts. Another feature that was defined was the ability for individual frames within a stream to declare whether that individual frame's payload uses the content encoding (read: compression) defined by the stream. The idea here is that some servers may serve data from a combination of caches and dynamic resolution. Data coming from caches may be pre-compressed. We want to facilitate servers being able to essentially stream bytes from caches to the wire with minimal overhead. Being able to mix and match with frames are compressed within a stream enables these types of advanced server functionality. This commit defines the new streams mechanism. Basic code for supporting streams in frames has been added. But that code is seriously lacking and doesn't fully conform to the defined protocol. For example, we don't close any streams. And support for content encoding within streams is not yet implemented. The change was rather invasive and I didn't think it would be reasonable to implement the entire feature in a single commit. For the record, I would have loved to reuse an existing multiplexing protocol to build the new wire protocol on top of. However, I couldn't find a protocol that offers the performance and scaling characteristics that I desired. Namely, it should support multiple compression contexts to facilitate scaling out to multiple CPU cores and compression contexts should be able to live longer than single RPC requests. HTTP/2 *almost* fits the bill. But the semantics of HTTP message exchange state that streams can only live for a single request-response. We /could/ tunnel on top of HTTP/2 streams and frames with HEADER and DATA frames. But there's no guarantee that HTTP/2 libraries and proxies would allow us to use HTTP/2 streams and frames without the HTTP message exchange semantics defined in RFC 7540 Section 8. Other RPC protocols like gRPC tunnel are built on top of HTTP/2 and thus preserve its semantics of stream per RPC invocation. Even QUIC does this. We could attempt to invent a higher-level stream that spans HTTP/2 streams. But this would be violating HTTP/2 because there is no guarantee that HTTP/2 streams are routed to the same server. The best we can do - which is what this protocol does - is shoehorn all request and response data into a single HTTP message and create streams within. At that point, we've defined a Content-Type in HTTP parlance. It just so happens our media type can also work as a standalone, stream-based protocol, without leaning on HTTP or similar protocol. Differential Revision: https://phab.mercurial-scm.org/D2907

File last commit:

r37102:f0b6fbea default
r37304:9bfcbe4f 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)
for i in xrange(0, n):
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))