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