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