##// END OF EJS Templates
py3: iterate bytes as a byte string in dagparser.py
Yuya Nishihara -
r34209:dfd009e5 default
parent child Browse files
Show More
@@ -1,488 +1,488 b''
1 # dagparser.py - parser and generator for concise description of DAGs
1 # dagparser.py - parser and generator for concise description of DAGs
2 #
2 #
3 # Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
3 # Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11 import string
11 import string
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 )
18 )
19
19
20 def parsedag(desc):
20 def parsedag(desc):
21 '''parses a DAG from a concise textual description; generates events
21 '''parses a DAG from a concise textual description; generates events
22
22
23 "+n" is a linear run of n nodes based on the current default parent
23 "+n" is a linear run of n nodes based on the current default parent
24 "." is a single node based on the current default parent
24 "." is a single node based on the current default parent
25 "$" resets the default parent to -1 (implied at the start);
25 "$" resets the default parent to -1 (implied at the start);
26 otherwise the default parent is always the last node created
26 otherwise the default parent is always the last node created
27 "<p" sets the default parent to the backref p
27 "<p" sets the default parent to the backref p
28 "*p" is a fork at parent p, where p is a backref
28 "*p" is a fork at parent p, where p is a backref
29 "*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
29 "*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
30 "/p2/.../pn" is a merge of the preceding node and p2..pn
30 "/p2/.../pn" is a merge of the preceding node and p2..pn
31 ":name" defines a label for the preceding node; labels can be redefined
31 ":name" defines a label for the preceding node; labels can be redefined
32 "@text" emits an annotation event for text
32 "@text" emits an annotation event for text
33 "!command" emits an action event for the current node
33 "!command" emits an action event for the current node
34 "!!my command\n" is like "!", but to the end of the line
34 "!!my command\n" is like "!", but to the end of the line
35 "#...\n" is a comment up to the end of the line
35 "#...\n" is a comment up to the end of the line
36
36
37 Whitespace between the above elements is ignored.
37 Whitespace between the above elements is ignored.
38
38
39 A backref is either
39 A backref is either
40 * a number n, which references the node curr-n, where curr is the current
40 * a number n, which references the node curr-n, where curr is the current
41 node, or
41 node, or
42 * the name of a label you placed earlier using ":name", or
42 * the name of a label you placed earlier using ":name", or
43 * empty to denote the default parent.
43 * empty to denote the default parent.
44
44
45 All string valued-elements are either strictly alphanumeric, or must
45 All string valued-elements are either strictly alphanumeric, or must
46 be enclosed in double quotes ("..."), with "\" as escape character.
46 be enclosed in double quotes ("..."), with "\" as escape character.
47
47
48 Generates sequence of
48 Generates sequence of
49
49
50 ('n', (id, [parentids])) for node creation
50 ('n', (id, [parentids])) for node creation
51 ('l', (id, labelname)) for labels on nodes
51 ('l', (id, labelname)) for labels on nodes
52 ('a', text) for annotations
52 ('a', text) for annotations
53 ('c', command) for actions (!)
53 ('c', command) for actions (!)
54 ('C', command) for line actions (!!)
54 ('C', command) for line actions (!!)
55
55
56 Examples
56 Examples
57 --------
57 --------
58
58
59 Example of a complex graph (output not shown for brevity):
59 Example of a complex graph (output not shown for brevity):
60
60
61 >>> len(list(parsedag(b"""
61 >>> len(list(parsedag(b"""
62 ...
62 ...
63 ... +3 # 3 nodes in linear run
63 ... +3 # 3 nodes in linear run
64 ... :forkhere # a label for the last of the 3 nodes from above
64 ... :forkhere # a label for the last of the 3 nodes from above
65 ... +5 # 5 more nodes on one branch
65 ... +5 # 5 more nodes on one branch
66 ... :mergethis # label again
66 ... :mergethis # label again
67 ... <forkhere # set default parent to labeled fork node
67 ... <forkhere # set default parent to labeled fork node
68 ... +10 # 10 more nodes on a parallel branch
68 ... +10 # 10 more nodes on a parallel branch
69 ... @stable # following nodes will be annotated as "stable"
69 ... @stable # following nodes will be annotated as "stable"
70 ... +5 # 5 nodes in stable
70 ... +5 # 5 nodes in stable
71 ... !addfile # custom command; could trigger new file in next node
71 ... !addfile # custom command; could trigger new file in next node
72 ... +2 # two more nodes
72 ... +2 # two more nodes
73 ... /mergethis # merge last node with labeled node
73 ... /mergethis # merge last node with labeled node
74 ... +4 # 4 more nodes descending from merge node
74 ... +4 # 4 more nodes descending from merge node
75 ...
75 ...
76 ... """)))
76 ... """)))
77 34
77 34
78
78
79 Empty list:
79 Empty list:
80
80
81 >>> list(parsedag(b""))
81 >>> list(parsedag(b""))
82 []
82 []
83
83
84 A simple linear run:
84 A simple linear run:
85
85
86 >>> list(parsedag(b"+3"))
86 >>> list(parsedag(b"+3"))
87 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
87 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
88
88
89 Some non-standard ways to define such runs:
89 Some non-standard ways to define such runs:
90
90
91 >>> list(parsedag(b"+1+2"))
91 >>> list(parsedag(b"+1+2"))
92 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
92 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
93
93
94 >>> list(parsedag(b"+1*1*"))
94 >>> list(parsedag(b"+1*1*"))
95 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
95 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
96
96
97 >>> list(parsedag(b"*"))
97 >>> list(parsedag(b"*"))
98 [('n', (0, [-1]))]
98 [('n', (0, [-1]))]
99
99
100 >>> list(parsedag(b"..."))
100 >>> list(parsedag(b"..."))
101 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
101 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
102
102
103 A fork and a join, using numeric back references:
103 A fork and a join, using numeric back references:
104
104
105 >>> list(parsedag(b"+2*2*/2"))
105 >>> list(parsedag(b"+2*2*/2"))
106 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
106 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
107
107
108 >>> list(parsedag(b"+2<2+1/2"))
108 >>> list(parsedag(b"+2<2+1/2"))
109 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
109 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
110
110
111 Placing a label:
111 Placing a label:
112
112
113 >>> list(parsedag(b"+1 :mylabel +1"))
113 >>> list(parsedag(b"+1 :mylabel +1"))
114 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
114 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
115
115
116 An empty label (silly, really):
116 An empty label (silly, really):
117
117
118 >>> list(parsedag(b"+1:+1"))
118 >>> list(parsedag(b"+1:+1"))
119 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
119 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
120
120
121 Fork and join, but with labels instead of numeric back references:
121 Fork and join, but with labels instead of numeric back references:
122
122
123 >>> list(parsedag(b"+1:f +1:p2 *f */p2"))
123 >>> list(parsedag(b"+1:f +1:p2 *f */p2"))
124 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
124 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
125 ('n', (2, [0])), ('n', (3, [2, 1]))]
125 ('n', (2, [0])), ('n', (3, [2, 1]))]
126
126
127 >>> list(parsedag(b"+1:f +1:p2 <f +1 /p2"))
127 >>> list(parsedag(b"+1:f +1:p2 <f +1 /p2"))
128 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
128 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
129 ('n', (2, [0])), ('n', (3, [2, 1]))]
129 ('n', (2, [0])), ('n', (3, [2, 1]))]
130
130
131 Restarting from the root:
131 Restarting from the root:
132
132
133 >>> list(parsedag(b"+1 $ +1"))
133 >>> list(parsedag(b"+1 $ +1"))
134 [('n', (0, [-1])), ('n', (1, [-1]))]
134 [('n', (0, [-1])), ('n', (1, [-1]))]
135
135
136 Annotations, which are meant to introduce sticky state for subsequent nodes:
136 Annotations, which are meant to introduce sticky state for subsequent nodes:
137
137
138 >>> list(parsedag(b"+1 @ann +1"))
138 >>> list(parsedag(b"+1 @ann +1"))
139 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
139 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
140
140
141 >>> list(parsedag(b'+1 @"my annotation" +1'))
141 >>> list(parsedag(b'+1 @"my annotation" +1'))
142 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
142 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
143
143
144 Commands, which are meant to operate on the most recently created node:
144 Commands, which are meant to operate on the most recently created node:
145
145
146 >>> list(parsedag(b"+1 !cmd +1"))
146 >>> list(parsedag(b"+1 !cmd +1"))
147 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
147 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
148
148
149 >>> list(parsedag(b'+1 !"my command" +1'))
149 >>> list(parsedag(b'+1 !"my command" +1'))
150 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
150 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
151
151
152 >>> list(parsedag(b'+1 !!my command line\\n +1'))
152 >>> list(parsedag(b'+1 !!my command line\\n +1'))
153 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
153 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
154
154
155 Comments, which extend to the end of the line:
155 Comments, which extend to the end of the line:
156
156
157 >>> list(parsedag(b'+1 # comment\\n+1'))
157 >>> list(parsedag(b'+1 # comment\\n+1'))
158 [('n', (0, [-1])), ('n', (1, [0]))]
158 [('n', (0, [-1])), ('n', (1, [0]))]
159
159
160 Error:
160 Error:
161
161
162 >>> try: list(parsedag(b'+1 bad'))
162 >>> try: list(parsedag(b'+1 bad'))
163 ... except Exception as e: print(pycompat.sysstr(bytes(e)))
163 ... except Exception as e: print(pycompat.sysstr(bytes(e)))
164 invalid character in dag description: bad...
164 invalid character in dag description: bad...
165
165
166 '''
166 '''
167 if not desc:
167 if not desc:
168 return
168 return
169
169
170 wordchars = pycompat.bytestr(string.ascii_letters + string.digits)
170 wordchars = pycompat.bytestr(string.ascii_letters + string.digits)
171
171
172 labels = {}
172 labels = {}
173 p1 = -1
173 p1 = -1
174 r = 0
174 r = 0
175
175
176 def resolve(ref):
176 def resolve(ref):
177 if not ref:
177 if not ref:
178 return p1
178 return p1
179 elif ref[0] in pycompat.bytestr(string.digits):
179 elif ref[0] in pycompat.bytestr(string.digits):
180 return r - int(ref)
180 return r - int(ref)
181 else:
181 else:
182 return labels[ref]
182 return labels[ref]
183
183
184 chiter = (c for c in desc)
184 chiter = pycompat.iterbytestr(desc)
185
185
186 def nextch():
186 def nextch():
187 return next(chiter, '\0')
187 return next(chiter, '\0')
188
188
189 def nextrun(c, allow):
189 def nextrun(c, allow):
190 s = ''
190 s = ''
191 while c in allow:
191 while c in allow:
192 s += c
192 s += c
193 c = nextch()
193 c = nextch()
194 return c, s
194 return c, s
195
195
196 def nextdelimited(c, limit, escape):
196 def nextdelimited(c, limit, escape):
197 s = ''
197 s = ''
198 while c != limit:
198 while c != limit:
199 if c == escape:
199 if c == escape:
200 c = nextch()
200 c = nextch()
201 s += c
201 s += c
202 c = nextch()
202 c = nextch()
203 return nextch(), s
203 return nextch(), s
204
204
205 def nextstring(c):
205 def nextstring(c):
206 if c == '"':
206 if c == '"':
207 return nextdelimited(nextch(), '"', '\\')
207 return nextdelimited(nextch(), '"', '\\')
208 else:
208 else:
209 return nextrun(c, wordchars)
209 return nextrun(c, wordchars)
210
210
211 c = nextch()
211 c = nextch()
212 while c != '\0':
212 while c != '\0':
213 while c in pycompat.bytestr(string.whitespace):
213 while c in pycompat.bytestr(string.whitespace):
214 c = nextch()
214 c = nextch()
215 if c == '.':
215 if c == '.':
216 yield 'n', (r, [p1])
216 yield 'n', (r, [p1])
217 p1 = r
217 p1 = r
218 r += 1
218 r += 1
219 c = nextch()
219 c = nextch()
220 elif c == '+':
220 elif c == '+':
221 c, digs = nextrun(nextch(), pycompat.bytestr(string.digits))
221 c, digs = nextrun(nextch(), pycompat.bytestr(string.digits))
222 n = int(digs)
222 n = int(digs)
223 for i in xrange(0, n):
223 for i in xrange(0, n):
224 yield 'n', (r, [p1])
224 yield 'n', (r, [p1])
225 p1 = r
225 p1 = r
226 r += 1
226 r += 1
227 elif c in '*/':
227 elif c in '*/':
228 if c == '*':
228 if c == '*':
229 c = nextch()
229 c = nextch()
230 c, pref = nextstring(c)
230 c, pref = nextstring(c)
231 prefs = [pref]
231 prefs = [pref]
232 while c == '/':
232 while c == '/':
233 c, pref = nextstring(nextch())
233 c, pref = nextstring(nextch())
234 prefs.append(pref)
234 prefs.append(pref)
235 ps = [resolve(ref) for ref in prefs]
235 ps = [resolve(ref) for ref in prefs]
236 yield 'n', (r, ps)
236 yield 'n', (r, ps)
237 p1 = r
237 p1 = r
238 r += 1
238 r += 1
239 elif c == '<':
239 elif c == '<':
240 c, ref = nextstring(nextch())
240 c, ref = nextstring(nextch())
241 p1 = resolve(ref)
241 p1 = resolve(ref)
242 elif c == ':':
242 elif c == ':':
243 c, name = nextstring(nextch())
243 c, name = nextstring(nextch())
244 labels[name] = p1
244 labels[name] = p1
245 yield 'l', (p1, name)
245 yield 'l', (p1, name)
246 elif c == '@':
246 elif c == '@':
247 c, text = nextstring(nextch())
247 c, text = nextstring(nextch())
248 yield 'a', text
248 yield 'a', text
249 elif c == '!':
249 elif c == '!':
250 c = nextch()
250 c = nextch()
251 if c == '!':
251 if c == '!':
252 cmd = ''
252 cmd = ''
253 c = nextch()
253 c = nextch()
254 while c not in '\n\r\0':
254 while c not in '\n\r\0':
255 cmd += c
255 cmd += c
256 c = nextch()
256 c = nextch()
257 yield 'C', cmd
257 yield 'C', cmd
258 else:
258 else:
259 c, cmd = nextstring(c)
259 c, cmd = nextstring(c)
260 yield 'c', cmd
260 yield 'c', cmd
261 elif c == '#':
261 elif c == '#':
262 while c not in '\n\r\0':
262 while c not in '\n\r\0':
263 c = nextch()
263 c = nextch()
264 elif c == '$':
264 elif c == '$':
265 p1 = -1
265 p1 = -1
266 c = nextch()
266 c = nextch()
267 elif c == '\0':
267 elif c == '\0':
268 return # in case it was preceded by whitespace
268 return # in case it was preceded by whitespace
269 else:
269 else:
270 s = ''
270 s = ''
271 i = 0
271 i = 0
272 while c != '\0' and i < 10:
272 while c != '\0' and i < 10:
273 s += c
273 s += c
274 i += 1
274 i += 1
275 c = nextch()
275 c = nextch()
276 raise error.Abort(_('invalid character in dag description: '
276 raise error.Abort(_('invalid character in dag description: '
277 '%s...') % s)
277 '%s...') % s)
278
278
279 def dagtextlines(events,
279 def dagtextlines(events,
280 addspaces=True,
280 addspaces=True,
281 wraplabels=False,
281 wraplabels=False,
282 wrapannotations=False,
282 wrapannotations=False,
283 wrapcommands=False,
283 wrapcommands=False,
284 wrapnonlinear=False,
284 wrapnonlinear=False,
285 usedots=False,
285 usedots=False,
286 maxlinewidth=70):
286 maxlinewidth=70):
287 '''generates single lines for dagtext()'''
287 '''generates single lines for dagtext()'''
288
288
289 def wrapstring(text):
289 def wrapstring(text):
290 if re.match("^[0-9a-z]*$", text):
290 if re.match("^[0-9a-z]*$", text):
291 return text
291 return text
292 return '"' + text.replace('\\', '\\\\').replace('"', '\"') + '"'
292 return '"' + text.replace('\\', '\\\\').replace('"', '\"') + '"'
293
293
294 def gen():
294 def gen():
295 labels = {}
295 labels = {}
296 run = 0
296 run = 0
297 wantr = 0
297 wantr = 0
298 needroot = False
298 needroot = False
299 for kind, data in events:
299 for kind, data in events:
300 if kind == 'n':
300 if kind == 'n':
301 r, ps = data
301 r, ps = data
302
302
303 # sanity check
303 # sanity check
304 if r != wantr:
304 if r != wantr:
305 raise error.Abort(_("expected id %i, got %i") % (wantr, r))
305 raise error.Abort(_("expected id %i, got %i") % (wantr, r))
306 if not ps:
306 if not ps:
307 ps = [-1]
307 ps = [-1]
308 else:
308 else:
309 for p in ps:
309 for p in ps:
310 if p >= r:
310 if p >= r:
311 raise error.Abort(_("parent id %i is larger than "
311 raise error.Abort(_("parent id %i is larger than "
312 "current id %i") % (p, r))
312 "current id %i") % (p, r))
313 wantr += 1
313 wantr += 1
314
314
315 # new root?
315 # new root?
316 p1 = r - 1
316 p1 = r - 1
317 if len(ps) == 1 and ps[0] == -1:
317 if len(ps) == 1 and ps[0] == -1:
318 if needroot:
318 if needroot:
319 if run:
319 if run:
320 yield '+%d' % run
320 yield '+%d' % run
321 run = 0
321 run = 0
322 if wrapnonlinear:
322 if wrapnonlinear:
323 yield '\n'
323 yield '\n'
324 yield '$'
324 yield '$'
325 p1 = -1
325 p1 = -1
326 else:
326 else:
327 needroot = True
327 needroot = True
328 if len(ps) == 1 and ps[0] == p1:
328 if len(ps) == 1 and ps[0] == p1:
329 if usedots:
329 if usedots:
330 yield "."
330 yield "."
331 else:
331 else:
332 run += 1
332 run += 1
333 else:
333 else:
334 if run:
334 if run:
335 yield '+%d' % run
335 yield '+%d' % run
336 run = 0
336 run = 0
337 if wrapnonlinear:
337 if wrapnonlinear:
338 yield '\n'
338 yield '\n'
339 prefs = []
339 prefs = []
340 for p in ps:
340 for p in ps:
341 if p == p1:
341 if p == p1:
342 prefs.append('')
342 prefs.append('')
343 elif p in labels:
343 elif p in labels:
344 prefs.append(labels[p])
344 prefs.append(labels[p])
345 else:
345 else:
346 prefs.append('%d' % (r - p))
346 prefs.append('%d' % (r - p))
347 yield '*' + '/'.join(prefs)
347 yield '*' + '/'.join(prefs)
348 else:
348 else:
349 if run:
349 if run:
350 yield '+%d' % run
350 yield '+%d' % run
351 run = 0
351 run = 0
352 if kind == 'l':
352 if kind == 'l':
353 rid, name = data
353 rid, name = data
354 labels[rid] = name
354 labels[rid] = name
355 yield ':' + name
355 yield ':' + name
356 if wraplabels:
356 if wraplabels:
357 yield '\n'
357 yield '\n'
358 elif kind == 'c':
358 elif kind == 'c':
359 yield '!' + wrapstring(data)
359 yield '!' + wrapstring(data)
360 if wrapcommands:
360 if wrapcommands:
361 yield '\n'
361 yield '\n'
362 elif kind == 'C':
362 elif kind == 'C':
363 yield '!!' + data
363 yield '!!' + data
364 yield '\n'
364 yield '\n'
365 elif kind == 'a':
365 elif kind == 'a':
366 if wrapannotations:
366 if wrapannotations:
367 yield '\n'
367 yield '\n'
368 yield '@' + wrapstring(data)
368 yield '@' + wrapstring(data)
369 elif kind == '#':
369 elif kind == '#':
370 yield '#' + data
370 yield '#' + data
371 yield '\n'
371 yield '\n'
372 else:
372 else:
373 raise error.Abort(_("invalid event type in dag: "
373 raise error.Abort(_("invalid event type in dag: "
374 "('%s', '%s')")
374 "('%s', '%s')")
375 % (util.escapestr(kind),
375 % (util.escapestr(kind),
376 util.escapestr(data)))
376 util.escapestr(data)))
377 if run:
377 if run:
378 yield '+%d' % run
378 yield '+%d' % run
379
379
380 line = ''
380 line = ''
381 for part in gen():
381 for part in gen():
382 if part == '\n':
382 if part == '\n':
383 if line:
383 if line:
384 yield line
384 yield line
385 line = ''
385 line = ''
386 else:
386 else:
387 if len(line) + len(part) >= maxlinewidth:
387 if len(line) + len(part) >= maxlinewidth:
388 yield line
388 yield line
389 line = ''
389 line = ''
390 elif addspaces and line and part != '.':
390 elif addspaces and line and part != '.':
391 line += ' '
391 line += ' '
392 line += part
392 line += part
393 if line:
393 if line:
394 yield line
394 yield line
395
395
396 def dagtext(dag,
396 def dagtext(dag,
397 addspaces=True,
397 addspaces=True,
398 wraplabels=False,
398 wraplabels=False,
399 wrapannotations=False,
399 wrapannotations=False,
400 wrapcommands=False,
400 wrapcommands=False,
401 wrapnonlinear=False,
401 wrapnonlinear=False,
402 usedots=False,
402 usedots=False,
403 maxlinewidth=70):
403 maxlinewidth=70):
404 '''generates lines of a textual representation for a dag event stream
404 '''generates lines of a textual representation for a dag event stream
405
405
406 events should generate what parsedag() does, so:
406 events should generate what parsedag() does, so:
407
407
408 ('n', (id, [parentids])) for node creation
408 ('n', (id, [parentids])) for node creation
409 ('l', (id, labelname)) for labels on nodes
409 ('l', (id, labelname)) for labels on nodes
410 ('a', text) for annotations
410 ('a', text) for annotations
411 ('c', text) for commands
411 ('c', text) for commands
412 ('C', text) for line commands ('!!')
412 ('C', text) for line commands ('!!')
413 ('#', text) for comment lines
413 ('#', text) for comment lines
414
414
415 Parent nodes must come before child nodes.
415 Parent nodes must come before child nodes.
416
416
417 Examples
417 Examples
418 --------
418 --------
419
419
420 Linear run:
420 Linear run:
421
421
422 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0]))])
422 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0]))])
423 '+2'
423 '+2'
424
424
425 Two roots:
425 Two roots:
426
426
427 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [-1]))])
427 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [-1]))])
428 '+1 $ +1'
428 '+1 $ +1'
429
429
430 Fork and join:
430 Fork and join:
431
431
432 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0])), (b'n', (2, [0])),
432 >>> dagtext([(b'n', (0, [-1])), (b'n', (1, [0])), (b'n', (2, [0])),
433 ... (b'n', (3, [2, 1]))])
433 ... (b'n', (3, [2, 1]))])
434 '+2 *2 */2'
434 '+2 *2 */2'
435
435
436 Fork and join with labels:
436 Fork and join with labels:
437
437
438 >>> dagtext([(b'n', (0, [-1])), (b'l', (0, b'f')), (b'n', (1, [0])),
438 >>> dagtext([(b'n', (0, [-1])), (b'l', (0, b'f')), (b'n', (1, [0])),
439 ... (b'l', (1, b'p2')), (b'n', (2, [0])), (b'n', (3, [2, 1]))])
439 ... (b'l', (1, b'p2')), (b'n', (2, [0])), (b'n', (3, [2, 1]))])
440 '+1 :f +1 :p2 *f */p2'
440 '+1 :f +1 :p2 *f */p2'
441
441
442 Annotations:
442 Annotations:
443
443
444 >>> dagtext([(b'n', (0, [-1])), (b'a', b'ann'), (b'n', (1, [0]))])
444 >>> dagtext([(b'n', (0, [-1])), (b'a', b'ann'), (b'n', (1, [0]))])
445 '+1 @ann +1'
445 '+1 @ann +1'
446
446
447 >>> dagtext([(b'n', (0, [-1])),
447 >>> dagtext([(b'n', (0, [-1])),
448 ... (b'a', b'my annotation'),
448 ... (b'a', b'my annotation'),
449 ... (b'n', (1, [0]))])
449 ... (b'n', (1, [0]))])
450 '+1 @"my annotation" +1'
450 '+1 @"my annotation" +1'
451
451
452 Commands:
452 Commands:
453
453
454 >>> dagtext([(b'n', (0, [-1])), (b'c', b'cmd'), (b'n', (1, [0]))])
454 >>> dagtext([(b'n', (0, [-1])), (b'c', b'cmd'), (b'n', (1, [0]))])
455 '+1 !cmd +1'
455 '+1 !cmd +1'
456
456
457 >>> dagtext([(b'n', (0, [-1])),
457 >>> dagtext([(b'n', (0, [-1])),
458 ... (b'c', b'my command'),
458 ... (b'c', b'my command'),
459 ... (b'n', (1, [0]))])
459 ... (b'n', (1, [0]))])
460 '+1 !"my command" +1'
460 '+1 !"my command" +1'
461
461
462 >>> dagtext([(b'n', (0, [-1])),
462 >>> dagtext([(b'n', (0, [-1])),
463 ... (b'C', b'my command line'),
463 ... (b'C', b'my command line'),
464 ... (b'n', (1, [0]))])
464 ... (b'n', (1, [0]))])
465 '+1 !!my command line\\n+1'
465 '+1 !!my command line\\n+1'
466
466
467 Comments:
467 Comments:
468
468
469 >>> dagtext([(b'n', (0, [-1])), (b'#', b' comment'), (b'n', (1, [0]))])
469 >>> dagtext([(b'n', (0, [-1])), (b'#', b' comment'), (b'n', (1, [0]))])
470 '+1 # comment\\n+1'
470 '+1 # comment\\n+1'
471
471
472 >>> dagtext([])
472 >>> dagtext([])
473 ''
473 ''
474
474
475 Combining parsedag and dagtext:
475 Combining parsedag and dagtext:
476
476
477 >>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2'))
477 >>> dagtext(parsedag(b'+1 :f +1 :p2 *f */p2'))
478 '+1 :f +1 :p2 *f */p2'
478 '+1 :f +1 :p2 *f */p2'
479
479
480 '''
480 '''
481 return "\n".join(dagtextlines(dag,
481 return "\n".join(dagtextlines(dag,
482 addspaces,
482 addspaces,
483 wraplabels,
483 wraplabels,
484 wrapannotations,
484 wrapannotations,
485 wrapcommands,
485 wrapcommands,
486 wrapnonlinear,
486 wrapnonlinear,
487 usedots,
487 usedots,
488 maxlinewidth))
488 maxlinewidth))
@@ -1,82 +1,81 b''
1 # this is hack to make sure no escape characters are inserted into the output
1 # this is hack to make sure no escape characters are inserted into the output
2
2
3 from __future__ import absolute_import
3 from __future__ import absolute_import
4
4
5 import doctest
5 import doctest
6 import os
6 import os
7 import re
7 import re
8 import sys
8 import sys
9
9
10 ispy3 = (sys.version_info[0] >= 3)
10 ispy3 = (sys.version_info[0] >= 3)
11
11
12 if 'TERM' in os.environ:
12 if 'TERM' in os.environ:
13 del os.environ['TERM']
13 del os.environ['TERM']
14
14
15 class py3docchecker(doctest.OutputChecker):
15 class py3docchecker(doctest.OutputChecker):
16 def check_output(self, want, got, optionflags):
16 def check_output(self, want, got, optionflags):
17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
20 # <exc.name>: <others> -> <name>: <others>
20 # <exc.name>: <others> -> <name>: <others>
21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
22 got2, re.MULTILINE)
22 got2, re.MULTILINE)
23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
25 for w, g in [(want, got), (want2, got2)])
25 for w, g in [(want, got), (want2, got2)])
26
26
27 # TODO: migrate doctests to py3 and enable them on both versions
27 # TODO: migrate doctests to py3 and enable them on both versions
28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
29 if not (not ispy3 and py2 or ispy3 and py3):
29 if not (not ispy3 and py2 or ispy3 and py3):
30 return
30 return
31 __import__(name)
31 __import__(name)
32 mod = sys.modules[name]
32 mod = sys.modules[name]
33 if testtarget is not None:
33 if testtarget is not None:
34 mod = getattr(mod, testtarget)
34 mod = getattr(mod, testtarget)
35
35
36 # minimal copy of doctest.testmod()
36 # minimal copy of doctest.testmod()
37 finder = doctest.DocTestFinder()
37 finder = doctest.DocTestFinder()
38 checker = None
38 checker = None
39 if ispy3:
39 if ispy3:
40 checker = py3docchecker()
40 checker = py3docchecker()
41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
42 for test in finder.find(mod, name):
42 for test in finder.find(mod, name):
43 runner.run(test)
43 runner.run(test)
44 runner.summarize()
44 runner.summarize()
45
45
46 testmod('mercurial.changegroup')
46 testmod('mercurial.changegroup')
47 testmod('mercurial.changelog')
47 testmod('mercurial.changelog')
48 testmod('mercurial.color')
48 testmod('mercurial.color')
49 testmod('mercurial.config')
49 testmod('mercurial.config')
50 testmod('mercurial.context')
50 testmod('mercurial.context')
51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE,
51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
52 py3=False) # py3: use of str()
53 testmod('mercurial.dispatch')
52 testmod('mercurial.dispatch')
54 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
53 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
55 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
54 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
56 testmod('mercurial.hg')
55 testmod('mercurial.hg')
57 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
56 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
58 testmod('mercurial.match')
57 testmod('mercurial.match')
59 testmod('mercurial.mdiff')
58 testmod('mercurial.mdiff')
60 testmod('mercurial.minirst')
59 testmod('mercurial.minirst')
61 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
60 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
62 testmod('mercurial.pathutil', py3=False) # py3: os.sep
61 testmod('mercurial.pathutil', py3=False) # py3: os.sep
63 testmod('mercurial.parser')
62 testmod('mercurial.parser')
64 testmod('mercurial.pycompat')
63 testmod('mercurial.pycompat')
65 testmod('mercurial.revsetlang')
64 testmod('mercurial.revsetlang')
66 testmod('mercurial.smartset')
65 testmod('mercurial.smartset')
67 testmod('mercurial.store', py3=False) # py3: bytes[n]
66 testmod('mercurial.store', py3=False) # py3: bytes[n]
68 testmod('mercurial.subrepo')
67 testmod('mercurial.subrepo')
69 testmod('mercurial.templatefilters')
68 testmod('mercurial.templatefilters')
70 testmod('mercurial.templater')
69 testmod('mercurial.templater')
71 testmod('mercurial.ui')
70 testmod('mercurial.ui')
72 testmod('mercurial.url')
71 testmod('mercurial.url')
73 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
72 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
74 testmod('mercurial.util', testtarget='platform')
73 testmod('mercurial.util', testtarget='platform')
75 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
74 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
76 testmod('hgext.convert.cvsps')
75 testmod('hgext.convert.cvsps')
77 testmod('hgext.convert.filemap')
76 testmod('hgext.convert.filemap')
78 testmod('hgext.convert.p4')
77 testmod('hgext.convert.p4')
79 testmod('hgext.convert.subversion')
78 testmod('hgext.convert.subversion')
80 testmod('hgext.mq')
79 testmod('hgext.mq')
81 # Helper scripts in tests/ that have doctests:
80 # Helper scripts in tests/ that have doctests:
82 testmod('drawdag')
81 testmod('drawdag')
General Comments 0
You need to be logged in to leave comments. Login now