##// END OF EJS Templates
py3: factor out byterepr() which returns an asciified value on py3
Yuya Nishihara -
r36279:b44fac3a default
parent child Browse files
Show More
@@ -1,544 +1,544 b''
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
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 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.verbose = verbose
49 49 ... ui.pushbuffer()
50 50 ... try:
51 51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 52 ... pycompat.byteskwargs(opts)))
53 53 ... finally:
54 54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55 55
56 56 Basic example:
57 57
58 58 >>> def files(ui, fm):
59 59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 60 ... for f in files:
61 61 ... fm.startitem()
62 62 ... fm.write(b'path', b'%s', f[0])
63 63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 65 ... fm.data(size=f[1])
66 66 ... fm.plain(b'\\n')
67 67 ... fm.end()
68 68 >>> show(files)
69 69 foo
70 70 bar
71 71 >>> show(files, verbose=True)
72 72 foo 1970-01-01 00:00:00
73 73 bar 1970-01-01 00:00:01
74 74 >>> show(files, template=b'json')
75 75 [
76 76 {
77 77 "date": [0, 0],
78 78 "path": "foo",
79 79 "size": 123
80 80 },
81 81 {
82 82 "date": [1, 0],
83 83 "path": "bar",
84 84 "size": 456
85 85 }
86 86 ]
87 87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 88 path: foo
89 89 date: 1970-01-01T00:00:00+00:00
90 90 path: bar
91 91 date: 1970-01-01T00:00:01+00:00
92 92
93 93 Nested example:
94 94
95 95 >>> def subrepos(ui, fm):
96 96 ... fm.startitem()
97 97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 98 ... files(ui, fm.nested(b'files'))
99 99 ... fm.end()
100 100 >>> show(subrepos)
101 101 [baz]
102 102 foo
103 103 bar
104 104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 105 baz: foo, bar
106 106 """
107 107
108 108 from __future__ import absolute_import, print_function
109 109
110 110 import collections
111 111 import contextlib
112 112 import itertools
113 113 import os
114 114
115 115 from .i18n import _
116 116 from .node import (
117 117 hex,
118 118 short,
119 119 )
120 120
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 templatefilters,
125 125 templatekw,
126 126 templater,
127 127 util,
128 128 )
129 129
130 130 pickle = util.pickle
131 131
132 132 class _nullconverter(object):
133 133 '''convert non-primitive data types to be processed by formatter'''
134 134
135 135 # set to True if context object should be stored as item
136 136 storecontext = False
137 137
138 138 @staticmethod
139 139 def formatdate(date, fmt):
140 140 '''convert date tuple to appropriate format'''
141 141 return date
142 142 @staticmethod
143 143 def formatdict(data, key, value, fmt, sep):
144 144 '''convert dict or key-value pairs to appropriate dict format'''
145 145 # use plain dict instead of util.sortdict so that data can be
146 146 # serialized as a builtin dict in pickle output
147 147 return dict(data)
148 148 @staticmethod
149 149 def formatlist(data, name, fmt, sep):
150 150 '''convert iterable to appropriate list format'''
151 151 return list(data)
152 152
153 153 class baseformatter(object):
154 154 def __init__(self, ui, topic, opts, converter):
155 155 self._ui = ui
156 156 self._topic = topic
157 157 self._style = opts.get("style")
158 158 self._template = opts.get("template")
159 159 self._converter = converter
160 160 self._item = None
161 161 # function to convert node to string suitable for this output
162 162 self.hexfunc = hex
163 163 def __enter__(self):
164 164 return self
165 165 def __exit__(self, exctype, excvalue, traceback):
166 166 if exctype is None:
167 167 self.end()
168 168 def _showitem(self):
169 169 '''show a formatted item once all data is collected'''
170 170 def startitem(self):
171 171 '''begin an item in the format list'''
172 172 if self._item is not None:
173 173 self._showitem()
174 174 self._item = {}
175 175 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
176 176 '''convert date tuple to appropriate format'''
177 177 return self._converter.formatdate(date, fmt)
178 178 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
179 179 '''convert dict or key-value pairs to appropriate dict format'''
180 180 return self._converter.formatdict(data, key, value, fmt, sep)
181 181 def formatlist(self, data, name, fmt='%s', sep=' '):
182 182 '''convert iterable to appropriate list format'''
183 183 # name is mandatory argument for now, but it could be optional if
184 184 # we have default template keyword, e.g. {item}
185 185 return self._converter.formatlist(data, name, fmt, sep)
186 186 def context(self, **ctxs):
187 187 '''insert context objects to be used to render template keywords'''
188 188 ctxs = pycompat.byteskwargs(ctxs)
189 189 assert all(k == 'ctx' for k in ctxs)
190 190 if self._converter.storecontext:
191 191 self._item.update(ctxs)
192 192 def data(self, **data):
193 193 '''insert data into item that's not shown in default output'''
194 194 data = pycompat.byteskwargs(data)
195 195 self._item.update(data)
196 196 def write(self, fields, deftext, *fielddata, **opts):
197 197 '''do default text output while assigning data to item'''
198 198 fieldkeys = fields.split()
199 199 assert len(fieldkeys) == len(fielddata)
200 200 self._item.update(zip(fieldkeys, fielddata))
201 201 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
202 202 '''do conditional write (primarily for plain formatter)'''
203 203 fieldkeys = fields.split()
204 204 assert len(fieldkeys) == len(fielddata)
205 205 self._item.update(zip(fieldkeys, fielddata))
206 206 def plain(self, text, **opts):
207 207 '''show raw text for non-templated mode'''
208 208 def isplain(self):
209 209 '''check for plain formatter usage'''
210 210 return False
211 211 def nested(self, field):
212 212 '''sub formatter to store nested data in the specified field'''
213 213 self._item[field] = data = []
214 214 return _nestedformatter(self._ui, self._converter, data)
215 215 def end(self):
216 216 '''end output for the formatter'''
217 217 if self._item is not None:
218 218 self._showitem()
219 219
220 220 def nullformatter(ui, topic):
221 221 '''formatter that prints nothing'''
222 222 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
223 223
224 224 class _nestedformatter(baseformatter):
225 225 '''build sub items and store them in the parent formatter'''
226 226 def __init__(self, ui, converter, data):
227 227 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
228 228 self._data = data
229 229 def _showitem(self):
230 230 self._data.append(self._item)
231 231
232 232 def _iteritems(data):
233 233 '''iterate key-value pairs in stable order'''
234 234 if isinstance(data, dict):
235 235 return sorted(data.iteritems())
236 236 return data
237 237
238 238 class _plainconverter(object):
239 239 '''convert non-primitive data types to text'''
240 240
241 241 storecontext = False
242 242
243 243 @staticmethod
244 244 def formatdate(date, fmt):
245 245 '''stringify date tuple in the given format'''
246 246 return util.datestr(date, fmt)
247 247 @staticmethod
248 248 def formatdict(data, key, value, fmt, sep):
249 249 '''stringify key-value pairs separated by sep'''
250 250 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
251 251 @staticmethod
252 252 def formatlist(data, name, fmt, sep):
253 253 '''stringify iterable separated by sep'''
254 254 return sep.join(fmt % e for e in data)
255 255
256 256 class plainformatter(baseformatter):
257 257 '''the default text output scheme'''
258 258 def __init__(self, ui, out, topic, opts):
259 259 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
260 260 if ui.debugflag:
261 261 self.hexfunc = hex
262 262 else:
263 263 self.hexfunc = short
264 264 if ui is out:
265 265 self._write = ui.write
266 266 else:
267 267 self._write = lambda s, **opts: out.write(s)
268 268 def startitem(self):
269 269 pass
270 270 def data(self, **data):
271 271 pass
272 272 def write(self, fields, deftext, *fielddata, **opts):
273 273 self._write(deftext % fielddata, **opts)
274 274 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
275 275 '''do conditional write'''
276 276 if cond:
277 277 self._write(deftext % fielddata, **opts)
278 278 def plain(self, text, **opts):
279 279 self._write(text, **opts)
280 280 def isplain(self):
281 281 return True
282 282 def nested(self, field):
283 283 # nested data will be directly written to ui
284 284 return self
285 285 def end(self):
286 286 pass
287 287
288 288 class debugformatter(baseformatter):
289 289 def __init__(self, ui, out, topic, opts):
290 290 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
291 291 self._out = out
292 292 self._out.write("%s = [\n" % self._topic)
293 293 def _showitem(self):
294 self._out.write(' %s,\n' % pycompat.sysbytes(repr(self._item)))
294 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
295 295 def end(self):
296 296 baseformatter.end(self)
297 297 self._out.write("]\n")
298 298
299 299 class pickleformatter(baseformatter):
300 300 def __init__(self, ui, out, topic, opts):
301 301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 302 self._out = out
303 303 self._data = []
304 304 def _showitem(self):
305 305 self._data.append(self._item)
306 306 def end(self):
307 307 baseformatter.end(self)
308 308 self._out.write(pickle.dumps(self._data))
309 309
310 310 class jsonformatter(baseformatter):
311 311 def __init__(self, ui, out, topic, opts):
312 312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 313 self._out = out
314 314 self._out.write("[")
315 315 self._first = True
316 316 def _showitem(self):
317 317 if self._first:
318 318 self._first = False
319 319 else:
320 320 self._out.write(",")
321 321
322 322 self._out.write("\n {\n")
323 323 first = True
324 324 for k, v in sorted(self._item.items()):
325 325 if first:
326 326 first = False
327 327 else:
328 328 self._out.write(",\n")
329 329 u = templatefilters.json(v, paranoid=False)
330 330 self._out.write(' "%s": %s' % (k, u))
331 331 self._out.write("\n }")
332 332 def end(self):
333 333 baseformatter.end(self)
334 334 self._out.write("\n]\n")
335 335
336 336 class _templateconverter(object):
337 337 '''convert non-primitive data types to be processed by templater'''
338 338
339 339 storecontext = True
340 340
341 341 @staticmethod
342 342 def formatdate(date, fmt):
343 343 '''return date tuple'''
344 344 return date
345 345 @staticmethod
346 346 def formatdict(data, key, value, fmt, sep):
347 347 '''build object that can be evaluated as either plain string or dict'''
348 348 data = util.sortdict(_iteritems(data))
349 349 def f():
350 350 yield _plainconverter.formatdict(data, key, value, fmt, sep)
351 351 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
352 352 @staticmethod
353 353 def formatlist(data, name, fmt, sep):
354 354 '''build object that can be evaluated as either plain string or list'''
355 355 data = list(data)
356 356 def f():
357 357 yield _plainconverter.formatlist(data, name, fmt, sep)
358 358 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f)
359 359
360 360 class templateformatter(baseformatter):
361 361 def __init__(self, ui, out, topic, opts):
362 362 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
363 363 self._out = out
364 364 spec = lookuptemplate(ui, topic, opts.get('template', ''))
365 365 self._tref = spec.ref
366 366 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
367 367 resources=templateresources(ui),
368 368 cache=templatekw.defaulttempl)
369 369 self._parts = templatepartsmap(spec, self._t,
370 370 ['docheader', 'docfooter', 'separator'])
371 371 self._counter = itertools.count()
372 372 self._renderitem('docheader', {})
373 373
374 374 def _showitem(self):
375 375 item = self._item.copy()
376 376 item['index'] = index = next(self._counter)
377 377 if index > 0:
378 378 self._renderitem('separator', {})
379 379 self._renderitem(self._tref, item)
380 380
381 381 def _renderitem(self, part, item):
382 382 if part not in self._parts:
383 383 return
384 384 ref = self._parts[part]
385 385
386 386 # TODO: add support for filectx. probably each template keyword or
387 387 # function will have to declare dependent resources. e.g.
388 388 # @templatekeyword(..., requires=('ctx',))
389 389 props = {}
390 390 # explicitly-defined fields precede templatekw
391 391 props.update(item)
392 392 if 'ctx' in item:
393 393 # but template resources must be always available
394 394 props['repo'] = props['ctx'].repo()
395 395 props['revcache'] = {}
396 396 props = pycompat.strkwargs(props)
397 397 g = self._t(ref, **props)
398 398 self._out.write(templater.stringify(g))
399 399
400 400 def end(self):
401 401 baseformatter.end(self)
402 402 self._renderitem('docfooter', {})
403 403
404 404 templatespec = collections.namedtuple(r'templatespec',
405 405 r'ref tmpl mapfile')
406 406
407 407 def lookuptemplate(ui, topic, tmpl):
408 408 """Find the template matching the given -T/--template spec 'tmpl'
409 409
410 410 'tmpl' can be any of the following:
411 411
412 412 - a literal template (e.g. '{rev}')
413 413 - a map-file name or path (e.g. 'changelog')
414 414 - a reference to [templates] in config file
415 415 - a path to raw template file
416 416
417 417 A map file defines a stand-alone template environment. If a map file
418 418 selected, all templates defined in the file will be loaded, and the
419 419 template matching the given topic will be rendered. Aliases won't be
420 420 loaded from user config, but from the map file.
421 421
422 422 If no map file selected, all templates in [templates] section will be
423 423 available as well as aliases in [templatealias].
424 424 """
425 425
426 426 # looks like a literal template?
427 427 if '{' in tmpl:
428 428 return templatespec('', tmpl, None)
429 429
430 430 # perhaps a stock style?
431 431 if not os.path.split(tmpl)[0]:
432 432 mapname = (templater.templatepath('map-cmdline.' + tmpl)
433 433 or templater.templatepath(tmpl))
434 434 if mapname and os.path.isfile(mapname):
435 435 return templatespec(topic, None, mapname)
436 436
437 437 # perhaps it's a reference to [templates]
438 438 if ui.config('templates', tmpl):
439 439 return templatespec(tmpl, None, None)
440 440
441 441 if tmpl == 'list':
442 442 ui.write(_("available styles: %s\n") % templater.stylelist())
443 443 raise error.Abort(_("specify a template"))
444 444
445 445 # perhaps it's a path to a map or a template
446 446 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
447 447 # is it a mapfile for a style?
448 448 if os.path.basename(tmpl).startswith("map-"):
449 449 return templatespec(topic, None, os.path.realpath(tmpl))
450 450 with util.posixfile(tmpl, 'rb') as f:
451 451 tmpl = f.read()
452 452 return templatespec('', tmpl, None)
453 453
454 454 # constant string?
455 455 return templatespec('', tmpl, None)
456 456
457 457 def templatepartsmap(spec, t, partnames):
458 458 """Create a mapping of {part: ref}"""
459 459 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
460 460 if spec.mapfile:
461 461 partsmap.update((p, p) for p in partnames if p in t)
462 462 elif spec.ref:
463 463 for part in partnames:
464 464 ref = '%s:%s' % (spec.ref, part) # select config sub-section
465 465 if ref in t:
466 466 partsmap[part] = ref
467 467 return partsmap
468 468
469 469 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
470 470 """Create a templater from either a literal template or loading from
471 471 a map file"""
472 472 assert not (spec.tmpl and spec.mapfile)
473 473 if spec.mapfile:
474 474 frommapfile = templater.templater.frommapfile
475 475 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
476 476 cache=cache)
477 477 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
478 478 cache=cache)
479 479
480 480 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
481 481 """Create a templater from a string template 'tmpl'"""
482 482 aliases = ui.configitems('templatealias')
483 483 t = templater.templater(defaults=defaults, resources=resources,
484 484 cache=cache, aliases=aliases)
485 485 t.cache.update((k, templater.unquotestring(v))
486 486 for k, v in ui.configitems('templates'))
487 487 if tmpl:
488 488 t.cache[''] = tmpl
489 489 return t
490 490
491 491 def templateresources(ui, repo=None):
492 492 """Create a dict of template resources designed for the default templatekw
493 493 and function"""
494 494 return {
495 495 'cache': {}, # for templatekw/funcs to store reusable data
496 496 'ctx': None,
497 497 'repo': repo,
498 498 'revcache': None, # per-ctx cache; set later
499 499 'ui': ui,
500 500 }
501 501
502 502 def formatter(ui, out, topic, opts):
503 503 template = opts.get("template", "")
504 504 if template == "json":
505 505 return jsonformatter(ui, out, topic, opts)
506 506 elif template == "pickle":
507 507 return pickleformatter(ui, out, topic, opts)
508 508 elif template == "debug":
509 509 return debugformatter(ui, out, topic, opts)
510 510 elif template != "":
511 511 return templateformatter(ui, out, topic, opts)
512 512 # developer config: ui.formatdebug
513 513 elif ui.configbool('ui', 'formatdebug'):
514 514 return debugformatter(ui, out, topic, opts)
515 515 # deprecated config: ui.formatjson
516 516 elif ui.configbool('ui', 'formatjson'):
517 517 return jsonformatter(ui, out, topic, opts)
518 518 return plainformatter(ui, out, topic, opts)
519 519
520 520 @contextlib.contextmanager
521 521 def openformatter(ui, filename, topic, opts):
522 522 """Create a formatter that writes outputs to the specified file
523 523
524 524 Must be invoked using the 'with' statement.
525 525 """
526 526 with util.posixfile(filename, 'wb') as out:
527 527 with formatter(ui, out, topic, opts) as fm:
528 528 yield fm
529 529
530 530 @contextlib.contextmanager
531 531 def _neverending(fm):
532 532 yield fm
533 533
534 534 def maybereopen(fm, filename, opts):
535 535 """Create a formatter backed by file if filename specified, else return
536 536 the given formatter
537 537
538 538 Must be invoked using the 'with' statement. This will never call fm.end()
539 539 of the given formatter.
540 540 """
541 541 if filename:
542 542 return openformatter(fm._ui, filename, fm._topic, opts)
543 543 else:
544 544 return _neverending(fm)
@@ -1,351 +1,353 b''
1 1 # pycompat.py - portability shim for python 3
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """Mercurial portability shim for python 3.
7 7
8 8 This contains aliases to hide python version-specific details from the core.
9 9 """
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import getopt
14 14 import inspect
15 15 import os
16 16 import shlex
17 17 import sys
18 18
19 19 ispy3 = (sys.version_info[0] >= 3)
20 20 ispypy = (r'__pypy__' in sys.builtin_module_names)
21 21
22 22 if not ispy3:
23 23 import cookielib
24 24 import cPickle as pickle
25 25 import httplib
26 26 import Queue as _queue
27 27 import SocketServer as socketserver
28 28 import xmlrpclib
29 29 else:
30 30 import http.cookiejar as cookielib
31 31 import http.client as httplib
32 32 import pickle
33 33 import queue as _queue
34 34 import socketserver
35 35 import xmlrpc.client as xmlrpclib
36 36
37 37 empty = _queue.Empty
38 38 queue = _queue.Queue
39 39
40 40 def identity(a):
41 41 return a
42 42
43 43 if ispy3:
44 44 import builtins
45 45 import functools
46 46 import io
47 47 import struct
48 48
49 49 fsencode = os.fsencode
50 50 fsdecode = os.fsdecode
51 51 oslinesep = os.linesep.encode('ascii')
52 52 osname = os.name.encode('ascii')
53 53 ospathsep = os.pathsep.encode('ascii')
54 54 ossep = os.sep.encode('ascii')
55 55 osaltsep = os.altsep
56 56 if osaltsep:
57 57 osaltsep = osaltsep.encode('ascii')
58 58 # os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
59 59 # returns bytes.
60 60 getcwd = os.getcwdb
61 61 sysplatform = sys.platform.encode('ascii')
62 62 sysexecutable = sys.executable
63 63 if sysexecutable:
64 64 sysexecutable = os.fsencode(sysexecutable)
65 65 stringio = io.BytesIO
66 66 maplist = lambda *args: list(map(*args))
67 67 ziplist = lambda *args: list(zip(*args))
68 68 rawinput = input
69 69 getargspec = inspect.getfullargspec
70 70
71 71 # TODO: .buffer might not exist if std streams were replaced; we'll need
72 72 # a silly wrapper to make a bytes stream backed by a unicode one.
73 73 stdin = sys.stdin.buffer
74 74 stdout = sys.stdout.buffer
75 75 stderr = sys.stderr.buffer
76 76
77 77 # Since Python 3 converts argv to wchar_t type by Py_DecodeLocale() on Unix,
78 78 # we can use os.fsencode() to get back bytes argv.
79 79 #
80 80 # https://hg.python.org/cpython/file/v3.5.1/Programs/python.c#l55
81 81 #
82 82 # TODO: On Windows, the native argv is wchar_t, so we'll need a different
83 83 # workaround to simulate the Python 2 (i.e. ANSI Win32 API) behavior.
84 84 if getattr(sys, 'argv', None) is not None:
85 85 sysargv = list(map(os.fsencode, sys.argv))
86 86
87 87 bytechr = struct.Struct('>B').pack
88 byterepr = b'%r'.__mod__
88 89
89 90 class bytestr(bytes):
90 91 """A bytes which mostly acts as a Python 2 str
91 92
92 93 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
93 94 ('', 'foo', 'ascii', '1')
94 95 >>> s = bytestr(b'foo')
95 96 >>> assert s is bytestr(s)
96 97
97 98 __bytes__() should be called if provided:
98 99
99 100 >>> class bytesable(object):
100 101 ... def __bytes__(self):
101 102 ... return b'bytes'
102 103 >>> bytestr(bytesable())
103 104 'bytes'
104 105
105 106 There's no implicit conversion from non-ascii str as its encoding is
106 107 unknown:
107 108
108 109 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
109 110 Traceback (most recent call last):
110 111 ...
111 112 UnicodeEncodeError: ...
112 113
113 114 Comparison between bytestr and bytes should work:
114 115
115 116 >>> assert bytestr(b'foo') == b'foo'
116 117 >>> assert b'foo' == bytestr(b'foo')
117 118 >>> assert b'f' in bytestr(b'foo')
118 119 >>> assert bytestr(b'f') in b'foo'
119 120
120 121 Sliced elements should be bytes, not integer:
121 122
122 123 >>> s[1], s[:2]
123 124 (b'o', b'fo')
124 125 >>> list(s), list(reversed(s))
125 126 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
126 127
127 128 As bytestr type isn't propagated across operations, you need to cast
128 129 bytes to bytestr explicitly:
129 130
130 131 >>> s = bytestr(b'foo').upper()
131 132 >>> t = bytestr(s)
132 133 >>> s[0], t[0]
133 134 (70, b'F')
134 135
135 136 Be careful to not pass a bytestr object to a function which expects
136 137 bytearray-like behavior.
137 138
138 139 >>> t = bytes(t) # cast to bytes
139 140 >>> assert type(t) is bytes
140 141 """
141 142
142 143 def __new__(cls, s=b''):
143 144 if isinstance(s, bytestr):
144 145 return s
145 146 if (not isinstance(s, (bytes, bytearray))
146 147 and not hasattr(s, u'__bytes__')): # hasattr-py3-only
147 148 s = str(s).encode(u'ascii')
148 149 return bytes.__new__(cls, s)
149 150
150 151 def __getitem__(self, key):
151 152 s = bytes.__getitem__(self, key)
152 153 if not isinstance(s, bytes):
153 154 s = bytechr(s)
154 155 return s
155 156
156 157 def __iter__(self):
157 158 return iterbytestr(bytes.__iter__(self))
158 159
159 160 def __repr__(self):
160 161 return bytes.__repr__(self)[1:] # drop b''
161 162
162 163 def iterbytestr(s):
163 164 """Iterate bytes as if it were a str object of Python 2"""
164 165 return map(bytechr, s)
165 166
166 167 def maybebytestr(s):
167 168 """Promote bytes to bytestr"""
168 169 if isinstance(s, bytes):
169 170 return bytestr(s)
170 171 return s
171 172
172 173 def sysbytes(s):
173 174 """Convert an internal str (e.g. keyword, __doc__) back to bytes
174 175
175 176 This never raises UnicodeEncodeError, but only ASCII characters
176 177 can be round-trip by sysstr(sysbytes(s)).
177 178 """
178 179 return s.encode(u'utf-8')
179 180
180 181 def sysstr(s):
181 182 """Return a keyword str to be passed to Python functions such as
182 183 getattr() and str.encode()
183 184
184 185 This never raises UnicodeDecodeError. Non-ascii characters are
185 186 considered invalid and mapped to arbitrary but unique code points
186 187 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
187 188 """
188 189 if isinstance(s, builtins.str):
189 190 return s
190 191 return s.decode(u'latin-1')
191 192
192 193 def strurl(url):
193 194 """Converts a bytes url back to str"""
194 195 return url.decode(u'ascii')
195 196
196 197 def bytesurl(url):
197 198 """Converts a str url to bytes by encoding in ascii"""
198 199 return url.encode(u'ascii')
199 200
200 201 def raisewithtb(exc, tb):
201 202 """Raise exception with the given traceback"""
202 203 raise exc.with_traceback(tb)
203 204
204 205 def getdoc(obj):
205 206 """Get docstring as bytes; may be None so gettext() won't confuse it
206 207 with _('')"""
207 208 doc = getattr(obj, u'__doc__', None)
208 209 if doc is None:
209 210 return doc
210 211 return sysbytes(doc)
211 212
212 213 def _wrapattrfunc(f):
213 214 @functools.wraps(f)
214 215 def w(object, name, *args):
215 216 return f(object, sysstr(name), *args)
216 217 return w
217 218
218 219 # these wrappers are automagically imported by hgloader
219 220 delattr = _wrapattrfunc(builtins.delattr)
220 221 getattr = _wrapattrfunc(builtins.getattr)
221 222 hasattr = _wrapattrfunc(builtins.hasattr)
222 223 setattr = _wrapattrfunc(builtins.setattr)
223 224 xrange = builtins.range
224 225 unicode = str
225 226
226 227 def open(name, mode='r', buffering=-1):
227 228 return builtins.open(name, sysstr(mode), buffering)
228 229
229 230 def _getoptbwrapper(orig, args, shortlist, namelist):
230 231 """
231 232 Takes bytes arguments, converts them to unicode, pass them to
232 233 getopt.getopt(), convert the returned values back to bytes and then
233 234 return them for Python 3 compatibility as getopt.getopt() don't accepts
234 235 bytes on Python 3.
235 236 """
236 237 args = [a.decode('latin-1') for a in args]
237 238 shortlist = shortlist.decode('latin-1')
238 239 namelist = [a.decode('latin-1') for a in namelist]
239 240 opts, args = orig(args, shortlist, namelist)
240 241 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1'))
241 242 for a in opts]
242 243 args = [a.encode('latin-1') for a in args]
243 244 return opts, args
244 245
245 246 def strkwargs(dic):
246 247 """
247 248 Converts the keys of a python dictonary to str i.e. unicodes so that
248 249 they can be passed as keyword arguments as dictonaries with bytes keys
249 250 can't be passed as keyword arguments to functions on Python 3.
250 251 """
251 252 dic = dict((k.decode('latin-1'), v) for k, v in dic.iteritems())
252 253 return dic
253 254
254 255 def byteskwargs(dic):
255 256 """
256 257 Converts keys of python dictonaries to bytes as they were converted to
257 258 str to pass that dictonary as a keyword argument on Python 3.
258 259 """
259 260 dic = dict((k.encode('latin-1'), v) for k, v in dic.iteritems())
260 261 return dic
261 262
262 263 # TODO: handle shlex.shlex().
263 264 def shlexsplit(s):
264 265 """
265 266 Takes bytes argument, convert it to str i.e. unicodes, pass that into
266 267 shlex.split(), convert the returned value to bytes and return that for
267 268 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
268 269 """
269 270 ret = shlex.split(s.decode('latin-1'))
270 271 return [a.encode('latin-1') for a in ret]
271 272
272 273 def emailparser(*args, **kwargs):
273 274 import email.parser
274 275 return email.parser.BytesParser(*args, **kwargs)
275 276
276 277 else:
277 278 import cStringIO
278 279
279 280 bytechr = chr
281 byterepr = repr
280 282 bytestr = str
281 283 iterbytestr = iter
282 284 maybebytestr = identity
283 285 sysbytes = identity
284 286 sysstr = identity
285 287 strurl = identity
286 288 bytesurl = identity
287 289
288 290 # this can't be parsed on Python 3
289 291 exec('def raisewithtb(exc, tb):\n'
290 292 ' raise exc, None, tb\n')
291 293
292 294 def fsencode(filename):
293 295 """
294 296 Partial backport from os.py in Python 3, which only accepts bytes.
295 297 In Python 2, our paths should only ever be bytes, a unicode path
296 298 indicates a bug.
297 299 """
298 300 if isinstance(filename, str):
299 301 return filename
300 302 else:
301 303 raise TypeError(
302 304 "expect str, not %s" % type(filename).__name__)
303 305
304 306 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
305 307 # better not to touch Python 2 part as it's already working fine.
306 308 fsdecode = identity
307 309
308 310 def getdoc(obj):
309 311 return getattr(obj, '__doc__', None)
310 312
311 313 def _getoptbwrapper(orig, args, shortlist, namelist):
312 314 return orig(args, shortlist, namelist)
313 315
314 316 strkwargs = identity
315 317 byteskwargs = identity
316 318
317 319 oslinesep = os.linesep
318 320 osname = os.name
319 321 ospathsep = os.pathsep
320 322 ossep = os.sep
321 323 osaltsep = os.altsep
322 324 stdin = sys.stdin
323 325 stdout = sys.stdout
324 326 stderr = sys.stderr
325 327 if getattr(sys, 'argv', None) is not None:
326 328 sysargv = sys.argv
327 329 sysplatform = sys.platform
328 330 getcwd = os.getcwd
329 331 sysexecutable = sys.executable
330 332 shlexsplit = shlex.split
331 333 stringio = cStringIO.StringIO
332 334 maplist = map
333 335 ziplist = zip
334 336 rawinput = raw_input
335 337 getargspec = inspect.getargspec
336 338
337 339 def emailparser(*args, **kwargs):
338 340 import email.parser
339 341 return email.parser.Parser(*args, **kwargs)
340 342
341 343 isjython = sysplatform.startswith('java')
342 344
343 345 isdarwin = sysplatform == 'darwin'
344 346 isposix = osname == 'posix'
345 347 iswindows = osname == 'nt'
346 348
347 349 def getoptb(args, shortlist, namelist):
348 350 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
349 351
350 352 def gnugetoptb(args, shortlist, namelist):
351 353 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
@@ -1,1145 +1,1145 b''
1 1 # smartset.py - data structure for revision set
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
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 from . import (
11 11 encoding,
12 12 error,
13 13 pycompat,
14 14 util,
15 15 )
16 16
17 17 def _formatsetrepr(r):
18 18 """Format an optional printable representation of a set
19 19
20 20 ======== =================================
21 21 type(r) example
22 22 ======== =================================
23 23 tuple ('<not %r>', other)
24 24 bytes '<branch closed>'
25 25 callable lambda: '<branch %r>' % sorted(b)
26 26 object other
27 27 ======== =================================
28 28 """
29 29 if r is None:
30 30 return ''
31 31 elif isinstance(r, tuple):
32 32 return r[0] % util.rapply(pycompat.maybebytestr, r[1:])
33 33 elif isinstance(r, bytes):
34 34 return r
35 35 elif callable(r):
36 36 return r()
37 37 else:
38 return pycompat.sysbytes(repr(r))
38 return pycompat.byterepr(r)
39 39
40 40 def _typename(o):
41 41 return pycompat.sysbytes(type(o).__name__).lstrip('_')
42 42
43 43 class abstractsmartset(object):
44 44
45 45 def __nonzero__(self):
46 46 """True if the smartset is not empty"""
47 47 raise NotImplementedError()
48 48
49 49 __bool__ = __nonzero__
50 50
51 51 def __contains__(self, rev):
52 52 """provide fast membership testing"""
53 53 raise NotImplementedError()
54 54
55 55 def __iter__(self):
56 56 """iterate the set in the order it is supposed to be iterated"""
57 57 raise NotImplementedError()
58 58
59 59 # Attributes containing a function to perform a fast iteration in a given
60 60 # direction. A smartset can have none, one, or both defined.
61 61 #
62 62 # Default value is None instead of a function returning None to avoid
63 63 # initializing an iterator just for testing if a fast method exists.
64 64 fastasc = None
65 65 fastdesc = None
66 66
67 67 def isascending(self):
68 68 """True if the set will iterate in ascending order"""
69 69 raise NotImplementedError()
70 70
71 71 def isdescending(self):
72 72 """True if the set will iterate in descending order"""
73 73 raise NotImplementedError()
74 74
75 75 def istopo(self):
76 76 """True if the set will iterate in topographical order"""
77 77 raise NotImplementedError()
78 78
79 79 def min(self):
80 80 """return the minimum element in the set"""
81 81 if self.fastasc is None:
82 82 v = min(self)
83 83 else:
84 84 for v in self.fastasc():
85 85 break
86 86 else:
87 87 raise ValueError('arg is an empty sequence')
88 88 self.min = lambda: v
89 89 return v
90 90
91 91 def max(self):
92 92 """return the maximum element in the set"""
93 93 if self.fastdesc is None:
94 94 return max(self)
95 95 else:
96 96 for v in self.fastdesc():
97 97 break
98 98 else:
99 99 raise ValueError('arg is an empty sequence')
100 100 self.max = lambda: v
101 101 return v
102 102
103 103 def first(self):
104 104 """return the first element in the set (user iteration perspective)
105 105
106 106 Return None if the set is empty"""
107 107 raise NotImplementedError()
108 108
109 109 def last(self):
110 110 """return the last element in the set (user iteration perspective)
111 111
112 112 Return None if the set is empty"""
113 113 raise NotImplementedError()
114 114
115 115 def __len__(self):
116 116 """return the length of the smartsets
117 117
118 118 This can be expensive on smartset that could be lazy otherwise."""
119 119 raise NotImplementedError()
120 120
121 121 def reverse(self):
122 122 """reverse the expected iteration order"""
123 123 raise NotImplementedError()
124 124
125 125 def sort(self, reverse=False):
126 126 """get the set to iterate in an ascending or descending order"""
127 127 raise NotImplementedError()
128 128
129 129 def __and__(self, other):
130 130 """Returns a new object with the intersection of the two collections.
131 131
132 132 This is part of the mandatory API for smartset."""
133 133 if isinstance(other, fullreposet):
134 134 return self
135 135 return self.filter(other.__contains__, condrepr=other, cache=False)
136 136
137 137 def __add__(self, other):
138 138 """Returns a new object with the union of the two collections.
139 139
140 140 This is part of the mandatory API for smartset."""
141 141 return addset(self, other)
142 142
143 143 def __sub__(self, other):
144 144 """Returns a new object with the substraction of the two collections.
145 145
146 146 This is part of the mandatory API for smartset."""
147 147 c = other.__contains__
148 148 return self.filter(lambda r: not c(r), condrepr=('<not %r>', other),
149 149 cache=False)
150 150
151 151 def filter(self, condition, condrepr=None, cache=True):
152 152 """Returns this smartset filtered by condition as a new smartset.
153 153
154 154 `condition` is a callable which takes a revision number and returns a
155 155 boolean. Optional `condrepr` provides a printable representation of
156 156 the given `condition`.
157 157
158 158 This is part of the mandatory API for smartset."""
159 159 # builtin cannot be cached. but do not needs to
160 160 if cache and util.safehasattr(condition, 'func_code'):
161 161 condition = util.cachefunc(condition)
162 162 return filteredset(self, condition, condrepr)
163 163
164 164 def slice(self, start, stop):
165 165 """Return new smartset that contains selected elements from this set"""
166 166 if start < 0 or stop < 0:
167 167 raise error.ProgrammingError('negative index not allowed')
168 168 return self._slice(start, stop)
169 169
170 170 def _slice(self, start, stop):
171 171 # sub classes may override this. start and stop must not be negative,
172 172 # but start > stop is allowed, which should be an empty set.
173 173 ys = []
174 174 it = iter(self)
175 175 for x in xrange(start):
176 176 y = next(it, None)
177 177 if y is None:
178 178 break
179 179 for x in xrange(stop - start):
180 180 y = next(it, None)
181 181 if y is None:
182 182 break
183 183 ys.append(y)
184 184 return baseset(ys, datarepr=('slice=%d:%d %r', start, stop, self))
185 185
186 186 class baseset(abstractsmartset):
187 187 """Basic data structure that represents a revset and contains the basic
188 188 operation that it should be able to perform.
189 189
190 190 Every method in this class should be implemented by any smartset class.
191 191
192 192 This class could be constructed by an (unordered) set, or an (ordered)
193 193 list-like object. If a set is provided, it'll be sorted lazily.
194 194
195 195 >>> x = [4, 0, 7, 6]
196 196 >>> y = [5, 6, 7, 3]
197 197
198 198 Construct by a set:
199 199 >>> xs = baseset(set(x))
200 200 >>> ys = baseset(set(y))
201 201 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
202 202 [[0, 4, 6, 7, 3, 5], [6, 7], [0, 4]]
203 203 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
204 204 ['addset', 'baseset', 'baseset']
205 205
206 206 Construct by a list-like:
207 207 >>> xs = baseset(x)
208 208 >>> ys = baseset(i for i in y)
209 209 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
210 210 [[4, 0, 7, 6, 5, 3], [7, 6], [4, 0]]
211 211 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
212 212 ['addset', 'filteredset', 'filteredset']
213 213
214 214 Populate "_set" fields in the lists so set optimization may be used:
215 215 >>> [1 in xs, 3 in ys]
216 216 [False, True]
217 217
218 218 Without sort(), results won't be changed:
219 219 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
220 220 [[4, 0, 7, 6, 5, 3], [7, 6], [4, 0]]
221 221 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
222 222 ['addset', 'filteredset', 'filteredset']
223 223
224 224 With sort(), set optimization could be used:
225 225 >>> xs.sort(reverse=True)
226 226 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
227 227 [[7, 6, 4, 0, 5, 3], [7, 6], [4, 0]]
228 228 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
229 229 ['addset', 'baseset', 'baseset']
230 230
231 231 >>> ys.sort()
232 232 >>> [list(i) for i in [xs + ys, xs & ys, xs - ys]]
233 233 [[7, 6, 4, 0, 3, 5], [7, 6], [4, 0]]
234 234 >>> [type(i).__name__ for i in [xs + ys, xs & ys, xs - ys]]
235 235 ['addset', 'baseset', 'baseset']
236 236
237 237 istopo is preserved across set operations
238 238 >>> xs = baseset(set(x), istopo=True)
239 239 >>> rs = xs & ys
240 240 >>> type(rs).__name__
241 241 'baseset'
242 242 >>> rs._istopo
243 243 True
244 244 """
245 245 def __init__(self, data=(), datarepr=None, istopo=False):
246 246 """
247 247 datarepr: a tuple of (format, obj, ...), a function or an object that
248 248 provides a printable representation of the given data.
249 249 """
250 250 self._ascending = None
251 251 self._istopo = istopo
252 252 if isinstance(data, set):
253 253 # converting set to list has a cost, do it lazily
254 254 self._set = data
255 255 # set has no order we pick one for stability purpose
256 256 self._ascending = True
257 257 else:
258 258 if not isinstance(data, list):
259 259 data = list(data)
260 260 self._list = data
261 261 self._datarepr = datarepr
262 262
263 263 @util.propertycache
264 264 def _set(self):
265 265 return set(self._list)
266 266
267 267 @util.propertycache
268 268 def _asclist(self):
269 269 asclist = self._list[:]
270 270 asclist.sort()
271 271 return asclist
272 272
273 273 @util.propertycache
274 274 def _list(self):
275 275 # _list is only lazily constructed if we have _set
276 276 assert r'_set' in self.__dict__
277 277 return list(self._set)
278 278
279 279 def __iter__(self):
280 280 if self._ascending is None:
281 281 return iter(self._list)
282 282 elif self._ascending:
283 283 return iter(self._asclist)
284 284 else:
285 285 return reversed(self._asclist)
286 286
287 287 def fastasc(self):
288 288 return iter(self._asclist)
289 289
290 290 def fastdesc(self):
291 291 return reversed(self._asclist)
292 292
293 293 @util.propertycache
294 294 def __contains__(self):
295 295 return self._set.__contains__
296 296
297 297 def __nonzero__(self):
298 298 return bool(len(self))
299 299
300 300 __bool__ = __nonzero__
301 301
302 302 def sort(self, reverse=False):
303 303 self._ascending = not bool(reverse)
304 304 self._istopo = False
305 305
306 306 def reverse(self):
307 307 if self._ascending is None:
308 308 self._list.reverse()
309 309 else:
310 310 self._ascending = not self._ascending
311 311 self._istopo = False
312 312
313 313 def __len__(self):
314 314 if r'_list' in self.__dict__:
315 315 return len(self._list)
316 316 else:
317 317 return len(self._set)
318 318
319 319 def isascending(self):
320 320 """Returns True if the collection is ascending order, False if not.
321 321
322 322 This is part of the mandatory API for smartset."""
323 323 if len(self) <= 1:
324 324 return True
325 325 return self._ascending is not None and self._ascending
326 326
327 327 def isdescending(self):
328 328 """Returns True if the collection is descending order, False if not.
329 329
330 330 This is part of the mandatory API for smartset."""
331 331 if len(self) <= 1:
332 332 return True
333 333 return self._ascending is not None and not self._ascending
334 334
335 335 def istopo(self):
336 336 """Is the collection is in topographical order or not.
337 337
338 338 This is part of the mandatory API for smartset."""
339 339 if len(self) <= 1:
340 340 return True
341 341 return self._istopo
342 342
343 343 def first(self):
344 344 if self:
345 345 if self._ascending is None:
346 346 return self._list[0]
347 347 elif self._ascending:
348 348 return self._asclist[0]
349 349 else:
350 350 return self._asclist[-1]
351 351 return None
352 352
353 353 def last(self):
354 354 if self:
355 355 if self._ascending is None:
356 356 return self._list[-1]
357 357 elif self._ascending:
358 358 return self._asclist[-1]
359 359 else:
360 360 return self._asclist[0]
361 361 return None
362 362
363 363 def _fastsetop(self, other, op):
364 364 # try to use native set operations as fast paths
365 365 if (type(other) is baseset and r'_set' in other.__dict__ and r'_set' in
366 366 self.__dict__ and self._ascending is not None):
367 367 s = baseset(data=getattr(self._set, op)(other._set),
368 368 istopo=self._istopo)
369 369 s._ascending = self._ascending
370 370 else:
371 371 s = getattr(super(baseset, self), op)(other)
372 372 return s
373 373
374 374 def __and__(self, other):
375 375 return self._fastsetop(other, '__and__')
376 376
377 377 def __sub__(self, other):
378 378 return self._fastsetop(other, '__sub__')
379 379
380 380 def _slice(self, start, stop):
381 381 # creating new list should be generally cheaper than iterating items
382 382 if self._ascending is None:
383 383 return baseset(self._list[start:stop], istopo=self._istopo)
384 384
385 385 data = self._asclist
386 386 if not self._ascending:
387 387 start, stop = max(len(data) - stop, 0), max(len(data) - start, 0)
388 388 s = baseset(data[start:stop], istopo=self._istopo)
389 389 s._ascending = self._ascending
390 390 return s
391 391
392 392 @encoding.strmethod
393 393 def __repr__(self):
394 394 d = {None: '', False: '-', True: '+'}[self._ascending]
395 395 s = _formatsetrepr(self._datarepr)
396 396 if not s:
397 397 l = self._list
398 398 # if _list has been built from a set, it might have a different
399 399 # order from one python implementation to another.
400 400 # We fallback to the sorted version for a stable output.
401 401 if self._ascending is not None:
402 402 l = self._asclist
403 s = pycompat.sysbytes(repr(l))
403 s = pycompat.byterepr(l)
404 404 return '<%s%s %s>' % (_typename(self), d, s)
405 405
406 406 class filteredset(abstractsmartset):
407 407 """Duck type for baseset class which iterates lazily over the revisions in
408 408 the subset and contains a function which tests for membership in the
409 409 revset
410 410 """
411 411 def __init__(self, subset, condition=lambda x: True, condrepr=None):
412 412 """
413 413 condition: a function that decide whether a revision in the subset
414 414 belongs to the revset or not.
415 415 condrepr: a tuple of (format, obj, ...), a function or an object that
416 416 provides a printable representation of the given condition.
417 417 """
418 418 self._subset = subset
419 419 self._condition = condition
420 420 self._condrepr = condrepr
421 421
422 422 def __contains__(self, x):
423 423 return x in self._subset and self._condition(x)
424 424
425 425 def __iter__(self):
426 426 return self._iterfilter(self._subset)
427 427
428 428 def _iterfilter(self, it):
429 429 cond = self._condition
430 430 for x in it:
431 431 if cond(x):
432 432 yield x
433 433
434 434 @property
435 435 def fastasc(self):
436 436 it = self._subset.fastasc
437 437 if it is None:
438 438 return None
439 439 return lambda: self._iterfilter(it())
440 440
441 441 @property
442 442 def fastdesc(self):
443 443 it = self._subset.fastdesc
444 444 if it is None:
445 445 return None
446 446 return lambda: self._iterfilter(it())
447 447
448 448 def __nonzero__(self):
449 449 fast = None
450 450 candidates = [self.fastasc if self.isascending() else None,
451 451 self.fastdesc if self.isdescending() else None,
452 452 self.fastasc,
453 453 self.fastdesc]
454 454 for candidate in candidates:
455 455 if candidate is not None:
456 456 fast = candidate
457 457 break
458 458
459 459 if fast is not None:
460 460 it = fast()
461 461 else:
462 462 it = self
463 463
464 464 for r in it:
465 465 return True
466 466 return False
467 467
468 468 __bool__ = __nonzero__
469 469
470 470 def __len__(self):
471 471 # Basic implementation to be changed in future patches.
472 472 # until this gets improved, we use generator expression
473 473 # here, since list comprehensions are free to call __len__ again
474 474 # causing infinite recursion
475 475 l = baseset(r for r in self)
476 476 return len(l)
477 477
478 478 def sort(self, reverse=False):
479 479 self._subset.sort(reverse=reverse)
480 480
481 481 def reverse(self):
482 482 self._subset.reverse()
483 483
484 484 def isascending(self):
485 485 return self._subset.isascending()
486 486
487 487 def isdescending(self):
488 488 return self._subset.isdescending()
489 489
490 490 def istopo(self):
491 491 return self._subset.istopo()
492 492
493 493 def first(self):
494 494 for x in self:
495 495 return x
496 496 return None
497 497
498 498 def last(self):
499 499 it = None
500 500 if self.isascending():
501 501 it = self.fastdesc
502 502 elif self.isdescending():
503 503 it = self.fastasc
504 504 if it is not None:
505 505 for x in it():
506 506 return x
507 507 return None #empty case
508 508 else:
509 509 x = None
510 510 for x in self:
511 511 pass
512 512 return x
513 513
514 514 @encoding.strmethod
515 515 def __repr__(self):
516 xs = [pycompat.sysbytes(repr(self._subset))]
516 xs = [pycompat.byterepr(self._subset)]
517 517 s = _formatsetrepr(self._condrepr)
518 518 if s:
519 519 xs.append(s)
520 520 return '<%s %s>' % (_typename(self), ', '.join(xs))
521 521
522 522 def _iterordered(ascending, iter1, iter2):
523 523 """produce an ordered iteration from two iterators with the same order
524 524
525 525 The ascending is used to indicated the iteration direction.
526 526 """
527 527 choice = max
528 528 if ascending:
529 529 choice = min
530 530
531 531 val1 = None
532 532 val2 = None
533 533 try:
534 534 # Consume both iterators in an ordered way until one is empty
535 535 while True:
536 536 if val1 is None:
537 537 val1 = next(iter1)
538 538 if val2 is None:
539 539 val2 = next(iter2)
540 540 n = choice(val1, val2)
541 541 yield n
542 542 if val1 == n:
543 543 val1 = None
544 544 if val2 == n:
545 545 val2 = None
546 546 except StopIteration:
547 547 # Flush any remaining values and consume the other one
548 548 it = iter2
549 549 if val1 is not None:
550 550 yield val1
551 551 it = iter1
552 552 elif val2 is not None:
553 553 # might have been equality and both are empty
554 554 yield val2
555 555 for val in it:
556 556 yield val
557 557
558 558 class addset(abstractsmartset):
559 559 """Represent the addition of two sets
560 560
561 561 Wrapper structure for lazily adding two structures without losing much
562 562 performance on the __contains__ method
563 563
564 564 If the ascending attribute is set, that means the two structures are
565 565 ordered in either an ascending or descending way. Therefore, we can add
566 566 them maintaining the order by iterating over both at the same time
567 567
568 568 >>> xs = baseset([0, 3, 2])
569 569 >>> ys = baseset([5, 2, 4])
570 570
571 571 >>> rs = addset(xs, ys)
572 572 >>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last()
573 573 (True, True, False, True, 0, 4)
574 574 >>> rs = addset(xs, baseset([]))
575 575 >>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last()
576 576 (True, True, False, 0, 2)
577 577 >>> rs = addset(baseset([]), baseset([]))
578 578 >>> bool(rs), 0 in rs, rs.first(), rs.last()
579 579 (False, False, None, None)
580 580
581 581 iterate unsorted:
582 582 >>> rs = addset(xs, ys)
583 583 >>> # (use generator because pypy could call len())
584 584 >>> list(x for x in rs) # without _genlist
585 585 [0, 3, 2, 5, 4]
586 586 >>> assert not rs._genlist
587 587 >>> len(rs)
588 588 5
589 589 >>> [x for x in rs] # with _genlist
590 590 [0, 3, 2, 5, 4]
591 591 >>> assert rs._genlist
592 592
593 593 iterate ascending:
594 594 >>> rs = addset(xs, ys, ascending=True)
595 595 >>> # (use generator because pypy could call len())
596 596 >>> list(x for x in rs), list(x for x in rs.fastasc()) # without _asclist
597 597 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
598 598 >>> assert not rs._asclist
599 599 >>> len(rs)
600 600 5
601 601 >>> [x for x in rs], [x for x in rs.fastasc()]
602 602 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
603 603 >>> assert rs._asclist
604 604
605 605 iterate descending:
606 606 >>> rs = addset(xs, ys, ascending=False)
607 607 >>> # (use generator because pypy could call len())
608 608 >>> list(x for x in rs), list(x for x in rs.fastdesc()) # without _asclist
609 609 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
610 610 >>> assert not rs._asclist
611 611 >>> len(rs)
612 612 5
613 613 >>> [x for x in rs], [x for x in rs.fastdesc()]
614 614 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
615 615 >>> assert rs._asclist
616 616
617 617 iterate ascending without fastasc:
618 618 >>> rs = addset(xs, generatorset(ys), ascending=True)
619 619 >>> assert rs.fastasc is None
620 620 >>> [x for x in rs]
621 621 [0, 2, 3, 4, 5]
622 622
623 623 iterate descending without fastdesc:
624 624 >>> rs = addset(generatorset(xs), ys, ascending=False)
625 625 >>> assert rs.fastdesc is None
626 626 >>> [x for x in rs]
627 627 [5, 4, 3, 2, 0]
628 628 """
629 629 def __init__(self, revs1, revs2, ascending=None):
630 630 self._r1 = revs1
631 631 self._r2 = revs2
632 632 self._iter = None
633 633 self._ascending = ascending
634 634 self._genlist = None
635 635 self._asclist = None
636 636
637 637 def __len__(self):
638 638 return len(self._list)
639 639
640 640 def __nonzero__(self):
641 641 return bool(self._r1) or bool(self._r2)
642 642
643 643 __bool__ = __nonzero__
644 644
645 645 @util.propertycache
646 646 def _list(self):
647 647 if not self._genlist:
648 648 self._genlist = baseset(iter(self))
649 649 return self._genlist
650 650
651 651 def __iter__(self):
652 652 """Iterate over both collections without repeating elements
653 653
654 654 If the ascending attribute is not set, iterate over the first one and
655 655 then over the second one checking for membership on the first one so we
656 656 dont yield any duplicates.
657 657
658 658 If the ascending attribute is set, iterate over both collections at the
659 659 same time, yielding only one value at a time in the given order.
660 660 """
661 661 if self._ascending is None:
662 662 if self._genlist:
663 663 return iter(self._genlist)
664 664 def arbitraryordergen():
665 665 for r in self._r1:
666 666 yield r
667 667 inr1 = self._r1.__contains__
668 668 for r in self._r2:
669 669 if not inr1(r):
670 670 yield r
671 671 return arbitraryordergen()
672 672 # try to use our own fast iterator if it exists
673 673 self._trysetasclist()
674 674 if self._ascending:
675 675 attr = 'fastasc'
676 676 else:
677 677 attr = 'fastdesc'
678 678 it = getattr(self, attr)
679 679 if it is not None:
680 680 return it()
681 681 # maybe half of the component supports fast
682 682 # get iterator for _r1
683 683 iter1 = getattr(self._r1, attr)
684 684 if iter1 is None:
685 685 # let's avoid side effect (not sure it matters)
686 686 iter1 = iter(sorted(self._r1, reverse=not self._ascending))
687 687 else:
688 688 iter1 = iter1()
689 689 # get iterator for _r2
690 690 iter2 = getattr(self._r2, attr)
691 691 if iter2 is None:
692 692 # let's avoid side effect (not sure it matters)
693 693 iter2 = iter(sorted(self._r2, reverse=not self._ascending))
694 694 else:
695 695 iter2 = iter2()
696 696 return _iterordered(self._ascending, iter1, iter2)
697 697
698 698 def _trysetasclist(self):
699 699 """populate the _asclist attribute if possible and necessary"""
700 700 if self._genlist is not None and self._asclist is None:
701 701 self._asclist = sorted(self._genlist)
702 702
703 703 @property
704 704 def fastasc(self):
705 705 self._trysetasclist()
706 706 if self._asclist is not None:
707 707 return self._asclist.__iter__
708 708 iter1 = self._r1.fastasc
709 709 iter2 = self._r2.fastasc
710 710 if None in (iter1, iter2):
711 711 return None
712 712 return lambda: _iterordered(True, iter1(), iter2())
713 713
714 714 @property
715 715 def fastdesc(self):
716 716 self._trysetasclist()
717 717 if self._asclist is not None:
718 718 return self._asclist.__reversed__
719 719 iter1 = self._r1.fastdesc
720 720 iter2 = self._r2.fastdesc
721 721 if None in (iter1, iter2):
722 722 return None
723 723 return lambda: _iterordered(False, iter1(), iter2())
724 724
725 725 def __contains__(self, x):
726 726 return x in self._r1 or x in self._r2
727 727
728 728 def sort(self, reverse=False):
729 729 """Sort the added set
730 730
731 731 For this we use the cached list with all the generated values and if we
732 732 know they are ascending or descending we can sort them in a smart way.
733 733 """
734 734 self._ascending = not reverse
735 735
736 736 def isascending(self):
737 737 return self._ascending is not None and self._ascending
738 738
739 739 def isdescending(self):
740 740 return self._ascending is not None and not self._ascending
741 741
742 742 def istopo(self):
743 743 # not worth the trouble asserting if the two sets combined are still
744 744 # in topographical order. Use the sort() predicate to explicitly sort
745 745 # again instead.
746 746 return False
747 747
748 748 def reverse(self):
749 749 if self._ascending is None:
750 750 self._list.reverse()
751 751 else:
752 752 self._ascending = not self._ascending
753 753
754 754 def first(self):
755 755 for x in self:
756 756 return x
757 757 return None
758 758
759 759 def last(self):
760 760 self.reverse()
761 761 val = self.first()
762 762 self.reverse()
763 763 return val
764 764
765 765 @encoding.strmethod
766 766 def __repr__(self):
767 767 d = {None: '', False: '-', True: '+'}[self._ascending]
768 768 return '<%s%s %r, %r>' % (_typename(self), d, self._r1, self._r2)
769 769
770 770 class generatorset(abstractsmartset):
771 771 """Wrap a generator for lazy iteration
772 772
773 773 Wrapper structure for generators that provides lazy membership and can
774 774 be iterated more than once.
775 775 When asked for membership it generates values until either it finds the
776 776 requested one or has gone through all the elements in the generator
777 777
778 778 >>> xs = generatorset([0, 1, 4], iterasc=True)
779 779 >>> assert xs.last() == xs.last()
780 780 >>> xs.last() # cached
781 781 4
782 782 """
783 783 def __new__(cls, gen, iterasc=None):
784 784 if iterasc is None:
785 785 typ = cls
786 786 elif iterasc:
787 787 typ = _generatorsetasc
788 788 else:
789 789 typ = _generatorsetdesc
790 790
791 791 return super(generatorset, cls).__new__(typ)
792 792
793 793 def __init__(self, gen, iterasc=None):
794 794 """
795 795 gen: a generator producing the values for the generatorset.
796 796 """
797 797 self._gen = gen
798 798 self._asclist = None
799 799 self._cache = {}
800 800 self._genlist = []
801 801 self._finished = False
802 802 self._ascending = True
803 803
804 804 def __nonzero__(self):
805 805 # Do not use 'for r in self' because it will enforce the iteration
806 806 # order (default ascending), possibly unrolling a whole descending
807 807 # iterator.
808 808 if self._genlist:
809 809 return True
810 810 for r in self._consumegen():
811 811 return True
812 812 return False
813 813
814 814 __bool__ = __nonzero__
815 815
816 816 def __contains__(self, x):
817 817 if x in self._cache:
818 818 return self._cache[x]
819 819
820 820 # Use new values only, as existing values would be cached.
821 821 for l in self._consumegen():
822 822 if l == x:
823 823 return True
824 824
825 825 self._cache[x] = False
826 826 return False
827 827
828 828 def __iter__(self):
829 829 if self._ascending:
830 830 it = self.fastasc
831 831 else:
832 832 it = self.fastdesc
833 833 if it is not None:
834 834 return it()
835 835 # we need to consume the iterator
836 836 for x in self._consumegen():
837 837 pass
838 838 # recall the same code
839 839 return iter(self)
840 840
841 841 def _iterator(self):
842 842 if self._finished:
843 843 return iter(self._genlist)
844 844
845 845 # We have to use this complex iteration strategy to allow multiple
846 846 # iterations at the same time. We need to be able to catch revision
847 847 # removed from _consumegen and added to genlist in another instance.
848 848 #
849 849 # Getting rid of it would provide an about 15% speed up on this
850 850 # iteration.
851 851 genlist = self._genlist
852 852 nextgen = self._consumegen()
853 853 _len, _next = len, next # cache global lookup
854 854 def gen():
855 855 i = 0
856 856 while True:
857 857 if i < _len(genlist):
858 858 yield genlist[i]
859 859 else:
860 860 try:
861 861 yield _next(nextgen)
862 862 except StopIteration:
863 863 return
864 864 i += 1
865 865 return gen()
866 866
867 867 def _consumegen(self):
868 868 cache = self._cache
869 869 genlist = self._genlist.append
870 870 for item in self._gen:
871 871 cache[item] = True
872 872 genlist(item)
873 873 yield item
874 874 if not self._finished:
875 875 self._finished = True
876 876 asc = self._genlist[:]
877 877 asc.sort()
878 878 self._asclist = asc
879 879 self.fastasc = asc.__iter__
880 880 self.fastdesc = asc.__reversed__
881 881
882 882 def __len__(self):
883 883 for x in self._consumegen():
884 884 pass
885 885 return len(self._genlist)
886 886
887 887 def sort(self, reverse=False):
888 888 self._ascending = not reverse
889 889
890 890 def reverse(self):
891 891 self._ascending = not self._ascending
892 892
893 893 def isascending(self):
894 894 return self._ascending
895 895
896 896 def isdescending(self):
897 897 return not self._ascending
898 898
899 899 def istopo(self):
900 900 # not worth the trouble asserting if the two sets combined are still
901 901 # in topographical order. Use the sort() predicate to explicitly sort
902 902 # again instead.
903 903 return False
904 904
905 905 def first(self):
906 906 if self._ascending:
907 907 it = self.fastasc
908 908 else:
909 909 it = self.fastdesc
910 910 if it is None:
911 911 # we need to consume all and try again
912 912 for x in self._consumegen():
913 913 pass
914 914 return self.first()
915 915 return next(it(), None)
916 916
917 917 def last(self):
918 918 if self._ascending:
919 919 it = self.fastdesc
920 920 else:
921 921 it = self.fastasc
922 922 if it is None:
923 923 # we need to consume all and try again
924 924 for x in self._consumegen():
925 925 pass
926 926 return self.last()
927 927 return next(it(), None)
928 928
929 929 @encoding.strmethod
930 930 def __repr__(self):
931 931 d = {False: '-', True: '+'}[self._ascending]
932 932 return '<%s%s>' % (_typename(self), d)
933 933
934 934 class _generatorsetasc(generatorset):
935 935 """Special case of generatorset optimized for ascending generators."""
936 936
937 937 fastasc = generatorset._iterator
938 938
939 939 def __contains__(self, x):
940 940 if x in self._cache:
941 941 return self._cache[x]
942 942
943 943 # Use new values only, as existing values would be cached.
944 944 for l in self._consumegen():
945 945 if l == x:
946 946 return True
947 947 if l > x:
948 948 break
949 949
950 950 self._cache[x] = False
951 951 return False
952 952
953 953 class _generatorsetdesc(generatorset):
954 954 """Special case of generatorset optimized for descending generators."""
955 955
956 956 fastdesc = generatorset._iterator
957 957
958 958 def __contains__(self, x):
959 959 if x in self._cache:
960 960 return self._cache[x]
961 961
962 962 # Use new values only, as existing values would be cached.
963 963 for l in self._consumegen():
964 964 if l == x:
965 965 return True
966 966 if l < x:
967 967 break
968 968
969 969 self._cache[x] = False
970 970 return False
971 971
972 972 def spanset(repo, start=0, end=None):
973 973 """Create a spanset that represents a range of repository revisions
974 974
975 975 start: first revision included the set (default to 0)
976 976 end: first revision excluded (last+1) (default to len(repo))
977 977
978 978 Spanset will be descending if `end` < `start`.
979 979 """
980 980 if end is None:
981 981 end = len(repo)
982 982 ascending = start <= end
983 983 if not ascending:
984 984 start, end = end + 1, start + 1
985 985 return _spanset(start, end, ascending, repo.changelog.filteredrevs)
986 986
987 987 class _spanset(abstractsmartset):
988 988 """Duck type for baseset class which represents a range of revisions and
989 989 can work lazily and without having all the range in memory
990 990
991 991 Note that spanset(x, y) behave almost like xrange(x, y) except for two
992 992 notable points:
993 993 - when x < y it will be automatically descending,
994 994 - revision filtered with this repoview will be skipped.
995 995
996 996 """
997 997 def __init__(self, start, end, ascending, hiddenrevs):
998 998 self._start = start
999 999 self._end = end
1000 1000 self._ascending = ascending
1001 1001 self._hiddenrevs = hiddenrevs
1002 1002
1003 1003 def sort(self, reverse=False):
1004 1004 self._ascending = not reverse
1005 1005
1006 1006 def reverse(self):
1007 1007 self._ascending = not self._ascending
1008 1008
1009 1009 def istopo(self):
1010 1010 # not worth the trouble asserting if the two sets combined are still
1011 1011 # in topographical order. Use the sort() predicate to explicitly sort
1012 1012 # again instead.
1013 1013 return False
1014 1014
1015 1015 def _iterfilter(self, iterrange):
1016 1016 s = self._hiddenrevs
1017 1017 for r in iterrange:
1018 1018 if r not in s:
1019 1019 yield r
1020 1020
1021 1021 def __iter__(self):
1022 1022 if self._ascending:
1023 1023 return self.fastasc()
1024 1024 else:
1025 1025 return self.fastdesc()
1026 1026
1027 1027 def fastasc(self):
1028 1028 iterrange = xrange(self._start, self._end)
1029 1029 if self._hiddenrevs:
1030 1030 return self._iterfilter(iterrange)
1031 1031 return iter(iterrange)
1032 1032
1033 1033 def fastdesc(self):
1034 1034 iterrange = xrange(self._end - 1, self._start - 1, -1)
1035 1035 if self._hiddenrevs:
1036 1036 return self._iterfilter(iterrange)
1037 1037 return iter(iterrange)
1038 1038
1039 1039 def __contains__(self, rev):
1040 1040 hidden = self._hiddenrevs
1041 1041 return ((self._start <= rev < self._end)
1042 1042 and not (hidden and rev in hidden))
1043 1043
1044 1044 def __nonzero__(self):
1045 1045 for r in self:
1046 1046 return True
1047 1047 return False
1048 1048
1049 1049 __bool__ = __nonzero__
1050 1050
1051 1051 def __len__(self):
1052 1052 if not self._hiddenrevs:
1053 1053 return abs(self._end - self._start)
1054 1054 else:
1055 1055 count = 0
1056 1056 start = self._start
1057 1057 end = self._end
1058 1058 for rev in self._hiddenrevs:
1059 1059 if (end < rev <= start) or (start <= rev < end):
1060 1060 count += 1
1061 1061 return abs(self._end - self._start) - count
1062 1062
1063 1063 def isascending(self):
1064 1064 return self._ascending
1065 1065
1066 1066 def isdescending(self):
1067 1067 return not self._ascending
1068 1068
1069 1069 def first(self):
1070 1070 if self._ascending:
1071 1071 it = self.fastasc
1072 1072 else:
1073 1073 it = self.fastdesc
1074 1074 for x in it():
1075 1075 return x
1076 1076 return None
1077 1077
1078 1078 def last(self):
1079 1079 if self._ascending:
1080 1080 it = self.fastdesc
1081 1081 else:
1082 1082 it = self.fastasc
1083 1083 for x in it():
1084 1084 return x
1085 1085 return None
1086 1086
1087 1087 def _slice(self, start, stop):
1088 1088 if self._hiddenrevs:
1089 1089 # unoptimized since all hidden revisions in range has to be scanned
1090 1090 return super(_spanset, self)._slice(start, stop)
1091 1091 if self._ascending:
1092 1092 x = min(self._start + start, self._end)
1093 1093 y = min(self._start + stop, self._end)
1094 1094 else:
1095 1095 x = max(self._end - stop, self._start)
1096 1096 y = max(self._end - start, self._start)
1097 1097 return _spanset(x, y, self._ascending, self._hiddenrevs)
1098 1098
1099 1099 @encoding.strmethod
1100 1100 def __repr__(self):
1101 1101 d = {False: '-', True: '+'}[self._ascending]
1102 1102 return '<%s%s %d:%d>' % (_typename(self), d, self._start, self._end)
1103 1103
1104 1104 class fullreposet(_spanset):
1105 1105 """a set containing all revisions in the repo
1106 1106
1107 1107 This class exists to host special optimization and magic to handle virtual
1108 1108 revisions such as "null".
1109 1109 """
1110 1110
1111 1111 def __init__(self, repo):
1112 1112 super(fullreposet, self).__init__(0, len(repo), True,
1113 1113 repo.changelog.filteredrevs)
1114 1114
1115 1115 def __and__(self, other):
1116 1116 """As self contains the whole repo, all of the other set should also be
1117 1117 in self. Therefore `self & other = other`.
1118 1118
1119 1119 This boldly assumes the other contains valid revs only.
1120 1120 """
1121 1121 # other not a smartset, make is so
1122 1122 if not util.safehasattr(other, 'isascending'):
1123 1123 # filter out hidden revision
1124 1124 # (this boldly assumes all smartset are pure)
1125 1125 #
1126 1126 # `other` was used with "&", let's assume this is a set like
1127 1127 # object.
1128 1128 other = baseset(other - self._hiddenrevs)
1129 1129
1130 1130 other.sort(reverse=self.isdescending())
1131 1131 return other
1132 1132
1133 1133 def prettyformat(revs):
1134 1134 lines = []
1135 rs = pycompat.sysbytes(repr(revs))
1135 rs = pycompat.byterepr(revs)
1136 1136 p = 0
1137 1137 while p < len(rs):
1138 1138 q = rs.find('<', p + 1)
1139 1139 if q < 0:
1140 1140 q = len(rs)
1141 1141 l = rs.count('<', 0, p) - rs.count('>', 0, p)
1142 1142 assert l >= 0
1143 1143 lines.append((l, rs[p:q].rstrip()))
1144 1144 p = q
1145 1145 return '\n'.join(' ' * l + s for l, s in lines)
General Comments 0
You need to be logged in to leave comments. Login now