##// END OF EJS Templates
formatter: more details on assertion failure...
Boris Feld -
r40175:46f9b1d2 default
parent child Browse files
Show More
@@ -1,656 +1,656 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or 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
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
99 ... fm.end()
99 ... fm.end()
100 >>> show(subrepos)
100 >>> show(subrepos)
101 [baz]
101 [baz]
102 foo
102 foo
103 bar
103 bar
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 baz: foo, bar
105 baz: foo, bar
106 """
106 """
107
107
108 from __future__ import absolute_import, print_function
108 from __future__ import absolute_import, print_function
109
109
110 import contextlib
110 import contextlib
111 import itertools
111 import itertools
112 import os
112 import os
113
113
114 from .i18n import _
114 from .i18n import _
115 from .node import (
115 from .node import (
116 hex,
116 hex,
117 short,
117 short,
118 )
118 )
119 from .thirdparty import (
119 from .thirdparty import (
120 attr,
120 attr,
121 )
121 )
122
122
123 from . import (
123 from . import (
124 error,
124 error,
125 pycompat,
125 pycompat,
126 templatefilters,
126 templatefilters,
127 templatekw,
127 templatekw,
128 templater,
128 templater,
129 templateutil,
129 templateutil,
130 util,
130 util,
131 )
131 )
132 from .utils import dateutil
132 from .utils import dateutil
133
133
134 pickle = util.pickle
134 pickle = util.pickle
135
135
136 class _nullconverter(object):
136 class _nullconverter(object):
137 '''convert non-primitive data types to be processed by formatter'''
137 '''convert non-primitive data types to be processed by formatter'''
138
138
139 # set to True if context object should be stored as item
139 # set to True if context object should be stored as item
140 storecontext = False
140 storecontext = False
141
141
142 @staticmethod
142 @staticmethod
143 def wrapnested(data, tmpl, sep):
143 def wrapnested(data, tmpl, sep):
144 '''wrap nested data by appropriate type'''
144 '''wrap nested data by appropriate type'''
145 return data
145 return data
146 @staticmethod
146 @staticmethod
147 def formatdate(date, fmt):
147 def formatdate(date, fmt):
148 '''convert date tuple to appropriate format'''
148 '''convert date tuple to appropriate format'''
149 # timestamp can be float, but the canonical form should be int
149 # timestamp can be float, but the canonical form should be int
150 ts, tz = date
150 ts, tz = date
151 return (int(ts), tz)
151 return (int(ts), tz)
152 @staticmethod
152 @staticmethod
153 def formatdict(data, key, value, fmt, sep):
153 def formatdict(data, key, value, fmt, sep):
154 '''convert dict or key-value pairs to appropriate dict format'''
154 '''convert dict or key-value pairs to appropriate dict format'''
155 # use plain dict instead of util.sortdict so that data can be
155 # use plain dict instead of util.sortdict so that data can be
156 # serialized as a builtin dict in pickle output
156 # serialized as a builtin dict in pickle output
157 return dict(data)
157 return dict(data)
158 @staticmethod
158 @staticmethod
159 def formatlist(data, name, fmt, sep):
159 def formatlist(data, name, fmt, sep):
160 '''convert iterable to appropriate list format'''
160 '''convert iterable to appropriate list format'''
161 return list(data)
161 return list(data)
162
162
163 class baseformatter(object):
163 class baseformatter(object):
164 def __init__(self, ui, topic, opts, converter):
164 def __init__(self, ui, topic, opts, converter):
165 self._ui = ui
165 self._ui = ui
166 self._topic = topic
166 self._topic = topic
167 self._opts = opts
167 self._opts = opts
168 self._converter = converter
168 self._converter = converter
169 self._item = None
169 self._item = None
170 # function to convert node to string suitable for this output
170 # function to convert node to string suitable for this output
171 self.hexfunc = hex
171 self.hexfunc = hex
172 def __enter__(self):
172 def __enter__(self):
173 return self
173 return self
174 def __exit__(self, exctype, excvalue, traceback):
174 def __exit__(self, exctype, excvalue, traceback):
175 if exctype is None:
175 if exctype is None:
176 self.end()
176 self.end()
177 def _showitem(self):
177 def _showitem(self):
178 '''show a formatted item once all data is collected'''
178 '''show a formatted item once all data is collected'''
179 def startitem(self):
179 def startitem(self):
180 '''begin an item in the format list'''
180 '''begin an item in the format list'''
181 if self._item is not None:
181 if self._item is not None:
182 self._showitem()
182 self._showitem()
183 self._item = {}
183 self._item = {}
184 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
184 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
185 '''convert date tuple to appropriate format'''
185 '''convert date tuple to appropriate format'''
186 return self._converter.formatdate(date, fmt)
186 return self._converter.formatdate(date, fmt)
187 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
187 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
188 '''convert dict or key-value pairs to appropriate dict format'''
188 '''convert dict or key-value pairs to appropriate dict format'''
189 return self._converter.formatdict(data, key, value, fmt, sep)
189 return self._converter.formatdict(data, key, value, fmt, sep)
190 def formatlist(self, data, name, fmt=None, sep=' '):
190 def formatlist(self, data, name, fmt=None, sep=' '):
191 '''convert iterable to appropriate list format'''
191 '''convert iterable to appropriate list format'''
192 # name is mandatory argument for now, but it could be optional if
192 # name is mandatory argument for now, but it could be optional if
193 # we have default template keyword, e.g. {item}
193 # we have default template keyword, e.g. {item}
194 return self._converter.formatlist(data, name, fmt, sep)
194 return self._converter.formatlist(data, name, fmt, sep)
195 def context(self, **ctxs):
195 def context(self, **ctxs):
196 '''insert context objects to be used to render template keywords'''
196 '''insert context objects to be used to render template keywords'''
197 ctxs = pycompat.byteskwargs(ctxs)
197 ctxs = pycompat.byteskwargs(ctxs)
198 assert all(k in {'repo', 'ctx', 'fctx'} for k in ctxs)
198 assert all(k in {'repo', 'ctx', 'fctx'} for k in ctxs)
199 if self._converter.storecontext:
199 if self._converter.storecontext:
200 # populate missing resources in fctx -> ctx -> repo order
200 # populate missing resources in fctx -> ctx -> repo order
201 if 'fctx' in ctxs and 'ctx' not in ctxs:
201 if 'fctx' in ctxs and 'ctx' not in ctxs:
202 ctxs['ctx'] = ctxs['fctx'].changectx()
202 ctxs['ctx'] = ctxs['fctx'].changectx()
203 if 'ctx' in ctxs and 'repo' not in ctxs:
203 if 'ctx' in ctxs and 'repo' not in ctxs:
204 ctxs['repo'] = ctxs['ctx'].repo()
204 ctxs['repo'] = ctxs['ctx'].repo()
205 self._item.update(ctxs)
205 self._item.update(ctxs)
206 def datahint(self):
206 def datahint(self):
207 '''set of field names to be referenced'''
207 '''set of field names to be referenced'''
208 return set()
208 return set()
209 def data(self, **data):
209 def data(self, **data):
210 '''insert data into item that's not shown in default output'''
210 '''insert data into item that's not shown in default output'''
211 data = pycompat.byteskwargs(data)
211 data = pycompat.byteskwargs(data)
212 self._item.update(data)
212 self._item.update(data)
213 def write(self, fields, deftext, *fielddata, **opts):
213 def write(self, fields, deftext, *fielddata, **opts):
214 '''do default text output while assigning data to item'''
214 '''do default text output while assigning data to item'''
215 fieldkeys = fields.split()
215 fieldkeys = fields.split()
216 assert len(fieldkeys) == len(fielddata)
216 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
217 self._item.update(zip(fieldkeys, fielddata))
217 self._item.update(zip(fieldkeys, fielddata))
218 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
218 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
219 '''do conditional write (primarily for plain formatter)'''
219 '''do conditional write (primarily for plain formatter)'''
220 fieldkeys = fields.split()
220 fieldkeys = fields.split()
221 assert len(fieldkeys) == len(fielddata)
221 assert len(fieldkeys) == len(fielddata)
222 self._item.update(zip(fieldkeys, fielddata))
222 self._item.update(zip(fieldkeys, fielddata))
223 def plain(self, text, **opts):
223 def plain(self, text, **opts):
224 '''show raw text for non-templated mode'''
224 '''show raw text for non-templated mode'''
225 def isplain(self):
225 def isplain(self):
226 '''check for plain formatter usage'''
226 '''check for plain formatter usage'''
227 return False
227 return False
228 def nested(self, field, tmpl=None, sep=''):
228 def nested(self, field, tmpl=None, sep=''):
229 '''sub formatter to store nested data in the specified field'''
229 '''sub formatter to store nested data in the specified field'''
230 data = []
230 data = []
231 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
231 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
232 return _nestedformatter(self._ui, self._converter, data)
232 return _nestedformatter(self._ui, self._converter, data)
233 def end(self):
233 def end(self):
234 '''end output for the formatter'''
234 '''end output for the formatter'''
235 if self._item is not None:
235 if self._item is not None:
236 self._showitem()
236 self._showitem()
237
237
238 def nullformatter(ui, topic, opts):
238 def nullformatter(ui, topic, opts):
239 '''formatter that prints nothing'''
239 '''formatter that prints nothing'''
240 return baseformatter(ui, topic, opts, converter=_nullconverter)
240 return baseformatter(ui, topic, opts, converter=_nullconverter)
241
241
242 class _nestedformatter(baseformatter):
242 class _nestedformatter(baseformatter):
243 '''build sub items and store them in the parent formatter'''
243 '''build sub items and store them in the parent formatter'''
244 def __init__(self, ui, converter, data):
244 def __init__(self, ui, converter, data):
245 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
245 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
246 self._data = data
246 self._data = data
247 def _showitem(self):
247 def _showitem(self):
248 self._data.append(self._item)
248 self._data.append(self._item)
249
249
250 def _iteritems(data):
250 def _iteritems(data):
251 '''iterate key-value pairs in stable order'''
251 '''iterate key-value pairs in stable order'''
252 if isinstance(data, dict):
252 if isinstance(data, dict):
253 return sorted(data.iteritems())
253 return sorted(data.iteritems())
254 return data
254 return data
255
255
256 class _plainconverter(object):
256 class _plainconverter(object):
257 '''convert non-primitive data types to text'''
257 '''convert non-primitive data types to text'''
258
258
259 storecontext = False
259 storecontext = False
260
260
261 @staticmethod
261 @staticmethod
262 def wrapnested(data, tmpl, sep):
262 def wrapnested(data, tmpl, sep):
263 raise error.ProgrammingError('plainformatter should never be nested')
263 raise error.ProgrammingError('plainformatter should never be nested')
264 @staticmethod
264 @staticmethod
265 def formatdate(date, fmt):
265 def formatdate(date, fmt):
266 '''stringify date tuple in the given format'''
266 '''stringify date tuple in the given format'''
267 return dateutil.datestr(date, fmt)
267 return dateutil.datestr(date, fmt)
268 @staticmethod
268 @staticmethod
269 def formatdict(data, key, value, fmt, sep):
269 def formatdict(data, key, value, fmt, sep):
270 '''stringify key-value pairs separated by sep'''
270 '''stringify key-value pairs separated by sep'''
271 prefmt = pycompat.identity
271 prefmt = pycompat.identity
272 if fmt is None:
272 if fmt is None:
273 fmt = '%s=%s'
273 fmt = '%s=%s'
274 prefmt = pycompat.bytestr
274 prefmt = pycompat.bytestr
275 return sep.join(fmt % (prefmt(k), prefmt(v))
275 return sep.join(fmt % (prefmt(k), prefmt(v))
276 for k, v in _iteritems(data))
276 for k, v in _iteritems(data))
277 @staticmethod
277 @staticmethod
278 def formatlist(data, name, fmt, sep):
278 def formatlist(data, name, fmt, sep):
279 '''stringify iterable separated by sep'''
279 '''stringify iterable separated by sep'''
280 prefmt = pycompat.identity
280 prefmt = pycompat.identity
281 if fmt is None:
281 if fmt is None:
282 fmt = '%s'
282 fmt = '%s'
283 prefmt = pycompat.bytestr
283 prefmt = pycompat.bytestr
284 return sep.join(fmt % prefmt(e) for e in data)
284 return sep.join(fmt % prefmt(e) for e in data)
285
285
286 class plainformatter(baseformatter):
286 class plainformatter(baseformatter):
287 '''the default text output scheme'''
287 '''the default text output scheme'''
288 def __init__(self, ui, out, topic, opts):
288 def __init__(self, ui, out, topic, opts):
289 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
289 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
290 if ui.debugflag:
290 if ui.debugflag:
291 self.hexfunc = hex
291 self.hexfunc = hex
292 else:
292 else:
293 self.hexfunc = short
293 self.hexfunc = short
294 if ui is out:
294 if ui is out:
295 self._write = ui.write
295 self._write = ui.write
296 else:
296 else:
297 self._write = lambda s, **opts: out.write(s)
297 self._write = lambda s, **opts: out.write(s)
298 def startitem(self):
298 def startitem(self):
299 pass
299 pass
300 def data(self, **data):
300 def data(self, **data):
301 pass
301 pass
302 def write(self, fields, deftext, *fielddata, **opts):
302 def write(self, fields, deftext, *fielddata, **opts):
303 self._write(deftext % fielddata, **opts)
303 self._write(deftext % fielddata, **opts)
304 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
304 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
305 '''do conditional write'''
305 '''do conditional write'''
306 if cond:
306 if cond:
307 self._write(deftext % fielddata, **opts)
307 self._write(deftext % fielddata, **opts)
308 def plain(self, text, **opts):
308 def plain(self, text, **opts):
309 self._write(text, **opts)
309 self._write(text, **opts)
310 def isplain(self):
310 def isplain(self):
311 return True
311 return True
312 def nested(self, field, tmpl=None, sep=''):
312 def nested(self, field, tmpl=None, sep=''):
313 # nested data will be directly written to ui
313 # nested data will be directly written to ui
314 return self
314 return self
315 def end(self):
315 def end(self):
316 pass
316 pass
317
317
318 class debugformatter(baseformatter):
318 class debugformatter(baseformatter):
319 def __init__(self, ui, out, topic, opts):
319 def __init__(self, ui, out, topic, opts):
320 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
320 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
321 self._out = out
321 self._out = out
322 self._out.write("%s = [\n" % self._topic)
322 self._out.write("%s = [\n" % self._topic)
323 def _showitem(self):
323 def _showitem(self):
324 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
324 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
325 def end(self):
325 def end(self):
326 baseformatter.end(self)
326 baseformatter.end(self)
327 self._out.write("]\n")
327 self._out.write("]\n")
328
328
329 class pickleformatter(baseformatter):
329 class pickleformatter(baseformatter):
330 def __init__(self, ui, out, topic, opts):
330 def __init__(self, ui, out, topic, opts):
331 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
331 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
332 self._out = out
332 self._out = out
333 self._data = []
333 self._data = []
334 def _showitem(self):
334 def _showitem(self):
335 self._data.append(self._item)
335 self._data.append(self._item)
336 def end(self):
336 def end(self):
337 baseformatter.end(self)
337 baseformatter.end(self)
338 self._out.write(pickle.dumps(self._data))
338 self._out.write(pickle.dumps(self._data))
339
339
340 class jsonformatter(baseformatter):
340 class jsonformatter(baseformatter):
341 def __init__(self, ui, out, topic, opts):
341 def __init__(self, ui, out, topic, opts):
342 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
342 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
343 self._out = out
343 self._out = out
344 self._out.write("[")
344 self._out.write("[")
345 self._first = True
345 self._first = True
346 def _showitem(self):
346 def _showitem(self):
347 if self._first:
347 if self._first:
348 self._first = False
348 self._first = False
349 else:
349 else:
350 self._out.write(",")
350 self._out.write(",")
351
351
352 self._out.write("\n {\n")
352 self._out.write("\n {\n")
353 first = True
353 first = True
354 for k, v in sorted(self._item.items()):
354 for k, v in sorted(self._item.items()):
355 if first:
355 if first:
356 first = False
356 first = False
357 else:
357 else:
358 self._out.write(",\n")
358 self._out.write(",\n")
359 u = templatefilters.json(v, paranoid=False)
359 u = templatefilters.json(v, paranoid=False)
360 self._out.write(' "%s": %s' % (k, u))
360 self._out.write(' "%s": %s' % (k, u))
361 self._out.write("\n }")
361 self._out.write("\n }")
362 def end(self):
362 def end(self):
363 baseformatter.end(self)
363 baseformatter.end(self)
364 self._out.write("\n]\n")
364 self._out.write("\n]\n")
365
365
366 class _templateconverter(object):
366 class _templateconverter(object):
367 '''convert non-primitive data types to be processed by templater'''
367 '''convert non-primitive data types to be processed by templater'''
368
368
369 storecontext = True
369 storecontext = True
370
370
371 @staticmethod
371 @staticmethod
372 def wrapnested(data, tmpl, sep):
372 def wrapnested(data, tmpl, sep):
373 '''wrap nested data by templatable type'''
373 '''wrap nested data by templatable type'''
374 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
374 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
375 @staticmethod
375 @staticmethod
376 def formatdate(date, fmt):
376 def formatdate(date, fmt):
377 '''return date tuple'''
377 '''return date tuple'''
378 return templateutil.date(date)
378 return templateutil.date(date)
379 @staticmethod
379 @staticmethod
380 def formatdict(data, key, value, fmt, sep):
380 def formatdict(data, key, value, fmt, sep):
381 '''build object that can be evaluated as either plain string or dict'''
381 '''build object that can be evaluated as either plain string or dict'''
382 data = util.sortdict(_iteritems(data))
382 data = util.sortdict(_iteritems(data))
383 def f():
383 def f():
384 yield _plainconverter.formatdict(data, key, value, fmt, sep)
384 yield _plainconverter.formatdict(data, key, value, fmt, sep)
385 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
385 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
386 gen=f)
386 gen=f)
387 @staticmethod
387 @staticmethod
388 def formatlist(data, name, fmt, sep):
388 def formatlist(data, name, fmt, sep):
389 '''build object that can be evaluated as either plain string or list'''
389 '''build object that can be evaluated as either plain string or list'''
390 data = list(data)
390 data = list(data)
391 def f():
391 def f():
392 yield _plainconverter.formatlist(data, name, fmt, sep)
392 yield _plainconverter.formatlist(data, name, fmt, sep)
393 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
393 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
394
394
395 class templateformatter(baseformatter):
395 class templateformatter(baseformatter):
396 def __init__(self, ui, out, topic, opts):
396 def __init__(self, ui, out, topic, opts):
397 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
397 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
398 self._out = out
398 self._out = out
399 spec = lookuptemplate(ui, topic, opts.get('template', ''))
399 spec = lookuptemplate(ui, topic, opts.get('template', ''))
400 self._tref = spec.ref
400 self._tref = spec.ref
401 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
401 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
402 resources=templateresources(ui),
402 resources=templateresources(ui),
403 cache=templatekw.defaulttempl)
403 cache=templatekw.defaulttempl)
404 self._parts = templatepartsmap(spec, self._t,
404 self._parts = templatepartsmap(spec, self._t,
405 ['docheader', 'docfooter', 'separator'])
405 ['docheader', 'docfooter', 'separator'])
406 self._counter = itertools.count()
406 self._counter = itertools.count()
407 self._renderitem('docheader', {})
407 self._renderitem('docheader', {})
408
408
409 def _showitem(self):
409 def _showitem(self):
410 item = self._item.copy()
410 item = self._item.copy()
411 item['index'] = index = next(self._counter)
411 item['index'] = index = next(self._counter)
412 if index > 0:
412 if index > 0:
413 self._renderitem('separator', {})
413 self._renderitem('separator', {})
414 self._renderitem(self._tref, item)
414 self._renderitem(self._tref, item)
415
415
416 def _renderitem(self, part, item):
416 def _renderitem(self, part, item):
417 if part not in self._parts:
417 if part not in self._parts:
418 return
418 return
419 ref = self._parts[part]
419 ref = self._parts[part]
420 self._out.write(self._t.render(ref, item))
420 self._out.write(self._t.render(ref, item))
421
421
422 @util.propertycache
422 @util.propertycache
423 def _symbolsused(self):
423 def _symbolsused(self):
424 return self._t.symbolsused(self._tref)
424 return self._t.symbolsused(self._tref)
425
425
426 def datahint(self):
426 def datahint(self):
427 '''set of field names to be referenced from the template'''
427 '''set of field names to be referenced from the template'''
428 return self._symbolsused[0]
428 return self._symbolsused[0]
429
429
430 def end(self):
430 def end(self):
431 baseformatter.end(self)
431 baseformatter.end(self)
432 self._renderitem('docfooter', {})
432 self._renderitem('docfooter', {})
433
433
434 @attr.s(frozen=True)
434 @attr.s(frozen=True)
435 class templatespec(object):
435 class templatespec(object):
436 ref = attr.ib()
436 ref = attr.ib()
437 tmpl = attr.ib()
437 tmpl = attr.ib()
438 mapfile = attr.ib()
438 mapfile = attr.ib()
439
439
440 def lookuptemplate(ui, topic, tmpl):
440 def lookuptemplate(ui, topic, tmpl):
441 """Find the template matching the given -T/--template spec 'tmpl'
441 """Find the template matching the given -T/--template spec 'tmpl'
442
442
443 'tmpl' can be any of the following:
443 'tmpl' can be any of the following:
444
444
445 - a literal template (e.g. '{rev}')
445 - a literal template (e.g. '{rev}')
446 - a map-file name or path (e.g. 'changelog')
446 - a map-file name or path (e.g. 'changelog')
447 - a reference to [templates] in config file
447 - a reference to [templates] in config file
448 - a path to raw template file
448 - a path to raw template file
449
449
450 A map file defines a stand-alone template environment. If a map file
450 A map file defines a stand-alone template environment. If a map file
451 selected, all templates defined in the file will be loaded, and the
451 selected, all templates defined in the file will be loaded, and the
452 template matching the given topic will be rendered. Aliases won't be
452 template matching the given topic will be rendered. Aliases won't be
453 loaded from user config, but from the map file.
453 loaded from user config, but from the map file.
454
454
455 If no map file selected, all templates in [templates] section will be
455 If no map file selected, all templates in [templates] section will be
456 available as well as aliases in [templatealias].
456 available as well as aliases in [templatealias].
457 """
457 """
458
458
459 # looks like a literal template?
459 # looks like a literal template?
460 if '{' in tmpl:
460 if '{' in tmpl:
461 return templatespec('', tmpl, None)
461 return templatespec('', tmpl, None)
462
462
463 # perhaps a stock style?
463 # perhaps a stock style?
464 if not os.path.split(tmpl)[0]:
464 if not os.path.split(tmpl)[0]:
465 mapname = (templater.templatepath('map-cmdline.' + tmpl)
465 mapname = (templater.templatepath('map-cmdline.' + tmpl)
466 or templater.templatepath(tmpl))
466 or templater.templatepath(tmpl))
467 if mapname and os.path.isfile(mapname):
467 if mapname and os.path.isfile(mapname):
468 return templatespec(topic, None, mapname)
468 return templatespec(topic, None, mapname)
469
469
470 # perhaps it's a reference to [templates]
470 # perhaps it's a reference to [templates]
471 if ui.config('templates', tmpl):
471 if ui.config('templates', tmpl):
472 return templatespec(tmpl, None, None)
472 return templatespec(tmpl, None, None)
473
473
474 if tmpl == 'list':
474 if tmpl == 'list':
475 ui.write(_("available styles: %s\n") % templater.stylelist())
475 ui.write(_("available styles: %s\n") % templater.stylelist())
476 raise error.Abort(_("specify a template"))
476 raise error.Abort(_("specify a template"))
477
477
478 # perhaps it's a path to a map or a template
478 # perhaps it's a path to a map or a template
479 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
479 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
480 # is it a mapfile for a style?
480 # is it a mapfile for a style?
481 if os.path.basename(tmpl).startswith("map-"):
481 if os.path.basename(tmpl).startswith("map-"):
482 return templatespec(topic, None, os.path.realpath(tmpl))
482 return templatespec(topic, None, os.path.realpath(tmpl))
483 with util.posixfile(tmpl, 'rb') as f:
483 with util.posixfile(tmpl, 'rb') as f:
484 tmpl = f.read()
484 tmpl = f.read()
485 return templatespec('', tmpl, None)
485 return templatespec('', tmpl, None)
486
486
487 # constant string?
487 # constant string?
488 return templatespec('', tmpl, None)
488 return templatespec('', tmpl, None)
489
489
490 def templatepartsmap(spec, t, partnames):
490 def templatepartsmap(spec, t, partnames):
491 """Create a mapping of {part: ref}"""
491 """Create a mapping of {part: ref}"""
492 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
492 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
493 if spec.mapfile:
493 if spec.mapfile:
494 partsmap.update((p, p) for p in partnames if p in t)
494 partsmap.update((p, p) for p in partnames if p in t)
495 elif spec.ref:
495 elif spec.ref:
496 for part in partnames:
496 for part in partnames:
497 ref = '%s:%s' % (spec.ref, part) # select config sub-section
497 ref = '%s:%s' % (spec.ref, part) # select config sub-section
498 if ref in t:
498 if ref in t:
499 partsmap[part] = ref
499 partsmap[part] = ref
500 return partsmap
500 return partsmap
501
501
502 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
502 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
503 """Create a templater from either a literal template or loading from
503 """Create a templater from either a literal template or loading from
504 a map file"""
504 a map file"""
505 assert not (spec.tmpl and spec.mapfile)
505 assert not (spec.tmpl and spec.mapfile)
506 if spec.mapfile:
506 if spec.mapfile:
507 frommapfile = templater.templater.frommapfile
507 frommapfile = templater.templater.frommapfile
508 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
508 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
509 cache=cache)
509 cache=cache)
510 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
510 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
511 cache=cache)
511 cache=cache)
512
512
513 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
513 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
514 """Create a templater from a string template 'tmpl'"""
514 """Create a templater from a string template 'tmpl'"""
515 aliases = ui.configitems('templatealias')
515 aliases = ui.configitems('templatealias')
516 t = templater.templater(defaults=defaults, resources=resources,
516 t = templater.templater(defaults=defaults, resources=resources,
517 cache=cache, aliases=aliases)
517 cache=cache, aliases=aliases)
518 t.cache.update((k, templater.unquotestring(v))
518 t.cache.update((k, templater.unquotestring(v))
519 for k, v in ui.configitems('templates'))
519 for k, v in ui.configitems('templates'))
520 if tmpl:
520 if tmpl:
521 t.cache[''] = tmpl
521 t.cache[''] = tmpl
522 return t
522 return t
523
523
524 # marker to denote a resource to be loaded on demand based on mapping values
524 # marker to denote a resource to be loaded on demand based on mapping values
525 # (e.g. (ctx, path) -> fctx)
525 # (e.g. (ctx, path) -> fctx)
526 _placeholder = object()
526 _placeholder = object()
527
527
528 class templateresources(templater.resourcemapper):
528 class templateresources(templater.resourcemapper):
529 """Resource mapper designed for the default templatekw and function"""
529 """Resource mapper designed for the default templatekw and function"""
530
530
531 def __init__(self, ui, repo=None):
531 def __init__(self, ui, repo=None):
532 self._resmap = {
532 self._resmap = {
533 'cache': {}, # for templatekw/funcs to store reusable data
533 'cache': {}, # for templatekw/funcs to store reusable data
534 'repo': repo,
534 'repo': repo,
535 'ui': ui,
535 'ui': ui,
536 }
536 }
537
537
538 def availablekeys(self, mapping):
538 def availablekeys(self, mapping):
539 return {k for k in self.knownkeys()
539 return {k for k in self.knownkeys()
540 if self._getsome(mapping, k) is not None}
540 if self._getsome(mapping, k) is not None}
541
541
542 def knownkeys(self):
542 def knownkeys(self):
543 return {'cache', 'ctx', 'fctx', 'repo', 'revcache', 'ui'}
543 return {'cache', 'ctx', 'fctx', 'repo', 'revcache', 'ui'}
544
544
545 def lookup(self, mapping, key):
545 def lookup(self, mapping, key):
546 if key not in self.knownkeys():
546 if key not in self.knownkeys():
547 return None
547 return None
548 v = self._getsome(mapping, key)
548 v = self._getsome(mapping, key)
549 if v is _placeholder:
549 if v is _placeholder:
550 v = mapping[key] = self._loadermap[key](self, mapping)
550 v = mapping[key] = self._loadermap[key](self, mapping)
551 return v
551 return v
552
552
553 def populatemap(self, context, origmapping, newmapping):
553 def populatemap(self, context, origmapping, newmapping):
554 mapping = {}
554 mapping = {}
555 if self._hasnodespec(newmapping):
555 if self._hasnodespec(newmapping):
556 mapping['revcache'] = {} # per-ctx cache
556 mapping['revcache'] = {} # per-ctx cache
557 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
557 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
558 orignode = templateutil.runsymbol(context, origmapping, 'node')
558 orignode = templateutil.runsymbol(context, origmapping, 'node')
559 mapping['originalnode'] = orignode
559 mapping['originalnode'] = orignode
560 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
560 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
561 # its existence to be reported by availablekeys()
561 # its existence to be reported by availablekeys()
562 if 'ctx' not in newmapping and self._hasliteral(newmapping, 'node'):
562 if 'ctx' not in newmapping and self._hasliteral(newmapping, 'node'):
563 mapping['ctx'] = _placeholder
563 mapping['ctx'] = _placeholder
564 if 'fctx' not in newmapping and self._hasliteral(newmapping, 'path'):
564 if 'fctx' not in newmapping and self._hasliteral(newmapping, 'path'):
565 mapping['fctx'] = _placeholder
565 mapping['fctx'] = _placeholder
566 return mapping
566 return mapping
567
567
568 def _getsome(self, mapping, key):
568 def _getsome(self, mapping, key):
569 v = mapping.get(key)
569 v = mapping.get(key)
570 if v is not None:
570 if v is not None:
571 return v
571 return v
572 return self._resmap.get(key)
572 return self._resmap.get(key)
573
573
574 def _hasliteral(self, mapping, key):
574 def _hasliteral(self, mapping, key):
575 """Test if a literal value is set or unset in the given mapping"""
575 """Test if a literal value is set or unset in the given mapping"""
576 return key in mapping and not callable(mapping[key])
576 return key in mapping and not callable(mapping[key])
577
577
578 def _getliteral(self, mapping, key):
578 def _getliteral(self, mapping, key):
579 """Return value of the given name if it is a literal"""
579 """Return value of the given name if it is a literal"""
580 v = mapping.get(key)
580 v = mapping.get(key)
581 if callable(v):
581 if callable(v):
582 return None
582 return None
583 return v
583 return v
584
584
585 def _hasnodespec(self, mapping):
585 def _hasnodespec(self, mapping):
586 """Test if context revision is set or unset in the given mapping"""
586 """Test if context revision is set or unset in the given mapping"""
587 return 'node' in mapping or 'ctx' in mapping
587 return 'node' in mapping or 'ctx' in mapping
588
588
589 def _loadctx(self, mapping):
589 def _loadctx(self, mapping):
590 repo = self._getsome(mapping, 'repo')
590 repo = self._getsome(mapping, 'repo')
591 node = self._getliteral(mapping, 'node')
591 node = self._getliteral(mapping, 'node')
592 if repo is None or node is None:
592 if repo is None or node is None:
593 return
593 return
594 try:
594 try:
595 return repo[node]
595 return repo[node]
596 except error.RepoLookupError:
596 except error.RepoLookupError:
597 return None # maybe hidden/non-existent node
597 return None # maybe hidden/non-existent node
598
598
599 def _loadfctx(self, mapping):
599 def _loadfctx(self, mapping):
600 ctx = self._getsome(mapping, 'ctx')
600 ctx = self._getsome(mapping, 'ctx')
601 path = self._getliteral(mapping, 'path')
601 path = self._getliteral(mapping, 'path')
602 if ctx is None or path is None:
602 if ctx is None or path is None:
603 return None
603 return None
604 try:
604 try:
605 return ctx[path]
605 return ctx[path]
606 except error.LookupError:
606 except error.LookupError:
607 return None # maybe removed file?
607 return None # maybe removed file?
608
608
609 _loadermap = {
609 _loadermap = {
610 'ctx': _loadctx,
610 'ctx': _loadctx,
611 'fctx': _loadfctx,
611 'fctx': _loadfctx,
612 }
612 }
613
613
614 def formatter(ui, out, topic, opts):
614 def formatter(ui, out, topic, opts):
615 template = opts.get("template", "")
615 template = opts.get("template", "")
616 if template == "json":
616 if template == "json":
617 return jsonformatter(ui, out, topic, opts)
617 return jsonformatter(ui, out, topic, opts)
618 elif template == "pickle":
618 elif template == "pickle":
619 return pickleformatter(ui, out, topic, opts)
619 return pickleformatter(ui, out, topic, opts)
620 elif template == "debug":
620 elif template == "debug":
621 return debugformatter(ui, out, topic, opts)
621 return debugformatter(ui, out, topic, opts)
622 elif template != "":
622 elif template != "":
623 return templateformatter(ui, out, topic, opts)
623 return templateformatter(ui, out, topic, opts)
624 # developer config: ui.formatdebug
624 # developer config: ui.formatdebug
625 elif ui.configbool('ui', 'formatdebug'):
625 elif ui.configbool('ui', 'formatdebug'):
626 return debugformatter(ui, out, topic, opts)
626 return debugformatter(ui, out, topic, opts)
627 # deprecated config: ui.formatjson
627 # deprecated config: ui.formatjson
628 elif ui.configbool('ui', 'formatjson'):
628 elif ui.configbool('ui', 'formatjson'):
629 return jsonformatter(ui, out, topic, opts)
629 return jsonformatter(ui, out, topic, opts)
630 return plainformatter(ui, out, topic, opts)
630 return plainformatter(ui, out, topic, opts)
631
631
632 @contextlib.contextmanager
632 @contextlib.contextmanager
633 def openformatter(ui, filename, topic, opts):
633 def openformatter(ui, filename, topic, opts):
634 """Create a formatter that writes outputs to the specified file
634 """Create a formatter that writes outputs to the specified file
635
635
636 Must be invoked using the 'with' statement.
636 Must be invoked using the 'with' statement.
637 """
637 """
638 with util.posixfile(filename, 'wb') as out:
638 with util.posixfile(filename, 'wb') as out:
639 with formatter(ui, out, topic, opts) as fm:
639 with formatter(ui, out, topic, opts) as fm:
640 yield fm
640 yield fm
641
641
642 @contextlib.contextmanager
642 @contextlib.contextmanager
643 def _neverending(fm):
643 def _neverending(fm):
644 yield fm
644 yield fm
645
645
646 def maybereopen(fm, filename):
646 def maybereopen(fm, filename):
647 """Create a formatter backed by file if filename specified, else return
647 """Create a formatter backed by file if filename specified, else return
648 the given formatter
648 the given formatter
649
649
650 Must be invoked using the 'with' statement. This will never call fm.end()
650 Must be invoked using the 'with' statement. This will never call fm.end()
651 of the given formatter.
651 of the given formatter.
652 """
652 """
653 if filename:
653 if filename:
654 return openformatter(fm._ui, filename, fm._topic, fm._opts)
654 return openformatter(fm._ui, filename, fm._topic, fm._opts)
655 else:
655 else:
656 return _neverending(fm)
656 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now