1# formatter.py - generic output formatting for mercurial
2#
3# Copyright 2012 Olivia Mackall <olivia@selenic.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8"""Generic output formatting for Mercurial
9
10The formatter provides API to show data in various ways. The following
11functions should be used in place of ui.write():
12
13- fm.write() for unconditional output
14- fm.condwrite() to show some extra data conditionally in plain output
15- fm.context() to provide changectx to template output
16- fm.data() to provide extra data to JSON or template output
17- fm.plain() to show raw text that isn't provided to JSON or template output
18
19To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20beforehand so the data is converted to the appropriate data type. Use
21fm.isplain() if you need to convert or format data conditionally which isn't
22supported by the formatter API.
23
24To build nested structure (i.e. a list of dicts), use fm.nested().
25
26See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
28fm.condwrite() vs 'if cond:':
29
30In most cases, use fm.condwrite() so users can selectively show the data
31in template output. If it's costly to build data, use plain 'if cond:' with
32fm.write().
33
34fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
36fm.nested() should be used to form a tree structure (a list of dicts of
37lists of dicts...) which can be accessed through template keywords, e.g.
38"{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39exports a dict-type object to template, which can be accessed by e.g.
40"{get(foo, key)}" function.
41
42Doctest helper:
43
44>>> def show(fn, verbose=False, **opts):
45...     import sys
46...     from . import ui as uimod
47...     ui = uimod.ui()
48...     ui.verbose = verbose
49...     ui.pushbuffer()
50...     try:
51...         return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52...                   pycompat.byteskwargs(opts)))
53...     finally:
54...         print(pycompat.sysstr(ui.popbuffer()), end='')
55
56Basic example:
57
58>>> def files(ui, fm):
59...     files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60...     for f in files:
61...         fm.startitem()
62...         fm.write(b'path', b'%s', f[0])
63...         fm.condwrite(ui.verbose, b'date', b'  %s',
64...                      fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65...         fm.data(size=f[1])
66...         fm.plain(b'\\n')
67...     fm.end()
68>>> show(files)
69foo
70bar
71>>> show(files, verbose=True)
72foo  1970-01-01 00:00:00
73bar  1970-01-01 00:00:01
74>>> show(files, template=b'json')
75[
76 {
77  "date": [0, 0],
78  "path": "foo",
79  "size": 123
80 },
81 {
82  "date": [1, 0],
83  "path": "bar",
84  "size": 456
85 }
86]
87>>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88path: foo
89date: 1970-01-01T00:00:00+00:00
90path: bar
91date: 1970-01-01T00:00:01+00:00
92
93Nested example:
94
95>>> def subrepos(ui, fm):
96...     fm.startitem()
97...     fm.write(b'reponame', b'[%s]\\n', b'baz')
98...     files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99...     fm.end()
100>>> show(subrepos)
101[baz]
102foo
103bar
104>>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105baz: foo, bar
106"""
107
108from __future__ import absolute_import, print_function
109
110import contextlib
111import itertools
112import os
113
114from .i18n import _
115from .node import (
116    hex,
117    short,
118)
119from .thirdparty import attr
120
121from . import (
122    error,
123    pycompat,
124    templatefilters,
125    templatekw,
126    templater,
127    templateutil,
128    util,
129)
130from .utils import (
131    cborutil,
132    dateutil,
133    stringutil,
134)
135
136pickle = util.pickle
137
138
139def isprintable(obj):
140    """Check if the given object can be directly passed in to formatter's
141    write() and data() functions
142
143    Returns False if the object is unsupported or must be pre-processed by
144    formatdate(), formatdict(), or formatlist().
145    """
146    return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
147
148
149class _nullconverter(object):
150    '''convert non-primitive data types to be processed by formatter'''
151
152    # set to True if context object should be stored as item
153    storecontext = False
154
155    @staticmethod
156    def wrapnested(data, tmpl, sep):
157        '''wrap nested data by appropriate type'''
158        return data
159
160    @staticmethod
161    def formatdate(date, fmt):
162        '''convert date tuple to appropriate format'''
163        # timestamp can be float, but the canonical form should be int
164        ts, tz = date
165        return (int(ts), tz)
166
167    @staticmethod
168    def formatdict(data, key, value, fmt, sep):
169        '''convert dict or key-value pairs to appropriate dict format'''
170        # use plain dict instead of util.sortdict so that data can be
171        # serialized as a builtin dict in pickle output
172        return dict(data)
173
174    @staticmethod
175    def formatlist(data, name, fmt, sep):
176        '''convert iterable to appropriate list format'''
177        return list(data)
178
179
180class baseformatter(object):
181
182    # set to True if the formater output a strict format that does not support
183    # arbitrary output in the stream.
184    strict_format = False
185
186    def __init__(self, ui, topic, opts, converter):
187        self._ui = ui
188        self._topic = topic
189        self._opts = opts
190        self._converter = converter
191        self._item = None
192        # function to convert node to string suitable for this output
193        self.hexfunc = hex
194
195    def __enter__(self):
196        return self
197
198    def __exit__(self, exctype, excvalue, traceback):
199        if exctype is None:
200            self.end()
201
202    def _showitem(self):
203        '''show a formatted item once all data is collected'''
204
205    def startitem(self):
206        '''begin an item in the format list'''
207        if self._item is not None:
208            self._showitem()
209        self._item = {}
210
211    def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
212        '''convert date tuple to appropriate format'''
213        return self._converter.formatdate(date, fmt)
214
215    def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
216        '''convert dict or key-value pairs to appropriate dict format'''
217        return self._converter.formatdict(data, key, value, fmt, sep)
218
219    def formatlist(self, data, name, fmt=None, sep=b' '):
220        '''convert iterable to appropriate list format'''
221        # name is mandatory argument for now, but it could be optional if
222        # we have default template keyword, e.g. {item}
223        return self._converter.formatlist(data, name, fmt, sep)
224
225    def context(self, **ctxs):
226        '''insert context objects to be used to render template keywords'''
227        ctxs = pycompat.byteskwargs(ctxs)
228        assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
229        if self._converter.storecontext:
230            # populate missing resources in fctx -> ctx -> repo order
231            if b'fctx' in ctxs and b'ctx' not in ctxs:
232                ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
233            if b'ctx' in ctxs and b'repo' not in ctxs:
234                ctxs[b'repo'] = ctxs[b'ctx'].repo()
235            self._item.update(ctxs)
236
237    def datahint(self):
238        '''set of field names to be referenced'''
239        return set()
240
241    def data(self, **data):
242        '''insert data into item that's not shown in default output'''
243        data = pycompat.byteskwargs(data)
244        self._item.update(data)
245
246    def write(self, fields, deftext, *fielddata, **opts):
247        '''do default text output while assigning data to item'''
248        fieldkeys = fields.split()
249        assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
250        self._item.update(zip(fieldkeys, fielddata))
251
252    def condwrite(self, cond, fields, deftext, *fielddata, **opts):
253        '''do conditional write (primarily for plain formatter)'''
254        fieldkeys = fields.split()
255        assert len(fieldkeys) == len(fielddata)
256        self._item.update(zip(fieldkeys, fielddata))
257
258    def plain(self, text, **opts):
259        '''show raw text for non-templated mode'''
260
261    def isplain(self):
262        '''check for plain formatter usage'''
263        return False
264
265    def nested(self, field, tmpl=None, sep=b''):
266        '''sub formatter to store nested data in the specified field'''
267        data = []
268        self._item[field] = self._converter.wrapnested(data, tmpl, sep)
269        return _nestedformatter(self._ui, self._converter, data)
270
271    def end(self):
272        '''end output for the formatter'''
273        if self._item is not None:
274            self._showitem()
275
276
277def nullformatter(ui, topic, opts):
278    '''formatter that prints nothing'''
279    return baseformatter(ui, topic, opts, converter=_nullconverter)
280
281
282class _nestedformatter(baseformatter):
283    '''build sub items and store them in the parent formatter'''
284
285    def __init__(self, ui, converter, data):
286        baseformatter.__init__(
287            self, ui, topic=b'', opts={}, converter=converter
288        )
289        self._data = data
290
291    def _showitem(self):
292        self._data.append(self._item)
293
294
295def _iteritems(data):
296    '''iterate key-value pairs in stable order'''
297    if isinstance(data, dict):
298        return sorted(pycompat.iteritems(data))
299    return data
300
301
302class _plainconverter(object):
303    '''convert non-primitive data types to text'''
304
305    storecontext = False
306
307    @staticmethod
308    def wrapnested(data, tmpl, sep):
309        raise error.ProgrammingError(b'plainformatter should never be nested')
310
311    @staticmethod
312    def formatdate(date, fmt):
313        '''stringify date tuple in the given format'''
314        return dateutil.datestr(date, fmt)
315
316    @staticmethod
317    def formatdict(data, key, value, fmt, sep):
318        '''stringify key-value pairs separated by sep'''
319        prefmt = pycompat.identity
320        if fmt is None:
321            fmt = b'%s=%s'
322            prefmt = pycompat.bytestr
323        return sep.join(
324            fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
325        )
326
327    @staticmethod
328    def formatlist(data, name, fmt, sep):
329        '''stringify iterable separated by sep'''
330        prefmt = pycompat.identity
331        if fmt is None:
332            fmt = b'%s'
333            prefmt = pycompat.bytestr
334        return sep.join(fmt % prefmt(e) for e in data)
335
336
337class plainformatter(baseformatter):
338    '''the default text output scheme'''
339
340    def __init__(self, ui, out, topic, opts):
341        baseformatter.__init__(self, ui, topic, opts, _plainconverter)
342        if ui.debugflag:
343            self.hexfunc = hex
344        else:
345            self.hexfunc = short
346        if ui is out:
347            self._write = ui.write
348        else:
349            self._write = lambda s, **opts: out.write(s)
350
351    def startitem(self):
352        pass
353
354    def data(self, **data):
355        pass
356
357    def write(self, fields, deftext, *fielddata, **opts):
358        self._write(deftext % fielddata, **opts)
359
360    def condwrite(self, cond, fields, deftext, *fielddata, **opts):
361        '''do conditional write'''
362        if cond:
363            self._write(deftext % fielddata, **opts)
364
365    def plain(self, text, **opts):
366        self._write(text, **opts)
367
368    def isplain(self):
369        return True
370
371    def nested(self, field, tmpl=None, sep=b''):
372        # nested data will be directly written to ui
373        return self
374
375    def end(self):
376        pass
377
378
379class debugformatter(baseformatter):
380    def __init__(self, ui, out, topic, opts):
381        baseformatter.__init__(self, ui, topic, opts, _nullconverter)
382        self._out = out
383        self._out.write(b"%s = [\n" % self._topic)
384
385    def _showitem(self):
386        self._out.write(
387            b'    %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
388        )
389
390    def end(self):
391        baseformatter.end(self)
392        self._out.write(b"]\n")
393
394
395class pickleformatter(baseformatter):
396    def __init__(self, ui, out, topic, opts):
397        baseformatter.__init__(self, ui, topic, opts, _nullconverter)
398        self._out = out
399        self._data = []
400
401    def _showitem(self):
402        self._data.append(self._item)
403
404    def end(self):
405        baseformatter.end(self)
406        self._out.write(pickle.dumps(self._data))
407
408
409class cborformatter(baseformatter):
410    '''serialize items as an indefinite-length CBOR array'''
411
412    def __init__(self, ui, out, topic, opts):
413        baseformatter.__init__(self, ui, topic, opts, _nullconverter)
414        self._out = out
415        self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
416
417    def _showitem(self):
418        self._out.write(b''.join(cborutil.streamencode(self._item)))
419
420    def end(self):
421        baseformatter.end(self)
422        self._out.write(cborutil.BREAK)
423
424
425class jsonformatter(baseformatter):
426
427    strict_format = True
428
429    def __init__(self, ui, out, topic, opts):
430        baseformatter.__init__(self, ui, topic, opts, _nullconverter)
431        self._out = out
432        self._out.write(b"[")
433        self._first = True
434
435    def _showitem(self):
436        if self._first:
437            self._first = False
438        else:
439            self._out.write(b",")
440
441        self._out.write(b"\n {\n")
442        first = True
443        for k, v in sorted(self._item.items()):
444            if first:
445                first = False
446            else:
447                self._out.write(b",\n")
448            u = templatefilters.json(v, paranoid=False)
449            self._out.write(b'  "%s": %s' % (k, u))
450        self._out.write(b"\n }")
451
452    def end(self):
453        baseformatter.end(self)
454        self._out.write(b"\n]\n")
455
456
457class _templateconverter(object):
458    '''convert non-primitive data types to be processed by templater'''
459
460    storecontext = True
461
462    @staticmethod
463    def wrapnested(data, tmpl, sep):
464        '''wrap nested data by templatable type'''
465        return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
466
467    @staticmethod
468    def formatdate(date, fmt):
469        '''return date tuple'''
470        return templateutil.date(date)
471
472    @staticmethod
473    def formatdict(data, key, value, fmt, sep):
474        '''build object that can be evaluated as either plain string or dict'''
475        data = util.sortdict(_iteritems(data))
476
477        def f():
478            yield _plainconverter.formatdict(data, key, value, fmt, sep)
479
480        return templateutil.hybriddict(
481            data, key=key, value=value, fmt=fmt, gen=f
482        )
483
484    @staticmethod
485    def formatlist(data, name, fmt, sep):
486        '''build object that can be evaluated as either plain string or list'''
487        data = list(data)
488
489        def f():
490            yield _plainconverter.formatlist(data, name, fmt, sep)
491
492        return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
493
494
495class templateformatter(baseformatter):
496    def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
497        baseformatter.__init__(self, ui, topic, opts, _templateconverter)
498        self._out = out
499        self._tref = spec.ref
500        self._t = loadtemplater(
501            ui,
502            spec,
503            defaults=templatekw.keywords,
504            resources=templateresources(ui),
505            cache=templatekw.defaulttempl,
506        )
507        if overridetemplates:
508            self._t.cache.update(overridetemplates)
509        self._parts = templatepartsmap(
510            spec, self._t, [b'docheader', b'docfooter', b'separator']
511        )
512        self._counter = itertools.count()
513        self._renderitem(b'docheader', {})
514
515    def _showitem(self):
516        item = self._item.copy()
517        item[b'index'] = index = next(self._counter)
518        if index > 0:
519            self._renderitem(b'separator', {})
520        self._renderitem(self._tref, item)
521
522    def _renderitem(self, part, item):
523        if part not in self._parts:
524            return
525        ref = self._parts[part]
526        # None can't be put in the mapping dict since it means <unset>
527        for k, v in item.items():
528            if v is None:
529                item[k] = templateutil.wrappedvalue(v)
530        self._out.write(self._t.render(ref, item))
531
532    @util.propertycache
533    def _symbolsused(self):
534        return self._t.symbolsused(self._tref)
535
536    def datahint(self):
537        '''set of field names to be referenced from the template'''
538        return self._symbolsused[0]
539
540    def end(self):
541        baseformatter.end(self)
542        self._renderitem(b'docfooter', {})
543
544
545@attr.s(frozen=True)
546class templatespec(object):
547    ref = attr.ib()
548    tmpl = attr.ib()
549    mapfile = attr.ib()
550    refargs = attr.ib(default=None)
551    fp = attr.ib(default=None)
552
553
554def empty_templatespec():
555    return templatespec(None, None, None)
556
557
558def reference_templatespec(ref, refargs=None):
559    return templatespec(ref, None, None, refargs)
560
561
562def literal_templatespec(tmpl):
563    if pycompat.ispy3:
564        assert not isinstance(tmpl, str), b'tmpl must not be a str'
565    return templatespec(b'', tmpl, None)
566
567
568def mapfile_templatespec(topic, mapfile, fp=None):
569    return templatespec(topic, None, mapfile, fp=fp)
570
571
572def lookuptemplate(ui, topic, tmpl):
573    """Find the template matching the given -T/--template spec 'tmpl'
574
575    'tmpl' can be any of the following:
576
577     - a literal template (e.g. '{rev}')
578     - a reference to built-in template (i.e. formatter)
579     - a map-file name or path (e.g. 'changelog')
580     - a reference to [templates] in config file
581     - a path to raw template file
582
583    A map file defines a stand-alone template environment. If a map file
584    selected, all templates defined in the file will be loaded, and the
585    template matching the given topic will be rendered. Aliases won't be
586    loaded from user config, but from the map file.
587
588    If no map file selected, all templates in [templates] section will be
589    available as well as aliases in [templatealias].
590    """
591
592    if not tmpl:
593        return empty_templatespec()
594
595    # looks like a literal template?
596    if b'{' in tmpl:
597        return literal_templatespec(tmpl)
598
599    # a reference to built-in (formatter) template
600    if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
601        return reference_templatespec(tmpl)
602
603    # a function-style reference to built-in template
604    func, fsep, ftail = tmpl.partition(b'(')
605    if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
606        templater.parseexpr(tmpl)  # make sure syntax errors are confined
607        return reference_templatespec(func, refargs=ftail[:-1])
608
609    # perhaps a stock style?
610    if not os.path.split(tmpl)[0]:
611        (mapname, fp) = templater.try_open_template(
612            b'map-cmdline.' + tmpl
613        ) or templater.try_open_template(tmpl)
614        if mapname:
615            return mapfile_templatespec(topic, mapname, fp)
616
617    # perhaps it's a reference to [templates]
618    if ui.config(b'templates', tmpl):
619        return reference_templatespec(tmpl)
620
621    if tmpl == b'list':
622        ui.write(_(b"available styles: %s\n") % templater.stylelist())
623        raise error.Abort(_(b"specify a template"))
624
625    # perhaps it's a path to a map or a template
626    if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
627        # is it a mapfile for a style?
628        if os.path.basename(tmpl).startswith(b"map-"):
629            return mapfile_templatespec(topic, os.path.realpath(tmpl))
630        with util.posixfile(tmpl, b'rb') as f:
631            tmpl = f.read()
632        return literal_templatespec(tmpl)
633
634    # constant string?
635    return literal_templatespec(tmpl)
636
637
638def templatepartsmap(spec, t, partnames):
639    """Create a mapping of {part: ref}"""
640    partsmap = {spec.ref: spec.ref}  # initial ref must exist in t
641    if spec.mapfile:
642        partsmap.update((p, p) for p in partnames if p in t)
643    elif spec.ref:
644        for part in partnames:
645            ref = b'%s:%s' % (spec.ref, part)  # select config sub-section
646            if ref in t:
647                partsmap[part] = ref
648    return partsmap
649
650
651def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
652    """Create a templater from either a literal template or loading from
653    a map file"""
654    assert not (spec.tmpl and spec.mapfile)
655    if spec.mapfile:
656        return templater.templater.frommapfile(
657            spec.mapfile,
658            spec.fp,
659            defaults=defaults,
660            resources=resources,
661            cache=cache,
662        )
663    return maketemplater(
664        ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
665    )
666
667
668def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
669    """Create a templater from a string template 'tmpl'"""
670    aliases = ui.configitems(b'templatealias')
671    t = templater.templater(
672        defaults=defaults, resources=resources, cache=cache, aliases=aliases
673    )
674    t.cache.update(
675        (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
676    )
677    if tmpl:
678        t.cache[b''] = tmpl
679    return t
680
681
682# marker to denote a resource to be loaded on demand based on mapping values
683# (e.g. (ctx, path) -> fctx)
684_placeholder = object()
685
686
687class templateresources(templater.resourcemapper):
688    """Resource mapper designed for the default templatekw and function"""
689
690    def __init__(self, ui, repo=None):
691        self._resmap = {
692            b'cache': {},  # for templatekw/funcs to store reusable data
693            b'repo': repo,
694            b'ui': ui,
695        }
696
697    def availablekeys(self, mapping):
698        return {
699            k for k in self.knownkeys() if self._getsome(mapping, k) is not None
700        }
701
702    def knownkeys(self):
703        return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
704
705    def lookup(self, mapping, key):
706        if key not in self.knownkeys():
707            return None
708        v = self._getsome(mapping, key)
709        if v is _placeholder:
710            v = mapping[key] = self._loadermap[key](self, mapping)
711        return v
712
713    def populatemap(self, context, origmapping, newmapping):
714        mapping = {}
715        if self._hasnodespec(newmapping):
716            mapping[b'revcache'] = {}  # per-ctx cache
717        if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
718            orignode = templateutil.runsymbol(context, origmapping, b'node')
719            mapping[b'originalnode'] = orignode
720        # put marker to override 'ctx'/'fctx' in mapping if any, and flag
721        # its existence to be reported by availablekeys()
722        if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
723            mapping[b'ctx'] = _placeholder
724        if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
725            mapping[b'fctx'] = _placeholder
726        return mapping
727
728    def _getsome(self, mapping, key):
729        v = mapping.get(key)
730        if v is not None:
731            return v
732        return self._resmap.get(key)
733
734    def _hasliteral(self, mapping, key):
735        """Test if a literal value is set or unset in the given mapping"""
736        return key in mapping and not callable(mapping[key])
737
738    def _getliteral(self, mapping, key):
739        """Return value of the given name if it is a literal"""
740        v = mapping.get(key)
741        if callable(v):
742            return None
743        return v
744
745    def _hasnodespec(self, mapping):
746        """Test if context revision is set or unset in the given mapping"""
747        return b'node' in mapping or b'ctx' in mapping
748
749    def _loadctx(self, mapping):
750        repo = self._getsome(mapping, b'repo')
751        node = self._getliteral(mapping, b'node')
752        if repo is None or node is None:
753            return
754        try:
755            return repo[node]
756        except error.RepoLookupError:
757            return None  # maybe hidden/non-existent node
758
759    def _loadfctx(self, mapping):
760        ctx = self._getsome(mapping, b'ctx')
761        path = self._getliteral(mapping, b'path')
762        if ctx is None or path is None:
763            return None
764        try:
765            return ctx[path]
766        except error.LookupError:
767            return None  # maybe removed file?
768
769    _loadermap = {
770        b'ctx': _loadctx,
771        b'fctx': _loadfctx,
772    }
773
774
775def _internaltemplateformatter(
776    ui,
777    out,
778    topic,
779    opts,
780    spec,
781    tmpl,
782    docheader=b'',
783    docfooter=b'',
784    separator=b'',
785):
786    """Build template formatter that handles customizable built-in templates
787    such as -Tjson(...)"""
788    templates = {spec.ref: tmpl}
789    if docheader:
790        templates[b'%s:docheader' % spec.ref] = docheader
791    if docfooter:
792        templates[b'%s:docfooter' % spec.ref] = docfooter
793    if separator:
794        templates[b'%s:separator' % spec.ref] = separator
795    return templateformatter(
796        ui, out, topic, opts, spec, overridetemplates=templates
797    )
798
799
800def formatter(ui, out, topic, opts):
801    spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
802    if spec.ref == b"cbor" and spec.refargs is not None:
803        return _internaltemplateformatter(
804            ui,
805            out,
806            topic,
807            opts,
808            spec,
809            tmpl=b'{dict(%s)|cbor}' % spec.refargs,
810            docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
811            docfooter=cborutil.BREAK,
812        )
813    elif spec.ref == b"cbor":
814        return cborformatter(ui, out, topic, opts)
815    elif spec.ref == b"json" and spec.refargs is not None:
816        return _internaltemplateformatter(
817            ui,
818            out,
819            topic,
820            opts,
821            spec,
822            tmpl=b'{dict(%s)|json}' % spec.refargs,
823            docheader=b'[\n ',
824            docfooter=b'\n]\n',
825            separator=b',\n ',
826        )
827    elif spec.ref == b"json":
828        return jsonformatter(ui, out, topic, opts)
829    elif spec.ref == b"pickle":
830        assert spec.refargs is None, r'function-style not supported'
831        return pickleformatter(ui, out, topic, opts)
832    elif spec.ref == b"debug":
833        assert spec.refargs is None, r'function-style not supported'
834        return debugformatter(ui, out, topic, opts)
835    elif spec.ref or spec.tmpl or spec.mapfile:
836        assert spec.refargs is None, r'function-style not supported'
837        return templateformatter(ui, out, topic, opts, spec)
838    # developer config: ui.formatdebug
839    elif ui.configbool(b'ui', b'formatdebug'):
840        return debugformatter(ui, out, topic, opts)
841    # deprecated config: ui.formatjson
842    elif ui.configbool(b'ui', b'formatjson'):
843        return jsonformatter(ui, out, topic, opts)
844    return plainformatter(ui, out, topic, opts)
845
846
847@contextlib.contextmanager
848def openformatter(ui, filename, topic, opts):
849    """Create a formatter that writes outputs to the specified file
850
851    Must be invoked using the 'with' statement.
852    """
853    with util.posixfile(filename, b'wb') as out:
854        with formatter(ui, out, topic, opts) as fm:
855            yield fm
856
857
858@contextlib.contextmanager
859def _neverending(fm):
860    yield fm
861
862
863def maybereopen(fm, filename):
864    """Create a formatter backed by file if filename specified, else return
865    the given formatter
866
867    Must be invoked using the 'with' statement. This will never call fm.end()
868    of the given formatter.
869    """
870    if filename:
871        return openformatter(fm._ui, filename, fm._topic, fm._opts)
872    else:
873        return _neverending(fm)
874