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