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