##// END OF EJS Templates
formatter: factor out function that detects node change and document it...
Yuya Nishihara -
r39621:990a0b07 default
parent child Browse files
Show More
@@ -1,629 +1,629 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', tmpl=b'{reponame}'))
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 contextlib
111 111 import itertools
112 112 import os
113 113
114 114 from .i18n import _
115 115 from .node import (
116 116 hex,
117 117 short,
118 118 )
119 119 from .thirdparty import (
120 120 attr,
121 121 )
122 122
123 123 from . import (
124 124 error,
125 125 pycompat,
126 126 templatefilters,
127 127 templatefuncs,
128 128 templatekw,
129 129 templater,
130 130 templateutil,
131 131 util,
132 132 )
133 133 from .utils import dateutil
134 134
135 135 pickle = util.pickle
136 136
137 137 class _nullconverter(object):
138 138 '''convert non-primitive data types to be processed by formatter'''
139 139
140 140 # set to True if context object should be stored as item
141 141 storecontext = False
142 142
143 143 @staticmethod
144 144 def wrapnested(data, tmpl, sep):
145 145 '''wrap nested data by appropriate type'''
146 146 return data
147 147 @staticmethod
148 148 def formatdate(date, fmt):
149 149 '''convert date tuple to appropriate format'''
150 150 # timestamp can be float, but the canonical form should be int
151 151 ts, tz = date
152 152 return (int(ts), tz)
153 153 @staticmethod
154 154 def formatdict(data, key, value, fmt, sep):
155 155 '''convert dict or key-value pairs to appropriate dict format'''
156 156 # use plain dict instead of util.sortdict so that data can be
157 157 # serialized as a builtin dict in pickle output
158 158 return dict(data)
159 159 @staticmethod
160 160 def formatlist(data, name, fmt, sep):
161 161 '''convert iterable to appropriate list format'''
162 162 return list(data)
163 163
164 164 class baseformatter(object):
165 165 def __init__(self, ui, topic, opts, converter):
166 166 self._ui = ui
167 167 self._topic = topic
168 168 self._opts = opts
169 169 self._converter = converter
170 170 self._item = None
171 171 # function to convert node to string suitable for this output
172 172 self.hexfunc = hex
173 173 def __enter__(self):
174 174 return self
175 175 def __exit__(self, exctype, excvalue, traceback):
176 176 if exctype is None:
177 177 self.end()
178 178 def _showitem(self):
179 179 '''show a formatted item once all data is collected'''
180 180 def startitem(self):
181 181 '''begin an item in the format list'''
182 182 if self._item is not None:
183 183 self._showitem()
184 184 self._item = {}
185 185 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
186 186 '''convert date tuple to appropriate format'''
187 187 return self._converter.formatdate(date, fmt)
188 188 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
189 189 '''convert dict or key-value pairs to appropriate dict format'''
190 190 return self._converter.formatdict(data, key, value, fmt, sep)
191 191 def formatlist(self, data, name, fmt=None, sep=' '):
192 192 '''convert iterable to appropriate list format'''
193 193 # name is mandatory argument for now, but it could be optional if
194 194 # we have default template keyword, e.g. {item}
195 195 return self._converter.formatlist(data, name, fmt, sep)
196 196 def contexthint(self, datafields):
197 197 '''set of context object keys to be required given datafields set'''
198 198 return set()
199 199 def context(self, **ctxs):
200 200 '''insert context objects to be used to render template keywords'''
201 201 ctxs = pycompat.byteskwargs(ctxs)
202 202 assert all(k in {'repo', 'ctx', 'fctx'} for k in ctxs)
203 203 if self._converter.storecontext:
204 204 # populate missing resources in fctx -> ctx -> repo order
205 205 if 'fctx' in ctxs and 'ctx' not in ctxs:
206 206 ctxs['ctx'] = ctxs['fctx'].changectx()
207 207 if 'ctx' in ctxs and 'repo' not in ctxs:
208 208 ctxs['repo'] = ctxs['ctx'].repo()
209 209 self._item.update(ctxs)
210 210 def datahint(self):
211 211 '''set of field names to be referenced'''
212 212 return set()
213 213 def data(self, **data):
214 214 '''insert data into item that's not shown in default output'''
215 215 data = pycompat.byteskwargs(data)
216 216 self._item.update(data)
217 217 def write(self, fields, deftext, *fielddata, **opts):
218 218 '''do default text output while assigning data to item'''
219 219 fieldkeys = fields.split()
220 220 assert len(fieldkeys) == len(fielddata)
221 221 self._item.update(zip(fieldkeys, fielddata))
222 222 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
223 223 '''do conditional write (primarily for plain formatter)'''
224 224 fieldkeys = fields.split()
225 225 assert len(fieldkeys) == len(fielddata)
226 226 self._item.update(zip(fieldkeys, fielddata))
227 227 def plain(self, text, **opts):
228 228 '''show raw text for non-templated mode'''
229 229 def isplain(self):
230 230 '''check for plain formatter usage'''
231 231 return False
232 232 def nested(self, field, tmpl=None, sep=''):
233 233 '''sub formatter to store nested data in the specified field'''
234 234 data = []
235 235 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
236 236 return _nestedformatter(self._ui, self._converter, data)
237 237 def end(self):
238 238 '''end output for the formatter'''
239 239 if self._item is not None:
240 240 self._showitem()
241 241
242 242 def nullformatter(ui, topic, opts):
243 243 '''formatter that prints nothing'''
244 244 return baseformatter(ui, topic, opts, converter=_nullconverter)
245 245
246 246 class _nestedformatter(baseformatter):
247 247 '''build sub items and store them in the parent formatter'''
248 248 def __init__(self, ui, converter, data):
249 249 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
250 250 self._data = data
251 251 def _showitem(self):
252 252 self._data.append(self._item)
253 253
254 254 def _iteritems(data):
255 255 '''iterate key-value pairs in stable order'''
256 256 if isinstance(data, dict):
257 257 return sorted(data.iteritems())
258 258 return data
259 259
260 260 class _plainconverter(object):
261 261 '''convert non-primitive data types to text'''
262 262
263 263 storecontext = False
264 264
265 265 @staticmethod
266 266 def wrapnested(data, tmpl, sep):
267 267 raise error.ProgrammingError('plainformatter should never be nested')
268 268 @staticmethod
269 269 def formatdate(date, fmt):
270 270 '''stringify date tuple in the given format'''
271 271 return dateutil.datestr(date, fmt)
272 272 @staticmethod
273 273 def formatdict(data, key, value, fmt, sep):
274 274 '''stringify key-value pairs separated by sep'''
275 275 prefmt = pycompat.identity
276 276 if fmt is None:
277 277 fmt = '%s=%s'
278 278 prefmt = pycompat.bytestr
279 279 return sep.join(fmt % (prefmt(k), prefmt(v))
280 280 for k, v in _iteritems(data))
281 281 @staticmethod
282 282 def formatlist(data, name, fmt, sep):
283 283 '''stringify iterable separated by sep'''
284 284 prefmt = pycompat.identity
285 285 if fmt is None:
286 286 fmt = '%s'
287 287 prefmt = pycompat.bytestr
288 288 return sep.join(fmt % prefmt(e) for e in data)
289 289
290 290 class plainformatter(baseformatter):
291 291 '''the default text output scheme'''
292 292 def __init__(self, ui, out, topic, opts):
293 293 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
294 294 if ui.debugflag:
295 295 self.hexfunc = hex
296 296 else:
297 297 self.hexfunc = short
298 298 if ui is out:
299 299 self._write = ui.write
300 300 else:
301 301 self._write = lambda s, **opts: out.write(s)
302 302 def startitem(self):
303 303 pass
304 304 def data(self, **data):
305 305 pass
306 306 def write(self, fields, deftext, *fielddata, **opts):
307 307 self._write(deftext % fielddata, **opts)
308 308 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
309 309 '''do conditional write'''
310 310 if cond:
311 311 self._write(deftext % fielddata, **opts)
312 312 def plain(self, text, **opts):
313 313 self._write(text, **opts)
314 314 def isplain(self):
315 315 return True
316 316 def nested(self, field, tmpl=None, sep=''):
317 317 # nested data will be directly written to ui
318 318 return self
319 319 def end(self):
320 320 pass
321 321
322 322 class debugformatter(baseformatter):
323 323 def __init__(self, ui, out, topic, opts):
324 324 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
325 325 self._out = out
326 326 self._out.write("%s = [\n" % self._topic)
327 327 def _showitem(self):
328 328 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
329 329 def end(self):
330 330 baseformatter.end(self)
331 331 self._out.write("]\n")
332 332
333 333 class pickleformatter(baseformatter):
334 334 def __init__(self, ui, out, topic, opts):
335 335 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
336 336 self._out = out
337 337 self._data = []
338 338 def _showitem(self):
339 339 self._data.append(self._item)
340 340 def end(self):
341 341 baseformatter.end(self)
342 342 self._out.write(pickle.dumps(self._data))
343 343
344 344 class jsonformatter(baseformatter):
345 345 def __init__(self, ui, out, topic, opts):
346 346 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
347 347 self._out = out
348 348 self._out.write("[")
349 349 self._first = True
350 350 def _showitem(self):
351 351 if self._first:
352 352 self._first = False
353 353 else:
354 354 self._out.write(",")
355 355
356 356 self._out.write("\n {\n")
357 357 first = True
358 358 for k, v in sorted(self._item.items()):
359 359 if first:
360 360 first = False
361 361 else:
362 362 self._out.write(",\n")
363 363 u = templatefilters.json(v, paranoid=False)
364 364 self._out.write(' "%s": %s' % (k, u))
365 365 self._out.write("\n }")
366 366 def end(self):
367 367 baseformatter.end(self)
368 368 self._out.write("\n]\n")
369 369
370 370 class _templateconverter(object):
371 371 '''convert non-primitive data types to be processed by templater'''
372 372
373 373 storecontext = True
374 374
375 375 @staticmethod
376 376 def wrapnested(data, tmpl, sep):
377 377 '''wrap nested data by templatable type'''
378 378 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
379 379 @staticmethod
380 380 def formatdate(date, fmt):
381 381 '''return date tuple'''
382 382 return templateutil.date(date)
383 383 @staticmethod
384 384 def formatdict(data, key, value, fmt, sep):
385 385 '''build object that can be evaluated as either plain string or dict'''
386 386 data = util.sortdict(_iteritems(data))
387 387 def f():
388 388 yield _plainconverter.formatdict(data, key, value, fmt, sep)
389 389 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
390 390 gen=f)
391 391 @staticmethod
392 392 def formatlist(data, name, fmt, sep):
393 393 '''build object that can be evaluated as either plain string or list'''
394 394 data = list(data)
395 395 def f():
396 396 yield _plainconverter.formatlist(data, name, fmt, sep)
397 397 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
398 398
399 399 class templateformatter(baseformatter):
400 400 def __init__(self, ui, out, topic, opts):
401 401 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
402 402 self._out = out
403 403 spec = lookuptemplate(ui, topic, opts.get('template', ''))
404 404 self._tref = spec.ref
405 405 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
406 406 resources=templateresources(ui),
407 407 cache=templatekw.defaulttempl)
408 408 self._parts = templatepartsmap(spec, self._t,
409 409 ['docheader', 'docfooter', 'separator'])
410 410 self._counter = itertools.count()
411 411 self._renderitem('docheader', {})
412 412
413 413 def _showitem(self):
414 414 item = self._item.copy()
415 415 item['index'] = index = next(self._counter)
416 416 if index > 0:
417 417 self._renderitem('separator', {})
418 418 self._renderitem(self._tref, item)
419 419
420 420 def _renderitem(self, part, item):
421 421 if part not in self._parts:
422 422 return
423 423 ref = self._parts[part]
424 424 self._out.write(self._t.render(ref, item))
425 425
426 426 @util.propertycache
427 427 def _symbolsused(self):
428 428 return self._t.symbolsused(self._tref)
429 429
430 430 def contexthint(self, datafields):
431 431 '''set of context object keys to be required by the template, given
432 432 datafields overridden by immediate values'''
433 433 requires = set()
434 434 ksyms, fsyms = self._symbolsused
435 435 ksyms = ksyms - set(datafields.split()) # exclude immediate fields
436 436 symtables = [(ksyms, templatekw.keywords),
437 437 (fsyms, templatefuncs.funcs)]
438 438 for syms, table in symtables:
439 439 for k in syms:
440 440 f = table.get(k)
441 441 if not f:
442 442 continue
443 443 requires.update(getattr(f, '_requires', ()))
444 444 if 'repo' in requires:
445 445 requires.add('ctx') # there's no API to pass repo to formatter
446 446 return requires & {'ctx', 'fctx'}
447 447
448 448 def datahint(self):
449 449 '''set of field names to be referenced from the template'''
450 450 return self._symbolsused[0]
451 451
452 452 def end(self):
453 453 baseformatter.end(self)
454 454 self._renderitem('docfooter', {})
455 455
456 456 @attr.s(frozen=True)
457 457 class templatespec(object):
458 458 ref = attr.ib()
459 459 tmpl = attr.ib()
460 460 mapfile = attr.ib()
461 461
462 462 def lookuptemplate(ui, topic, tmpl):
463 463 """Find the template matching the given -T/--template spec 'tmpl'
464 464
465 465 'tmpl' can be any of the following:
466 466
467 467 - a literal template (e.g. '{rev}')
468 468 - a map-file name or path (e.g. 'changelog')
469 469 - a reference to [templates] in config file
470 470 - a path to raw template file
471 471
472 472 A map file defines a stand-alone template environment. If a map file
473 473 selected, all templates defined in the file will be loaded, and the
474 474 template matching the given topic will be rendered. Aliases won't be
475 475 loaded from user config, but from the map file.
476 476
477 477 If no map file selected, all templates in [templates] section will be
478 478 available as well as aliases in [templatealias].
479 479 """
480 480
481 481 # looks like a literal template?
482 482 if '{' in tmpl:
483 483 return templatespec('', tmpl, None)
484 484
485 485 # perhaps a stock style?
486 486 if not os.path.split(tmpl)[0]:
487 487 mapname = (templater.templatepath('map-cmdline.' + tmpl)
488 488 or templater.templatepath(tmpl))
489 489 if mapname and os.path.isfile(mapname):
490 490 return templatespec(topic, None, mapname)
491 491
492 492 # perhaps it's a reference to [templates]
493 493 if ui.config('templates', tmpl):
494 494 return templatespec(tmpl, None, None)
495 495
496 496 if tmpl == 'list':
497 497 ui.write(_("available styles: %s\n") % templater.stylelist())
498 498 raise error.Abort(_("specify a template"))
499 499
500 500 # perhaps it's a path to a map or a template
501 501 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
502 502 # is it a mapfile for a style?
503 503 if os.path.basename(tmpl).startswith("map-"):
504 504 return templatespec(topic, None, os.path.realpath(tmpl))
505 505 with util.posixfile(tmpl, 'rb') as f:
506 506 tmpl = f.read()
507 507 return templatespec('', tmpl, None)
508 508
509 509 # constant string?
510 510 return templatespec('', tmpl, None)
511 511
512 512 def templatepartsmap(spec, t, partnames):
513 513 """Create a mapping of {part: ref}"""
514 514 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
515 515 if spec.mapfile:
516 516 partsmap.update((p, p) for p in partnames if p in t)
517 517 elif spec.ref:
518 518 for part in partnames:
519 519 ref = '%s:%s' % (spec.ref, part) # select config sub-section
520 520 if ref in t:
521 521 partsmap[part] = ref
522 522 return partsmap
523 523
524 524 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
525 525 """Create a templater from either a literal template or loading from
526 526 a map file"""
527 527 assert not (spec.tmpl and spec.mapfile)
528 528 if spec.mapfile:
529 529 frommapfile = templater.templater.frommapfile
530 530 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
531 531 cache=cache)
532 532 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
533 533 cache=cache)
534 534
535 535 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
536 536 """Create a templater from a string template 'tmpl'"""
537 537 aliases = ui.configitems('templatealias')
538 538 t = templater.templater(defaults=defaults, resources=resources,
539 539 cache=cache, aliases=aliases)
540 540 t.cache.update((k, templater.unquotestring(v))
541 541 for k, v in ui.configitems('templates'))
542 542 if tmpl:
543 543 t.cache[''] = tmpl
544 544 return t
545 545
546 546 class templateresources(templater.resourcemapper):
547 547 """Resource mapper designed for the default templatekw and function"""
548 548
549 549 def __init__(self, ui, repo=None):
550 550 self._resmap = {
551 551 'cache': {}, # for templatekw/funcs to store reusable data
552 552 'repo': repo,
553 553 'ui': ui,
554 554 }
555 555
556 556 def availablekeys(self, mapping):
557 557 return {k for k in self.knownkeys()
558 558 if self._getsome(mapping, k) is not None}
559 559
560 560 def knownkeys(self):
561 561 return {'cache', 'ctx', 'fctx', 'repo', 'revcache', 'ui'}
562 562
563 563 def lookup(self, mapping, key):
564 564 if key not in self.knownkeys():
565 565 return None
566 566 return self._getsome(mapping, key)
567 567
568 568 def populatemap(self, context, origmapping, newmapping):
569 569 mapping = {}
570 if self._hasctx(newmapping):
570 if self._hasnodespec(newmapping):
571 571 mapping['revcache'] = {} # per-ctx cache
572 if (('node' in origmapping or self._hasctx(origmapping))
573 and ('node' in newmapping or self._hasctx(newmapping))):
572 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
574 573 orignode = templateutil.runsymbol(context, origmapping, 'node')
575 574 mapping['originalnode'] = orignode
576 575 return mapping
577 576
578 577 def _getsome(self, mapping, key):
579 578 v = mapping.get(key)
580 579 if v is not None:
581 580 return v
582 581 return self._resmap.get(key)
583 582
584 def _hasctx(self, mapping):
585 return 'ctx' in mapping
583 def _hasnodespec(self, mapping):
584 """Test if context revision is set or unset in the given mapping"""
585 return 'node' in mapping or 'ctx' in mapping
586 586
587 587 def formatter(ui, out, topic, opts):
588 588 template = opts.get("template", "")
589 589 if template == "json":
590 590 return jsonformatter(ui, out, topic, opts)
591 591 elif template == "pickle":
592 592 return pickleformatter(ui, out, topic, opts)
593 593 elif template == "debug":
594 594 return debugformatter(ui, out, topic, opts)
595 595 elif template != "":
596 596 return templateformatter(ui, out, topic, opts)
597 597 # developer config: ui.formatdebug
598 598 elif ui.configbool('ui', 'formatdebug'):
599 599 return debugformatter(ui, out, topic, opts)
600 600 # deprecated config: ui.formatjson
601 601 elif ui.configbool('ui', 'formatjson'):
602 602 return jsonformatter(ui, out, topic, opts)
603 603 return plainformatter(ui, out, topic, opts)
604 604
605 605 @contextlib.contextmanager
606 606 def openformatter(ui, filename, topic, opts):
607 607 """Create a formatter that writes outputs to the specified file
608 608
609 609 Must be invoked using the 'with' statement.
610 610 """
611 611 with util.posixfile(filename, 'wb') as out:
612 612 with formatter(ui, out, topic, opts) as fm:
613 613 yield fm
614 614
615 615 @contextlib.contextmanager
616 616 def _neverending(fm):
617 617 yield fm
618 618
619 619 def maybereopen(fm, filename):
620 620 """Create a formatter backed by file if filename specified, else return
621 621 the given formatter
622 622
623 623 Must be invoked using the 'with' statement. This will never call fm.end()
624 624 of the given formatter.
625 625 """
626 626 if filename:
627 627 return openformatter(fm._ui, filename, fm._topic, fm._opts)
628 628 else:
629 629 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now