##// END OF EJS Templates
formatter: set _first on formatter, not ui...
formatter: set _first on formatter, not ui The _first field is used for tracking when to emit a separator between items. It seems like it's clearly formatter state, not ui state, so let's move it there.

File last commit:

r31298:59d09565 default
r31298:59d09565 default
Show More
formatter.py
443 lines | 15.0 KiB | text/x-python | PythonLexer
Matt Mackall
formatter: add basic formatters
r16134 # formatter.py - generic output formatting for mercurial
#
# Copyright 2012 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
Yuya Nishihara
formatter: add overview of API and example as doctest
r30560 """Generic output formatting for Mercurial
The formatter provides API to show data in various ways. The following
functions should be used in place of ui.write():
- fm.write() for unconditional output
- fm.condwrite() to show some extra data conditionally in plain output
Yuya Nishihara
formatter: add support for changeset templating...
r31172 - fm.context() to provide changectx to template output
Yuya Nishihara
formatter: add overview of API and example as doctest
r30560 - fm.data() to provide extra data to JSON or template output
- fm.plain() to show raw text that isn't provided to JSON or template output
To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
beforehand so the data is converted to the appropriate data type. Use
fm.isplain() if you need to convert or format data conditionally which isn't
supported by the formatter API.
To build nested structure (i.e. a list of dicts), use fm.nested().
See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
fm.condwrite() vs 'if cond:':
In most cases, use fm.condwrite() so users can selectively show the data
in template output. If it's costly to build data, use plain 'if cond:' with
fm.write().
fm.nested() vs fm.formatdict() (or fm.formatlist()):
fm.nested() should be used to form a tree structure (a list of dicts of
lists of dicts...) which can be accessed through template keywords, e.g.
"{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
exports a dict-type object to template, which can be accessed by e.g.
"{get(foo, key)}" function.
Doctest helper:
>>> def show(fn, verbose=False, **opts):
... import sys
... from . import ui as uimod
... ui = uimod.ui()
... ui.fout = sys.stdout # redirect to doctest
... ui.verbose = verbose
... return fn(ui, ui.formatter(fn.__name__, opts))
Basic example:
>>> def files(ui, fm):
... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
... for f in files:
... fm.startitem()
... fm.write('path', '%s', f[0])
... fm.condwrite(ui.verbose, 'date', ' %s',
... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
... fm.data(size=f[1])
... fm.plain('\\n')
... fm.end()
>>> show(files)
foo
bar
>>> show(files, verbose=True)
foo 1970-01-01 00:00:00
bar 1970-01-01 00:00:01
>>> show(files, template='json')
[
{
"date": [0, 0],
"path": "foo",
"size": 123
},
{
"date": [1, 0],
"path": "bar",
"size": 456
}
]
>>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
path: foo
date: 1970-01-01T00:00:00+00:00
path: bar
date: 1970-01-01T00:00:01+00:00
Nested example:
>>> def subrepos(ui, fm):
... fm.startitem()
... fm.write('repo', '[%s]\\n', 'baz')
... files(ui, fm.nested('files'))
... fm.end()
>>> show(subrepos)
[baz]
foo
bar
>>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
baz: foo, bar
"""
Gregory Szorc
formatter: use absolute_import
r25950 from __future__ import absolute_import
Matt Mackall
formatter: move most of template option helper to formatter...
r25511 import os
Matt Mackall
formatter: add json formatter
r22428
Gregory Szorc
formatter: use absolute_import
r25950 from .i18n import _
from .node import (
hex,
short,
)
from . import (
encoding,
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 error,
Yuya Nishihara
formatter: add function to convert list to appropriate format (issue5217)...
r29676 templatekw,
Gregory Szorc
formatter: use absolute_import
r25950 templater,
Pulkit Goyal
py3: conditionalize cPickle import by adding in util...
r29324 util,
Gregory Szorc
formatter: use absolute_import
r25950 )
Pulkit Goyal
py3: conditionalize cPickle import by adding in util...
r29324 pickle = util.pickle
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 class _nullconverter(object):
'''convert non-primitive data types to be processed by formatter'''
@staticmethod
def formatdate(date, fmt):
'''convert date tuple to appropriate format'''
return date
@staticmethod
def formatdict(data, key, value, fmt, sep):
'''convert dict or key-value pairs to appropriate dict format'''
# use plain dict instead of util.sortdict so that data can be
# serialized as a builtin dict in pickle output
return dict(data)
@staticmethod
def formatlist(data, name, fmt, sep):
'''convert iterable to appropriate list format'''
return list(data)
Matt Mackall
formatter: add basic formatters
r16134 class baseformatter(object):
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 def __init__(self, ui, topic, opts, converter):
Matt Mackall
formatter: add basic formatters
r16134 self._ui = ui
self._topic = topic
self._style = opts.get("style")
self._template = opts.get("template")
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 self._converter = converter
Matt Mackall
formatter: add basic formatters
r16134 self._item = None
Yuya Nishihara
formatter: add general way to switch hex/short functions...
r22701 # function to convert node to string suitable for this output
self.hexfunc = hex
Yuya Nishihara
formatter: add context manager interface for convenience...
r29882 def __enter__(self):
return self
def __exit__(self, exctype, excvalue, traceback):
if exctype is None:
self.end()
Matt Mackall
formatter: add basic formatters
r16134 def _showitem(self):
'''show a formatted item once all data is collected'''
pass
def startitem(self):
'''begin an item in the format list'''
if self._item is not None:
self._showitem()
self._item = {}
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
Yuya Nishihara
formatter: add function to convert date tuple to appropriate format
r29678 '''convert date tuple to appropriate format'''
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 return self._converter.formatdate(date, fmt)
def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
Yuya Nishihara
formatter: add function to convert dict to appropriate format...
r29794 '''convert dict or key-value pairs to appropriate dict format'''
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 return self._converter.formatdict(data, key, value, fmt, sep)
def formatlist(self, data, name, fmt='%s', sep=' '):
Yuya Nishihara
formatter: add function to convert list to appropriate format (issue5217)...
r29676 '''convert iterable to appropriate list format'''
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 # name is mandatory argument for now, but it could be optional if
# we have default template keyword, e.g. {item}
return self._converter.formatlist(data, name, fmt, sep)
Yuya Nishihara
formatter: add support for changeset templating...
r31172 def context(self, **ctxs):
'''insert context objects to be used to render template keywords'''
pass
Matt Mackall
formatter: add basic formatters
r16134 def data(self, **data):
'''insert data into item that's not shown in default output'''
David M. Carr
formatter: improve implementation of data method...
r17630 self._item.update(data)
Matt Mackall
formatter: add basic formatters
r16134 def write(self, fields, deftext, *fielddata, **opts):
'''do default text output while assigning data to item'''
Yuya Nishihara
formatter: verify number of arguments passed to write functions...
r26372 fieldkeys = fields.split()
assert len(fieldkeys) == len(fielddata)
Yuya Nishihara
formatter: use dict.update() to set arguments passed to write functions...
r26373 self._item.update(zip(fieldkeys, fielddata))
Matt Mackall
formatter: add condwrite method...
r17909 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
'''do conditional write (primarily for plain formatter)'''
Yuya Nishihara
formatter: verify number of arguments passed to write functions...
r26372 fieldkeys = fields.split()
assert len(fieldkeys) == len(fielddata)
Yuya Nishihara
formatter: use dict.update() to set arguments passed to write functions...
r26373 self._item.update(zip(fieldkeys, fielddata))
Matt Mackall
formatter: add basic formatters
r16134 def plain(self, text, **opts):
'''show raw text for non-templated mode'''
pass
Mathias De Maré
formatter: introduce isplain() to replace (the inverse of) __nonzero__() (API)...
r29949 def isplain(self):
'''check for plain formatter usage'''
return False
Yuya Nishihara
formatter: add fm.nested(field) to either write or build sub items...
r29837 def nested(self, field):
'''sub formatter to store nested data in the specified field'''
self._item[field] = data = []
return _nestedformatter(self._ui, self._converter, data)
Matt Mackall
formatter: add basic formatters
r16134 def end(self):
'''end output for the formatter'''
if self._item is not None:
self._showitem()
Yuya Nishihara
formatter: add fm.nested(field) to either write or build sub items...
r29837 class _nestedformatter(baseformatter):
'''build sub items and store them in the parent formatter'''
def __init__(self, ui, converter, data):
baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
self._data = data
def _showitem(self):
self._data.append(self._item)
Yuya Nishihara
formatter: add function to convert dict to appropriate format...
r29794 def _iteritems(data):
'''iterate key-value pairs in stable order'''
if isinstance(data, dict):
return sorted(data.iteritems())
return data
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 class _plainconverter(object):
'''convert non-primitive data types to text'''
@staticmethod
def formatdate(date, fmt):
'''stringify date tuple in the given format'''
return util.datestr(date, fmt)
@staticmethod
def formatdict(data, key, value, fmt, sep):
'''stringify key-value pairs separated by sep'''
return sep.join(fmt % (k, v) for k, v in _iteritems(data))
@staticmethod
def formatlist(data, name, fmt, sep):
'''stringify iterable separated by sep'''
return sep.join(fmt % e for e in data)
Matt Mackall
formatter: add basic formatters
r16134 class plainformatter(baseformatter):
'''the default text output scheme'''
def __init__(self, ui, topic, opts):
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
Yuya Nishihara
formatter: add general way to switch hex/short functions...
r22701 if ui.debugflag:
self.hexfunc = hex
else:
self.hexfunc = short
Matt Mackall
formatter: add basic formatters
r16134 def startitem(self):
pass
def data(self, **data):
pass
def write(self, fields, deftext, *fielddata, **opts):
self._ui.write(deftext % fielddata, **opts)
Matt Mackall
formatter: add condwrite method...
r17909 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
'''do conditional write'''
if cond:
self._ui.write(deftext % fielddata, **opts)
Matt Mackall
formatter: add basic formatters
r16134 def plain(self, text, **opts):
self._ui.write(text, **opts)
Mathias De Maré
formatter: introduce isplain() to replace (the inverse of) __nonzero__() (API)...
r29949 def isplain(self):
return True
Yuya Nishihara
formatter: add fm.nested(field) to either write or build sub items...
r29837 def nested(self, field):
# nested data will be directly written to ui
return self
Matt Mackall
formatter: add basic formatters
r16134 def end(self):
pass
class debugformatter(baseformatter):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 def __init__(self, ui, out, topic, opts):
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out = out
self._out.write("%s = [\n" % self._topic)
Matt Mackall
formatter: add basic formatters
r16134 def _showitem(self):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write(" " + repr(self._item) + ",\n")
Matt Mackall
formatter: add basic formatters
r16134 def end(self):
baseformatter.end(self)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write("]\n")
Matt Mackall
formatter: add basic formatters
r16134
Matt Mackall
formatter: add pickle format...
r22430 class pickleformatter(baseformatter):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 def __init__(self, ui, out, topic, opts):
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out = out
Matt Mackall
formatter: add pickle format...
r22430 self._data = []
def _showitem(self):
self._data.append(self._item)
def end(self):
baseformatter.end(self)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write(pickle.dumps(self._data))
Matt Mackall
formatter: add pickle format...
r22430
Yuya Nishihara
formatter: extract function that encode values to json string...
r22474 def _jsonifyobj(v):
Yuya Nishihara
formatter: add function to convert dict to appropriate format...
r29794 if isinstance(v, dict):
xs = ['"%s": %s' % (encoding.jsonescape(k), _jsonifyobj(u))
for k, u in sorted(v.iteritems())]
return '{' + ', '.join(xs) + '}'
elif isinstance(v, (list, tuple)):
Yuya Nishihara
formatter: have jsonformatter accept tuple as value...
r22475 return '[' + ', '.join(_jsonifyobj(e) for e in v) + ']'
Yuya Nishihara
formatter: convert None to json null...
r24321 elif v is None:
return 'null'
Yuya Nishihara
formatter: convert booleans to json...
r22674 elif v is True:
return 'true'
elif v is False:
return 'false'
Yuya Nishihara
formatter: convert float value to json...
r22476 elif isinstance(v, (int, float)):
return str(v)
Yuya Nishihara
formatter: extract function that encode values to json string...
r22474 else:
return '"%s"' % encoding.jsonescape(v)
Matt Mackall
formatter: add json formatter
r22428 class jsonformatter(baseformatter):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 def __init__(self, ui, out, topic, opts):
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out = out
self._out.write("[")
Martin von Zweigbergk
formatter: set _first on formatter, not ui...
r31298 self._first = True
Matt Mackall
formatter: add json formatter
r22428 def _showitem(self):
Martin von Zweigbergk
formatter: set _first on formatter, not ui...
r31298 if self._first:
self._first = False
Matt Mackall
formatter: add json formatter
r22428 else:
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write(",")
Matt Mackall
formatter: add json formatter
r22428
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write("\n {\n")
Matt Mackall
formatter: add json formatter
r22428 first = True
for k, v in sorted(self._item.items()):
if first:
first = False
else:
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write(",\n")
self._out.write(' "%s": %s' % (k, _jsonifyobj(v)))
self._out.write("\n }")
Matt Mackall
formatter: add json formatter
r22428 def end(self):
baseformatter.end(self)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write("\n]\n")
Matt Mackall
formatter: add json formatter
r22428
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 class _templateconverter(object):
'''convert non-primitive data types to be processed by templater'''
@staticmethod
def formatdate(date, fmt):
'''return date tuple'''
return date
@staticmethod
def formatdict(data, key, value, fmt, sep):
'''build object that can be evaluated as either plain string or dict'''
data = util.sortdict(_iteritems(data))
def f():
yield _plainconverter.formatdict(data, key, value, fmt, sep)
return templatekw._hybrid(f(), data, lambda k: {key: k, value: data[k]},
lambda d: fmt % (d[key], d[value]))
@staticmethod
def formatlist(data, name, fmt, sep):
'''build object that can be evaluated as either plain string or list'''
data = list(data)
def f():
yield _plainconverter.formatlist(data, name, fmt, sep)
return templatekw._hybrid(f(), data, lambda x: {name: x},
lambda d: fmt % d[name])
Matt Mackall
formatter: add template support...
r25513 class templateformatter(baseformatter):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 def __init__(self, ui, out, topic, opts):
Yuya Nishihara
formatter: factor out format*() functions to separate classes...
r29836 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out = out
Matt Mackall
formatter: add template support...
r25513 self._topic = topic
Yuya Nishihara
formatter: add support for changeset templating...
r31172 self._t = gettemplater(ui, topic, opts.get('template', ''),
cache=templatekw.defaulttempl)
self._cache = {} # for templatekw/funcs to store reusable data
def context(self, **ctxs):
'''insert context objects to be used to render template keywords'''
assert all(k == 'ctx' for k in ctxs)
self._item.update(ctxs)
Matt Mackall
formatter: add template support...
r25513 def _showitem(self):
Yuya Nishihara
formatter: add support for changeset templating...
r31172 # TODO: add support for filectx. probably each template keyword or
# function will have to declare dependent resources. e.g.
# @templatekeyword(..., requires=('ctx',))
if 'ctx' in self._item:
props = templatekw.keywords.copy()
# explicitly-defined fields precede templatekw
props.update(self._item)
# but template resources must be always available
props['templ'] = self._t
props['repo'] = props['ctx'].repo()
props['revcache'] = {}
else:
props = self._item
g = self._t(self._topic, ui=self._ui, cache=self._cache, **props)
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 self._out.write(templater.stringify(g))
Matt Mackall
formatter: add template support...
r25513
Matt Mackall
formatter: move most of template option helper to formatter...
r25511 def lookuptemplate(ui, topic, tmpl):
# looks like a literal template?
if '{' in tmpl:
return tmpl, None
# perhaps a stock style?
if not os.path.split(tmpl)[0]:
mapname = (templater.templatepath('map-cmdline.' + tmpl)
or templater.templatepath(tmpl))
if mapname and os.path.isfile(mapname):
return None, mapname
# perhaps it's a reference to [templates]
t = ui.config('templates', tmpl)
if t:
Yuya Nishihara
templater: relax unquotestring() to fall back to bare string...
r28630 return templater.unquotestring(t), None
Matt Mackall
formatter: move most of template option helper to formatter...
r25511
if tmpl == 'list':
ui.write(_("available styles: %s\n") % templater.stylelist())
Pierre-Yves David
error: get Abort from 'error' instead of 'util'...
r26587 raise error.Abort(_("specify a template"))
Matt Mackall
formatter: move most of template option helper to formatter...
r25511
# perhaps it's a path to a map or a template
if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
# is it a mapfile for a style?
if os.path.basename(tmpl).startswith("map-"):
return None, os.path.realpath(tmpl)
tmpl = open(tmpl).read()
return tmpl, None
# constant string?
return tmpl, None
Yuya Nishihara
formatter: add support for changeset templating...
r31172 def gettemplater(ui, topic, spec, cache=None):
Matt Mackall
formatter: add a method to build a full templater from a -T option
r25512 tmpl, mapfile = lookuptemplate(ui, topic, spec)
Yuya Nishihara
templater: separate function to create templater from map file (API)...
r28954 assert not (tmpl and mapfile)
if mapfile:
Yuya Nishihara
formatter: add support for changeset templating...
r31172 return templater.templater.frommapfile(mapfile, cache=cache)
return maketemplater(ui, topic, tmpl, cache=cache)
Yuya Nishihara
templater: factor out function that creates templater from string template...
r28955
Yuya Nishihara
formatter: drop filters argument from maketemplater()...
r31170 def maketemplater(ui, topic, tmpl, cache=None):
Yuya Nishihara
templater: factor out function that creates templater from string template...
r28955 """Create a templater from a string template 'tmpl'"""
Yuya Nishihara
templater: load and expand aliases by template engine (API) (issue4842)...
r28957 aliases = ui.configitems('templatealias')
Yuya Nishihara
formatter: drop filters argument from maketemplater()...
r31170 t = templater.templater(cache=cache, aliases=aliases)
Matt Mackall
formatter: add a method to build a full templater from a -T option
r25512 if tmpl:
t.cache[topic] = tmpl
return t
Matt Mackall
formatter: add basic formatters
r16134 def formatter(ui, topic, opts):
Matt Mackall
formatter: add json formatter
r22428 template = opts.get("template", "")
if template == "json":
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 return jsonformatter(ui, ui, topic, opts)
Matt Mackall
formatter: add pickle format...
r22430 elif template == "pickle":
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 return pickleformatter(ui, ui, topic, opts)
Matt Mackall
formatter: add json formatter
r22428 elif template == "debug":
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 return debugformatter(ui, ui, topic, opts)
Matt Mackall
formatter: add json formatter
r22428 elif template != "":
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 return templateformatter(ui, ui, topic, opts)
Matt Mackall
formatter: mark developer options
r25838 # developer config: ui.formatdebug
Matt Mackall
formatter: add json formatter
r22428 elif ui.configbool('ui', 'formatdebug'):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 return debugformatter(ui, ui, topic, opts)
Matt Mackall
formatter: mark developer options
r25838 # deprecated config: ui.formatjson
Matt Mackall
formatter: add json formatter
r22428 elif ui.configbool('ui', 'formatjson'):
Yuya Nishihara
formatter: add argument to change output file of non-plain formatter...
r31182 return jsonformatter(ui, ui, topic, opts)
Matt Mackall
formatter: add basic formatters
r16134 return plainformatter(ui, topic, opts)