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