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