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