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