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