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