##// END OF EJS Templates
formatter: port handling of 'originalnode' to populatemap() hook...
Yuya Nishihara -
r37122:7db3c28d default
parent child Browse files
Show More
@@ -1,594 +1,596 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 in {'ctx', 'fctx'} 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 self._out.write(self._t.render(ref, item))
397 self._out.write(self._t.render(ref, item))
398
398
399 def end(self):
399 def end(self):
400 baseformatter.end(self)
400 baseformatter.end(self)
401 self._renderitem('docfooter', {})
401 self._renderitem('docfooter', {})
402
402
403 templatespec = collections.namedtuple(r'templatespec',
403 templatespec = collections.namedtuple(r'templatespec',
404 r'ref tmpl mapfile')
404 r'ref tmpl mapfile')
405
405
406 def lookuptemplate(ui, topic, tmpl):
406 def lookuptemplate(ui, topic, tmpl):
407 """Find the template matching the given -T/--template spec 'tmpl'
407 """Find the template matching the given -T/--template spec 'tmpl'
408
408
409 'tmpl' can be any of the following:
409 'tmpl' can be any of the following:
410
410
411 - a literal template (e.g. '{rev}')
411 - a literal template (e.g. '{rev}')
412 - a map-file name or path (e.g. 'changelog')
412 - a map-file name or path (e.g. 'changelog')
413 - a reference to [templates] in config file
413 - a reference to [templates] in config file
414 - a path to raw template file
414 - a path to raw template file
415
415
416 A map file defines a stand-alone template environment. If a map file
416 A map file defines a stand-alone template environment. If a map file
417 selected, all templates defined in the file will be loaded, and the
417 selected, all templates defined in the file will be loaded, and the
418 template matching the given topic will be rendered. Aliases won't be
418 template matching the given topic will be rendered. Aliases won't be
419 loaded from user config, but from the map file.
419 loaded from user config, but from the map file.
420
420
421 If no map file selected, all templates in [templates] section will be
421 If no map file selected, all templates in [templates] section will be
422 available as well as aliases in [templatealias].
422 available as well as aliases in [templatealias].
423 """
423 """
424
424
425 # looks like a literal template?
425 # looks like a literal template?
426 if '{' in tmpl:
426 if '{' in tmpl:
427 return templatespec('', tmpl, None)
427 return templatespec('', tmpl, None)
428
428
429 # perhaps a stock style?
429 # perhaps a stock style?
430 if not os.path.split(tmpl)[0]:
430 if not os.path.split(tmpl)[0]:
431 mapname = (templater.templatepath('map-cmdline.' + tmpl)
431 mapname = (templater.templatepath('map-cmdline.' + tmpl)
432 or templater.templatepath(tmpl))
432 or templater.templatepath(tmpl))
433 if mapname and os.path.isfile(mapname):
433 if mapname and os.path.isfile(mapname):
434 return templatespec(topic, None, mapname)
434 return templatespec(topic, None, mapname)
435
435
436 # perhaps it's a reference to [templates]
436 # perhaps it's a reference to [templates]
437 if ui.config('templates', tmpl):
437 if ui.config('templates', tmpl):
438 return templatespec(tmpl, None, None)
438 return templatespec(tmpl, None, None)
439
439
440 if tmpl == 'list':
440 if tmpl == 'list':
441 ui.write(_("available styles: %s\n") % templater.stylelist())
441 ui.write(_("available styles: %s\n") % templater.stylelist())
442 raise error.Abort(_("specify a template"))
442 raise error.Abort(_("specify a template"))
443
443
444 # perhaps it's a path to a map or a template
444 # perhaps it's a path to a map or a template
445 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
445 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
446 # is it a mapfile for a style?
446 # is it a mapfile for a style?
447 if os.path.basename(tmpl).startswith("map-"):
447 if os.path.basename(tmpl).startswith("map-"):
448 return templatespec(topic, None, os.path.realpath(tmpl))
448 return templatespec(topic, None, os.path.realpath(tmpl))
449 with util.posixfile(tmpl, 'rb') as f:
449 with util.posixfile(tmpl, 'rb') as f:
450 tmpl = f.read()
450 tmpl = f.read()
451 return templatespec('', tmpl, None)
451 return templatespec('', tmpl, None)
452
452
453 # constant string?
453 # constant string?
454 return templatespec('', tmpl, None)
454 return templatespec('', tmpl, None)
455
455
456 def templatepartsmap(spec, t, partnames):
456 def templatepartsmap(spec, t, partnames):
457 """Create a mapping of {part: ref}"""
457 """Create a mapping of {part: ref}"""
458 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
458 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
459 if spec.mapfile:
459 if spec.mapfile:
460 partsmap.update((p, p) for p in partnames if p in t)
460 partsmap.update((p, p) for p in partnames if p in t)
461 elif spec.ref:
461 elif spec.ref:
462 for part in partnames:
462 for part in partnames:
463 ref = '%s:%s' % (spec.ref, part) # select config sub-section
463 ref = '%s:%s' % (spec.ref, part) # select config sub-section
464 if ref in t:
464 if ref in t:
465 partsmap[part] = ref
465 partsmap[part] = ref
466 return partsmap
466 return partsmap
467
467
468 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
468 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
469 """Create a templater from either a literal template or loading from
469 """Create a templater from either a literal template or loading from
470 a map file"""
470 a map file"""
471 assert not (spec.tmpl and spec.mapfile)
471 assert not (spec.tmpl and spec.mapfile)
472 if spec.mapfile:
472 if spec.mapfile:
473 frommapfile = templater.templater.frommapfile
473 frommapfile = templater.templater.frommapfile
474 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
474 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
475 cache=cache)
475 cache=cache)
476 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
476 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
477 cache=cache)
477 cache=cache)
478
478
479 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
479 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
480 """Create a templater from a string template 'tmpl'"""
480 """Create a templater from a string template 'tmpl'"""
481 aliases = ui.configitems('templatealias')
481 aliases = ui.configitems('templatealias')
482 t = templater.templater(defaults=defaults, resources=resources,
482 t = templater.templater(defaults=defaults, resources=resources,
483 cache=cache, aliases=aliases)
483 cache=cache, aliases=aliases)
484 t.cache.update((k, templater.unquotestring(v))
484 t.cache.update((k, templater.unquotestring(v))
485 for k, v in ui.configitems('templates'))
485 for k, v in ui.configitems('templates'))
486 if tmpl:
486 if tmpl:
487 t.cache[''] = tmpl
487 t.cache[''] = tmpl
488 return t
488 return t
489
489
490 class templateresources(templater.resourcemapper):
490 class templateresources(templater.resourcemapper):
491 """Resource mapper designed for the default templatekw and function"""
491 """Resource mapper designed for the default templatekw and function"""
492
492
493 def __init__(self, ui, repo=None):
493 def __init__(self, ui, repo=None):
494 self._resmap = {
494 self._resmap = {
495 'cache': {}, # for templatekw/funcs to store reusable data
495 'cache': {}, # for templatekw/funcs to store reusable data
496 'repo': repo,
496 'repo': repo,
497 'ui': ui,
497 'ui': ui,
498 }
498 }
499
499
500 def availablekeys(self, context, mapping):
500 def availablekeys(self, context, mapping):
501 return {k for k, g in self._gettermap.iteritems()
501 return {k for k, g in self._gettermap.iteritems()
502 if g(self, context, mapping, k) is not None}
502 if g(self, context, mapping, k) is not None}
503
503
504 def knownkeys(self):
504 def knownkeys(self):
505 return self._knownkeys
505 return self._knownkeys
506
506
507 def lookup(self, context, mapping, key):
507 def lookup(self, context, mapping, key):
508 get = self._gettermap.get(key)
508 get = self._gettermap.get(key)
509 if not get:
509 if not get:
510 return None
510 return None
511 return get(self, context, mapping, key)
511 return get(self, context, mapping, key)
512
512
513 def populatemap(self, context, origmapping, newmapping):
513 def populatemap(self, context, origmapping, newmapping):
514 mapping = {}
514 mapping = {}
515 if self._hasctx(newmapping):
515 if self._hasctx(newmapping):
516 mapping['revcache'] = {} # per-ctx cache
516 mapping['revcache'] = {} # per-ctx cache
517 if 'node' in origmapping and 'node' in newmapping:
518 mapping['originalnode'] = origmapping['node']
517 return mapping
519 return mapping
518
520
519 def _getsome(self, context, mapping, key):
521 def _getsome(self, context, mapping, key):
520 v = mapping.get(key)
522 v = mapping.get(key)
521 if v is not None:
523 if v is not None:
522 return v
524 return v
523 return self._resmap.get(key)
525 return self._resmap.get(key)
524
526
525 def _hasctx(self, mapping):
527 def _hasctx(self, mapping):
526 return 'ctx' in mapping or 'fctx' in mapping
528 return 'ctx' in mapping or 'fctx' in mapping
527
529
528 def _getctx(self, context, mapping, key):
530 def _getctx(self, context, mapping, key):
529 ctx = mapping.get('ctx')
531 ctx = mapping.get('ctx')
530 if ctx is not None:
532 if ctx is not None:
531 return ctx
533 return ctx
532 fctx = mapping.get('fctx')
534 fctx = mapping.get('fctx')
533 if fctx is not None:
535 if fctx is not None:
534 return fctx.changectx()
536 return fctx.changectx()
535
537
536 def _getrepo(self, context, mapping, key):
538 def _getrepo(self, context, mapping, key):
537 ctx = self._getctx(context, mapping, 'ctx')
539 ctx = self._getctx(context, mapping, 'ctx')
538 if ctx is not None:
540 if ctx is not None:
539 return ctx.repo()
541 return ctx.repo()
540 return self._getsome(context, mapping, key)
542 return self._getsome(context, mapping, key)
541
543
542 _gettermap = {
544 _gettermap = {
543 'cache': _getsome,
545 'cache': _getsome,
544 'ctx': _getctx,
546 'ctx': _getctx,
545 'fctx': _getsome,
547 'fctx': _getsome,
546 'repo': _getrepo,
548 'repo': _getrepo,
547 'revcache': _getsome,
549 'revcache': _getsome,
548 'ui': _getsome,
550 'ui': _getsome,
549 }
551 }
550 _knownkeys = set(_gettermap.keys())
552 _knownkeys = set(_gettermap.keys())
551
553
552 def formatter(ui, out, topic, opts):
554 def formatter(ui, out, topic, opts):
553 template = opts.get("template", "")
555 template = opts.get("template", "")
554 if template == "json":
556 if template == "json":
555 return jsonformatter(ui, out, topic, opts)
557 return jsonformatter(ui, out, topic, opts)
556 elif template == "pickle":
558 elif template == "pickle":
557 return pickleformatter(ui, out, topic, opts)
559 return pickleformatter(ui, out, topic, opts)
558 elif template == "debug":
560 elif template == "debug":
559 return debugformatter(ui, out, topic, opts)
561 return debugformatter(ui, out, topic, opts)
560 elif template != "":
562 elif template != "":
561 return templateformatter(ui, out, topic, opts)
563 return templateformatter(ui, out, topic, opts)
562 # developer config: ui.formatdebug
564 # developer config: ui.formatdebug
563 elif ui.configbool('ui', 'formatdebug'):
565 elif ui.configbool('ui', 'formatdebug'):
564 return debugformatter(ui, out, topic, opts)
566 return debugformatter(ui, out, topic, opts)
565 # deprecated config: ui.formatjson
567 # deprecated config: ui.formatjson
566 elif ui.configbool('ui', 'formatjson'):
568 elif ui.configbool('ui', 'formatjson'):
567 return jsonformatter(ui, out, topic, opts)
569 return jsonformatter(ui, out, topic, opts)
568 return plainformatter(ui, out, topic, opts)
570 return plainformatter(ui, out, topic, opts)
569
571
570 @contextlib.contextmanager
572 @contextlib.contextmanager
571 def openformatter(ui, filename, topic, opts):
573 def openformatter(ui, filename, topic, opts):
572 """Create a formatter that writes outputs to the specified file
574 """Create a formatter that writes outputs to the specified file
573
575
574 Must be invoked using the 'with' statement.
576 Must be invoked using the 'with' statement.
575 """
577 """
576 with util.posixfile(filename, 'wb') as out:
578 with util.posixfile(filename, 'wb') as out:
577 with formatter(ui, out, topic, opts) as fm:
579 with formatter(ui, out, topic, opts) as fm:
578 yield fm
580 yield fm
579
581
580 @contextlib.contextmanager
582 @contextlib.contextmanager
581 def _neverending(fm):
583 def _neverending(fm):
582 yield fm
584 yield fm
583
585
584 def maybereopen(fm, filename, opts):
586 def maybereopen(fm, filename, opts):
585 """Create a formatter backed by file if filename specified, else return
587 """Create a formatter backed by file if filename specified, else return
586 the given formatter
588 the given formatter
587
589
588 Must be invoked using the 'with' statement. This will never call fm.end()
590 Must be invoked using the 'with' statement. This will never call fm.end()
589 of the given formatter.
591 of the given formatter.
590 """
592 """
591 if filename:
593 if filename:
592 return openformatter(fm._ui, filename, fm._topic, opts)
594 return openformatter(fm._ui, filename, fm._topic, opts)
593 else:
595 else:
594 return _neverending(fm)
596 return _neverending(fm)
@@ -1,452 +1,451 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import types
10 import types
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 )
17 )
18 from .utils import (
18 from .utils import (
19 stringutil,
19 stringutil,
20 )
20 )
21
21
22 class ResourceUnavailable(error.Abort):
22 class ResourceUnavailable(error.Abort):
23 pass
23 pass
24
24
25 class TemplateNotFound(error.Abort):
25 class TemplateNotFound(error.Abort):
26 pass
26 pass
27
27
28 class hybrid(object):
28 class hybrid(object):
29 """Wrapper for list or dict to support legacy template
29 """Wrapper for list or dict to support legacy template
30
30
31 This class allows us to handle both:
31 This class allows us to handle both:
32 - "{files}" (legacy command-line-specific list hack) and
32 - "{files}" (legacy command-line-specific list hack) and
33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 and to access raw values:
34 and to access raw values:
35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 - "{get(extras, key)}"
36 - "{get(extras, key)}"
37 - "{files|json}"
37 - "{files|json}"
38 """
38 """
39
39
40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 if gen is not None:
41 if gen is not None:
42 self.gen = gen # generator or function returning generator
42 self.gen = gen # generator or function returning generator
43 self._values = values
43 self._values = values
44 self._makemap = makemap
44 self._makemap = makemap
45 self.joinfmt = joinfmt
45 self.joinfmt = joinfmt
46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 def gen(self):
47 def gen(self):
48 """Default generator to stringify this as {join(self, ' ')}"""
48 """Default generator to stringify this as {join(self, ' ')}"""
49 for i, x in enumerate(self._values):
49 for i, x in enumerate(self._values):
50 if i > 0:
50 if i > 0:
51 yield ' '
51 yield ' '
52 yield self.joinfmt(x)
52 yield self.joinfmt(x)
53 def itermaps(self):
53 def itermaps(self):
54 makemap = self._makemap
54 makemap = self._makemap
55 for x in self._values:
55 for x in self._values:
56 yield makemap(x)
56 yield makemap(x)
57 def __contains__(self, x):
57 def __contains__(self, x):
58 return x in self._values
58 return x in self._values
59 def __getitem__(self, key):
59 def __getitem__(self, key):
60 return self._values[key]
60 return self._values[key]
61 def __len__(self):
61 def __len__(self):
62 return len(self._values)
62 return len(self._values)
63 def __iter__(self):
63 def __iter__(self):
64 return iter(self._values)
64 return iter(self._values)
65 def __getattr__(self, name):
65 def __getattr__(self, name):
66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 r'itervalues', r'keys', r'values'):
67 r'itervalues', r'keys', r'values'):
68 raise AttributeError(name)
68 raise AttributeError(name)
69 return getattr(self._values, name)
69 return getattr(self._values, name)
70
70
71 class mappable(object):
71 class mappable(object):
72 """Wrapper for non-list/dict object to support map operation
72 """Wrapper for non-list/dict object to support map operation
73
73
74 This class allows us to handle both:
74 This class allows us to handle both:
75 - "{manifest}"
75 - "{manifest}"
76 - "{manifest % '{rev}:{node}'}"
76 - "{manifest % '{rev}:{node}'}"
77 - "{manifest.rev}"
77 - "{manifest.rev}"
78
78
79 Unlike a hybrid, this does not simulate the behavior of the underling
79 Unlike a hybrid, this does not simulate the behavior of the underling
80 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
80 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 """
81 """
82
82
83 def __init__(self, gen, key, value, makemap):
83 def __init__(self, gen, key, value, makemap):
84 if gen is not None:
84 if gen is not None:
85 self.gen = gen # generator or function returning generator
85 self.gen = gen # generator or function returning generator
86 self._key = key
86 self._key = key
87 self._value = value # may be generator of strings
87 self._value = value # may be generator of strings
88 self._makemap = makemap
88 self._makemap = makemap
89
89
90 def gen(self):
90 def gen(self):
91 yield pycompat.bytestr(self._value)
91 yield pycompat.bytestr(self._value)
92
92
93 def tomap(self):
93 def tomap(self):
94 return self._makemap(self._key)
94 return self._makemap(self._key)
95
95
96 def itermaps(self):
96 def itermaps(self):
97 yield self.tomap()
97 yield self.tomap()
98
98
99 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
99 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
100 """Wrap data to support both dict-like and string-like operations"""
100 """Wrap data to support both dict-like and string-like operations"""
101 prefmt = pycompat.identity
101 prefmt = pycompat.identity
102 if fmt is None:
102 if fmt is None:
103 fmt = '%s=%s'
103 fmt = '%s=%s'
104 prefmt = pycompat.bytestr
104 prefmt = pycompat.bytestr
105 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
105 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
106 lambda k: fmt % (prefmt(k), prefmt(data[k])))
106 lambda k: fmt % (prefmt(k), prefmt(data[k])))
107
107
108 def hybridlist(data, name, fmt=None, gen=None):
108 def hybridlist(data, name, fmt=None, gen=None):
109 """Wrap data to support both list-like and string-like operations"""
109 """Wrap data to support both list-like and string-like operations"""
110 prefmt = pycompat.identity
110 prefmt = pycompat.identity
111 if fmt is None:
111 if fmt is None:
112 fmt = '%s'
112 fmt = '%s'
113 prefmt = pycompat.bytestr
113 prefmt = pycompat.bytestr
114 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
114 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
115
115
116 def unwraphybrid(thing):
116 def unwraphybrid(thing):
117 """Return an object which can be stringified possibly by using a legacy
117 """Return an object which can be stringified possibly by using a legacy
118 template"""
118 template"""
119 gen = getattr(thing, 'gen', None)
119 gen = getattr(thing, 'gen', None)
120 if gen is None:
120 if gen is None:
121 return thing
121 return thing
122 if callable(gen):
122 if callable(gen):
123 return gen()
123 return gen()
124 return gen
124 return gen
125
125
126 def unwrapvalue(thing):
126 def unwrapvalue(thing):
127 """Move the inner value object out of the wrapper"""
127 """Move the inner value object out of the wrapper"""
128 if not util.safehasattr(thing, '_value'):
128 if not util.safehasattr(thing, '_value'):
129 return thing
129 return thing
130 return thing._value
130 return thing._value
131
131
132 def wraphybridvalue(container, key, value):
132 def wraphybridvalue(container, key, value):
133 """Wrap an element of hybrid container to be mappable
133 """Wrap an element of hybrid container to be mappable
134
134
135 The key is passed to the makemap function of the given container, which
135 The key is passed to the makemap function of the given container, which
136 should be an item generated by iter(container).
136 should be an item generated by iter(container).
137 """
137 """
138 makemap = getattr(container, '_makemap', None)
138 makemap = getattr(container, '_makemap', None)
139 if makemap is None:
139 if makemap is None:
140 return value
140 return value
141 if util.safehasattr(value, '_makemap'):
141 if util.safehasattr(value, '_makemap'):
142 # a nested hybrid list/dict, which has its own way of map operation
142 # a nested hybrid list/dict, which has its own way of map operation
143 return value
143 return value
144 return mappable(None, key, value, makemap)
144 return mappable(None, key, value, makemap)
145
145
146 def compatdict(context, mapping, name, data, key='key', value='value',
146 def compatdict(context, mapping, name, data, key='key', value='value',
147 fmt=None, plural=None, separator=' '):
147 fmt=None, plural=None, separator=' '):
148 """Wrap data like hybriddict(), but also supports old-style list template
148 """Wrap data like hybriddict(), but also supports old-style list template
149
149
150 This exists for backward compatibility with the old-style template. Use
150 This exists for backward compatibility with the old-style template. Use
151 hybriddict() for new template keywords.
151 hybriddict() for new template keywords.
152 """
152 """
153 c = [{key: k, value: v} for k, v in data.iteritems()]
153 c = [{key: k, value: v} for k, v in data.iteritems()]
154 f = _showcompatlist(context, mapping, name, c, plural, separator)
154 f = _showcompatlist(context, mapping, name, c, plural, separator)
155 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
155 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
156
156
157 def compatlist(context, mapping, name, data, element=None, fmt=None,
157 def compatlist(context, mapping, name, data, element=None, fmt=None,
158 plural=None, separator=' '):
158 plural=None, separator=' '):
159 """Wrap data like hybridlist(), but also supports old-style list template
159 """Wrap data like hybridlist(), but also supports old-style list template
160
160
161 This exists for backward compatibility with the old-style template. Use
161 This exists for backward compatibility with the old-style template. Use
162 hybridlist() for new template keywords.
162 hybridlist() for new template keywords.
163 """
163 """
164 f = _showcompatlist(context, mapping, name, data, plural, separator)
164 f = _showcompatlist(context, mapping, name, data, plural, separator)
165 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
165 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
166
166
167 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
167 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
168 """Return a generator that renders old-style list template
168 """Return a generator that renders old-style list template
169
169
170 name is name of key in template map.
170 name is name of key in template map.
171 values is list of strings or dicts.
171 values is list of strings or dicts.
172 plural is plural of name, if not simply name + 's'.
172 plural is plural of name, if not simply name + 's'.
173 separator is used to join values as a string
173 separator is used to join values as a string
174
174
175 expansion works like this, given name 'foo'.
175 expansion works like this, given name 'foo'.
176
176
177 if values is empty, expand 'no_foos'.
177 if values is empty, expand 'no_foos'.
178
178
179 if 'foo' not in template map, return values as a string,
179 if 'foo' not in template map, return values as a string,
180 joined by 'separator'.
180 joined by 'separator'.
181
181
182 expand 'start_foos'.
182 expand 'start_foos'.
183
183
184 for each value, expand 'foo'. if 'last_foo' in template
184 for each value, expand 'foo'. if 'last_foo' in template
185 map, expand it instead of 'foo' for last key.
185 map, expand it instead of 'foo' for last key.
186
186
187 expand 'end_foos'.
187 expand 'end_foos'.
188 """
188 """
189 if not plural:
189 if not plural:
190 plural = name + 's'
190 plural = name + 's'
191 if not values:
191 if not values:
192 noname = 'no_' + plural
192 noname = 'no_' + plural
193 if context.preload(noname):
193 if context.preload(noname):
194 yield context.process(noname, mapping)
194 yield context.process(noname, mapping)
195 return
195 return
196 if not context.preload(name):
196 if not context.preload(name):
197 if isinstance(values[0], bytes):
197 if isinstance(values[0], bytes):
198 yield separator.join(values)
198 yield separator.join(values)
199 else:
199 else:
200 for v in values:
200 for v in values:
201 r = dict(v)
201 r = dict(v)
202 r.update(mapping)
202 r.update(mapping)
203 yield r
203 yield r
204 return
204 return
205 startname = 'start_' + plural
205 startname = 'start_' + plural
206 if context.preload(startname):
206 if context.preload(startname):
207 yield context.process(startname, mapping)
207 yield context.process(startname, mapping)
208 def one(v, tag=name):
208 def one(v, tag=name):
209 vmapping = {}
209 vmapping = {}
210 try:
210 try:
211 vmapping.update(v)
211 vmapping.update(v)
212 # Python 2 raises ValueError if the type of v is wrong. Python
212 # Python 2 raises ValueError if the type of v is wrong. Python
213 # 3 raises TypeError.
213 # 3 raises TypeError.
214 except (AttributeError, TypeError, ValueError):
214 except (AttributeError, TypeError, ValueError):
215 try:
215 try:
216 # Python 2 raises ValueError trying to destructure an e.g.
216 # Python 2 raises ValueError trying to destructure an e.g.
217 # bytes. Python 3 raises TypeError.
217 # bytes. Python 3 raises TypeError.
218 for a, b in v:
218 for a, b in v:
219 vmapping[a] = b
219 vmapping[a] = b
220 except (TypeError, ValueError):
220 except (TypeError, ValueError):
221 vmapping[name] = v
221 vmapping[name] = v
222 vmapping = context.overlaymap(mapping, vmapping)
222 vmapping = context.overlaymap(mapping, vmapping)
223 return context.process(tag, vmapping)
223 return context.process(tag, vmapping)
224 lastname = 'last_' + name
224 lastname = 'last_' + name
225 if context.preload(lastname):
225 if context.preload(lastname):
226 last = values.pop()
226 last = values.pop()
227 else:
227 else:
228 last = None
228 last = None
229 for v in values:
229 for v in values:
230 yield one(v)
230 yield one(v)
231 if last is not None:
231 if last is not None:
232 yield one(last, tag=lastname)
232 yield one(last, tag=lastname)
233 endname = 'end_' + plural
233 endname = 'end_' + plural
234 if context.preload(endname):
234 if context.preload(endname):
235 yield context.process(endname, mapping)
235 yield context.process(endname, mapping)
236
236
237 def stringify(thing):
237 def stringify(thing):
238 """Turn values into bytes by converting into text and concatenating them"""
238 """Turn values into bytes by converting into text and concatenating them"""
239 thing = unwraphybrid(thing)
239 thing = unwraphybrid(thing)
240 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
240 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
241 if isinstance(thing, str):
241 if isinstance(thing, str):
242 # This is only reachable on Python 3 (otherwise
242 # This is only reachable on Python 3 (otherwise
243 # isinstance(thing, bytes) would have been true), and is
243 # isinstance(thing, bytes) would have been true), and is
244 # here to prevent infinite recursion bugs on Python 3.
244 # here to prevent infinite recursion bugs on Python 3.
245 raise error.ProgrammingError(
245 raise error.ProgrammingError(
246 'stringify got unexpected unicode string: %r' % thing)
246 'stringify got unexpected unicode string: %r' % thing)
247 return "".join([stringify(t) for t in thing if t is not None])
247 return "".join([stringify(t) for t in thing if t is not None])
248 if thing is None:
248 if thing is None:
249 return ""
249 return ""
250 return pycompat.bytestr(thing)
250 return pycompat.bytestr(thing)
251
251
252 def findsymbolicname(arg):
252 def findsymbolicname(arg):
253 """Find symbolic name for the given compiled expression; returns None
253 """Find symbolic name for the given compiled expression; returns None
254 if nothing found reliably"""
254 if nothing found reliably"""
255 while True:
255 while True:
256 func, data = arg
256 func, data = arg
257 if func is runsymbol:
257 if func is runsymbol:
258 return data
258 return data
259 elif func is runfilter:
259 elif func is runfilter:
260 arg = data[0]
260 arg = data[0]
261 else:
261 else:
262 return None
262 return None
263
263
264 def evalrawexp(context, mapping, arg):
264 def evalrawexp(context, mapping, arg):
265 """Evaluate given argument as a bare template object which may require
265 """Evaluate given argument as a bare template object which may require
266 further processing (such as folding generator of strings)"""
266 further processing (such as folding generator of strings)"""
267 func, data = arg
267 func, data = arg
268 return func(context, mapping, data)
268 return func(context, mapping, data)
269
269
270 def evalfuncarg(context, mapping, arg):
270 def evalfuncarg(context, mapping, arg):
271 """Evaluate given argument as value type"""
271 """Evaluate given argument as value type"""
272 thing = evalrawexp(context, mapping, arg)
272 thing = evalrawexp(context, mapping, arg)
273 thing = unwrapvalue(thing)
273 thing = unwrapvalue(thing)
274 # evalrawexp() may return string, generator of strings or arbitrary object
274 # evalrawexp() may return string, generator of strings or arbitrary object
275 # such as date tuple, but filter does not want generator.
275 # such as date tuple, but filter does not want generator.
276 if isinstance(thing, types.GeneratorType):
276 if isinstance(thing, types.GeneratorType):
277 thing = stringify(thing)
277 thing = stringify(thing)
278 return thing
278 return thing
279
279
280 def evalboolean(context, mapping, arg):
280 def evalboolean(context, mapping, arg):
281 """Evaluate given argument as boolean, but also takes boolean literals"""
281 """Evaluate given argument as boolean, but also takes boolean literals"""
282 func, data = arg
282 func, data = arg
283 if func is runsymbol:
283 if func is runsymbol:
284 thing = func(context, mapping, data, default=None)
284 thing = func(context, mapping, data, default=None)
285 if thing is None:
285 if thing is None:
286 # not a template keyword, takes as a boolean literal
286 # not a template keyword, takes as a boolean literal
287 thing = stringutil.parsebool(data)
287 thing = stringutil.parsebool(data)
288 else:
288 else:
289 thing = func(context, mapping, data)
289 thing = func(context, mapping, data)
290 thing = unwrapvalue(thing)
290 thing = unwrapvalue(thing)
291 if isinstance(thing, bool):
291 if isinstance(thing, bool):
292 return thing
292 return thing
293 # other objects are evaluated as strings, which means 0 is True, but
293 # other objects are evaluated as strings, which means 0 is True, but
294 # empty dict/list should be False as they are expected to be ''
294 # empty dict/list should be False as they are expected to be ''
295 return bool(stringify(thing))
295 return bool(stringify(thing))
296
296
297 def evalinteger(context, mapping, arg, err=None):
297 def evalinteger(context, mapping, arg, err=None):
298 v = evalfuncarg(context, mapping, arg)
298 v = evalfuncarg(context, mapping, arg)
299 try:
299 try:
300 return int(v)
300 return int(v)
301 except (TypeError, ValueError):
301 except (TypeError, ValueError):
302 raise error.ParseError(err or _('not an integer'))
302 raise error.ParseError(err or _('not an integer'))
303
303
304 def evalstring(context, mapping, arg):
304 def evalstring(context, mapping, arg):
305 return stringify(evalrawexp(context, mapping, arg))
305 return stringify(evalrawexp(context, mapping, arg))
306
306
307 def evalstringliteral(context, mapping, arg):
307 def evalstringliteral(context, mapping, arg):
308 """Evaluate given argument as string template, but returns symbol name
308 """Evaluate given argument as string template, but returns symbol name
309 if it is unknown"""
309 if it is unknown"""
310 func, data = arg
310 func, data = arg
311 if func is runsymbol:
311 if func is runsymbol:
312 thing = func(context, mapping, data, default=data)
312 thing = func(context, mapping, data, default=data)
313 else:
313 else:
314 thing = func(context, mapping, data)
314 thing = func(context, mapping, data)
315 return stringify(thing)
315 return stringify(thing)
316
316
317 _evalfuncbytype = {
317 _evalfuncbytype = {
318 bool: evalboolean,
318 bool: evalboolean,
319 bytes: evalstring,
319 bytes: evalstring,
320 int: evalinteger,
320 int: evalinteger,
321 }
321 }
322
322
323 def evalastype(context, mapping, arg, typ):
323 def evalastype(context, mapping, arg, typ):
324 """Evaluate given argument and coerce its type"""
324 """Evaluate given argument and coerce its type"""
325 try:
325 try:
326 f = _evalfuncbytype[typ]
326 f = _evalfuncbytype[typ]
327 except KeyError:
327 except KeyError:
328 raise error.ProgrammingError('invalid type specified: %r' % typ)
328 raise error.ProgrammingError('invalid type specified: %r' % typ)
329 return f(context, mapping, arg)
329 return f(context, mapping, arg)
330
330
331 def runinteger(context, mapping, data):
331 def runinteger(context, mapping, data):
332 return int(data)
332 return int(data)
333
333
334 def runstring(context, mapping, data):
334 def runstring(context, mapping, data):
335 return data
335 return data
336
336
337 def _recursivesymbolblocker(key):
337 def _recursivesymbolblocker(key):
338 def showrecursion(**args):
338 def showrecursion(**args):
339 raise error.Abort(_("recursive reference '%s' in template") % key)
339 raise error.Abort(_("recursive reference '%s' in template") % key)
340 return showrecursion
340 return showrecursion
341
341
342 def runsymbol(context, mapping, key, default=''):
342 def runsymbol(context, mapping, key, default=''):
343 v = context.symbol(mapping, key)
343 v = context.symbol(mapping, key)
344 if v is None:
344 if v is None:
345 # put poison to cut recursion. we can't move this to parsing phase
345 # put poison to cut recursion. we can't move this to parsing phase
346 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
346 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
347 safemapping = mapping.copy()
347 safemapping = mapping.copy()
348 safemapping[key] = _recursivesymbolblocker(key)
348 safemapping[key] = _recursivesymbolblocker(key)
349 try:
349 try:
350 v = context.process(key, safemapping)
350 v = context.process(key, safemapping)
351 except TemplateNotFound:
351 except TemplateNotFound:
352 v = default
352 v = default
353 if callable(v) and getattr(v, '_requires', None) is None:
353 if callable(v) and getattr(v, '_requires', None) is None:
354 # old templatekw: expand all keywords and resources
354 # old templatekw: expand all keywords and resources
355 # (TODO: deprecate this after porting web template keywords to new API)
355 # (TODO: deprecate this after porting web template keywords to new API)
356 props = {k: context._resources.lookup(context, mapping, k)
356 props = {k: context._resources.lookup(context, mapping, k)
357 for k in context._resources.knownkeys()}
357 for k in context._resources.knownkeys()}
358 # pass context to _showcompatlist() through templatekw._showlist()
358 # pass context to _showcompatlist() through templatekw._showlist()
359 props['templ'] = context
359 props['templ'] = context
360 props.update(mapping)
360 props.update(mapping)
361 return v(**pycompat.strkwargs(props))
361 return v(**pycompat.strkwargs(props))
362 if callable(v):
362 if callable(v):
363 # new templatekw
363 # new templatekw
364 try:
364 try:
365 return v(context, mapping)
365 return v(context, mapping)
366 except ResourceUnavailable:
366 except ResourceUnavailable:
367 # unsupported keyword is mapped to empty just like unknown keyword
367 # unsupported keyword is mapped to empty just like unknown keyword
368 return None
368 return None
369 return v
369 return v
370
370
371 def runtemplate(context, mapping, template):
371 def runtemplate(context, mapping, template):
372 for arg in template:
372 for arg in template:
373 yield evalrawexp(context, mapping, arg)
373 yield evalrawexp(context, mapping, arg)
374
374
375 def runfilter(context, mapping, data):
375 def runfilter(context, mapping, data):
376 arg, filt = data
376 arg, filt = data
377 thing = evalfuncarg(context, mapping, arg)
377 thing = evalfuncarg(context, mapping, arg)
378 try:
378 try:
379 return filt(thing)
379 return filt(thing)
380 except (ValueError, AttributeError, TypeError):
380 except (ValueError, AttributeError, TypeError):
381 sym = findsymbolicname(arg)
381 sym = findsymbolicname(arg)
382 if sym:
382 if sym:
383 msg = (_("template filter '%s' is not compatible with keyword '%s'")
383 msg = (_("template filter '%s' is not compatible with keyword '%s'")
384 % (pycompat.sysbytes(filt.__name__), sym))
384 % (pycompat.sysbytes(filt.__name__), sym))
385 else:
385 else:
386 msg = (_("incompatible use of template filter '%s'")
386 msg = (_("incompatible use of template filter '%s'")
387 % pycompat.sysbytes(filt.__name__))
387 % pycompat.sysbytes(filt.__name__))
388 raise error.Abort(msg)
388 raise error.Abort(msg)
389
389
390 def runmap(context, mapping, data):
390 def runmap(context, mapping, data):
391 darg, targ = data
391 darg, targ = data
392 d = evalrawexp(context, mapping, darg)
392 d = evalrawexp(context, mapping, darg)
393 if util.safehasattr(d, 'itermaps'):
393 if util.safehasattr(d, 'itermaps'):
394 diter = d.itermaps()
394 diter = d.itermaps()
395 else:
395 else:
396 try:
396 try:
397 diter = iter(d)
397 diter = iter(d)
398 except TypeError:
398 except TypeError:
399 sym = findsymbolicname(darg)
399 sym = findsymbolicname(darg)
400 if sym:
400 if sym:
401 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
401 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
402 else:
402 else:
403 raise error.ParseError(_("%r is not iterable") % d)
403 raise error.ParseError(_("%r is not iterable") % d)
404
404
405 for i, v in enumerate(diter):
405 for i, v in enumerate(diter):
406 if isinstance(v, dict):
406 if isinstance(v, dict):
407 lm = context.overlaymap(mapping, v)
407 lm = context.overlaymap(mapping, v)
408 lm['index'] = i
408 lm['index'] = i
409 lm['originalnode'] = mapping.get('node')
410 yield evalrawexp(context, lm, targ)
409 yield evalrawexp(context, lm, targ)
411 else:
410 else:
412 # v is not an iterable of dicts, this happen when 'key'
411 # v is not an iterable of dicts, this happen when 'key'
413 # has been fully expanded already and format is useless.
412 # has been fully expanded already and format is useless.
414 # If so, return the expanded value.
413 # If so, return the expanded value.
415 yield v
414 yield v
416
415
417 def runmember(context, mapping, data):
416 def runmember(context, mapping, data):
418 darg, memb = data
417 darg, memb = data
419 d = evalrawexp(context, mapping, darg)
418 d = evalrawexp(context, mapping, darg)
420 if util.safehasattr(d, 'tomap'):
419 if util.safehasattr(d, 'tomap'):
421 lm = context.overlaymap(mapping, d.tomap())
420 lm = context.overlaymap(mapping, d.tomap())
422 return runsymbol(context, lm, memb)
421 return runsymbol(context, lm, memb)
423 if util.safehasattr(d, 'get'):
422 if util.safehasattr(d, 'get'):
424 return getdictitem(d, memb)
423 return getdictitem(d, memb)
425
424
426 sym = findsymbolicname(darg)
425 sym = findsymbolicname(darg)
427 if sym:
426 if sym:
428 raise error.ParseError(_("keyword '%s' has no member") % sym)
427 raise error.ParseError(_("keyword '%s' has no member") % sym)
429 else:
428 else:
430 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
429 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
431
430
432 def runnegate(context, mapping, data):
431 def runnegate(context, mapping, data):
433 data = evalinteger(context, mapping, data,
432 data = evalinteger(context, mapping, data,
434 _('negation needs an integer argument'))
433 _('negation needs an integer argument'))
435 return -data
434 return -data
436
435
437 def runarithmetic(context, mapping, data):
436 def runarithmetic(context, mapping, data):
438 func, left, right = data
437 func, left, right = data
439 left = evalinteger(context, mapping, left,
438 left = evalinteger(context, mapping, left,
440 _('arithmetic only defined on integers'))
439 _('arithmetic only defined on integers'))
441 right = evalinteger(context, mapping, right,
440 right = evalinteger(context, mapping, right,
442 _('arithmetic only defined on integers'))
441 _('arithmetic only defined on integers'))
443 try:
442 try:
444 return func(left, right)
443 return func(left, right)
445 except ZeroDivisionError:
444 except ZeroDivisionError:
446 raise error.Abort(_('division by zero is not defined'))
445 raise error.Abort(_('division by zero is not defined'))
447
446
448 def getdictitem(dictarg, key):
447 def getdictitem(dictarg, key):
449 val = dictarg.get(key)
448 val = dictarg.get(key)
450 if val is None:
449 if val is None:
451 return
450 return
452 return wraphybridvalue(dictarg, key, val)
451 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now