##// END OF EJS Templates
formatter: open raw template file in posix semantics...
Yuya Nishihara -
r32829:470820c2 default
parent child Browse files
Show More
@@ -1,466 +1,466 b''
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.fout = sys.stdout # redirect to doctest
49 49 ... ui.verbose = verbose
50 50 ... return fn(ui, ui.formatter(fn.__name__, opts))
51 51
52 52 Basic example:
53 53
54 54 >>> def files(ui, fm):
55 55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
56 56 ... for f in files:
57 57 ... fm.startitem()
58 58 ... fm.write('path', '%s', f[0])
59 59 ... fm.condwrite(ui.verbose, 'date', ' %s',
60 60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
61 61 ... fm.data(size=f[1])
62 62 ... fm.plain('\\n')
63 63 ... fm.end()
64 64 >>> show(files)
65 65 foo
66 66 bar
67 67 >>> show(files, verbose=True)
68 68 foo 1970-01-01 00:00:00
69 69 bar 1970-01-01 00:00:01
70 70 >>> show(files, template='json')
71 71 [
72 72 {
73 73 "date": [0, 0],
74 74 "path": "foo",
75 75 "size": 123
76 76 },
77 77 {
78 78 "date": [1, 0],
79 79 "path": "bar",
80 80 "size": 456
81 81 }
82 82 ]
83 83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
84 84 path: foo
85 85 date: 1970-01-01T00:00:00+00:00
86 86 path: bar
87 87 date: 1970-01-01T00:00:01+00:00
88 88
89 89 Nested example:
90 90
91 91 >>> def subrepos(ui, fm):
92 92 ... fm.startitem()
93 93 ... fm.write('repo', '[%s]\\n', 'baz')
94 94 ... files(ui, fm.nested('files'))
95 95 ... fm.end()
96 96 >>> show(subrepos)
97 97 [baz]
98 98 foo
99 99 bar
100 100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
101 101 baz: foo, bar
102 102 """
103 103
104 104 from __future__ import absolute_import
105 105
106 106 import contextlib
107 107 import itertools
108 108 import os
109 109
110 110 from .i18n import _
111 111 from .node import (
112 112 hex,
113 113 short,
114 114 )
115 115
116 116 from . import (
117 117 error,
118 118 pycompat,
119 119 templatefilters,
120 120 templatekw,
121 121 templater,
122 122 util,
123 123 )
124 124
125 125 pickle = util.pickle
126 126
127 127 class _nullconverter(object):
128 128 '''convert non-primitive data types to be processed by formatter'''
129 129 @staticmethod
130 130 def formatdate(date, fmt):
131 131 '''convert date tuple to appropriate format'''
132 132 return date
133 133 @staticmethod
134 134 def formatdict(data, key, value, fmt, sep):
135 135 '''convert dict or key-value pairs to appropriate dict format'''
136 136 # use plain dict instead of util.sortdict so that data can be
137 137 # serialized as a builtin dict in pickle output
138 138 return dict(data)
139 139 @staticmethod
140 140 def formatlist(data, name, fmt, sep):
141 141 '''convert iterable to appropriate list format'''
142 142 return list(data)
143 143
144 144 class baseformatter(object):
145 145 def __init__(self, ui, topic, opts, converter):
146 146 self._ui = ui
147 147 self._topic = topic
148 148 self._style = opts.get("style")
149 149 self._template = opts.get("template")
150 150 self._converter = converter
151 151 self._item = None
152 152 # function to convert node to string suitable for this output
153 153 self.hexfunc = hex
154 154 def __enter__(self):
155 155 return self
156 156 def __exit__(self, exctype, excvalue, traceback):
157 157 if exctype is None:
158 158 self.end()
159 159 def _showitem(self):
160 160 '''show a formatted item once all data is collected'''
161 161 pass
162 162 def startitem(self):
163 163 '''begin an item in the format list'''
164 164 if self._item is not None:
165 165 self._showitem()
166 166 self._item = {}
167 167 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
168 168 '''convert date tuple to appropriate format'''
169 169 return self._converter.formatdate(date, fmt)
170 170 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
171 171 '''convert dict or key-value pairs to appropriate dict format'''
172 172 return self._converter.formatdict(data, key, value, fmt, sep)
173 173 def formatlist(self, data, name, fmt='%s', sep=' '):
174 174 '''convert iterable to appropriate list format'''
175 175 # name is mandatory argument for now, but it could be optional if
176 176 # we have default template keyword, e.g. {item}
177 177 return self._converter.formatlist(data, name, fmt, sep)
178 178 def context(self, **ctxs):
179 179 '''insert context objects to be used to render template keywords'''
180 180 pass
181 181 def data(self, **data):
182 182 '''insert data into item that's not shown in default output'''
183 183 data = pycompat.byteskwargs(data)
184 184 self._item.update(data)
185 185 def write(self, fields, deftext, *fielddata, **opts):
186 186 '''do default text output while assigning data to item'''
187 187 fieldkeys = fields.split()
188 188 assert len(fieldkeys) == len(fielddata)
189 189 self._item.update(zip(fieldkeys, fielddata))
190 190 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
191 191 '''do conditional write (primarily for plain formatter)'''
192 192 fieldkeys = fields.split()
193 193 assert len(fieldkeys) == len(fielddata)
194 194 self._item.update(zip(fieldkeys, fielddata))
195 195 def plain(self, text, **opts):
196 196 '''show raw text for non-templated mode'''
197 197 pass
198 198 def isplain(self):
199 199 '''check for plain formatter usage'''
200 200 return False
201 201 def nested(self, field):
202 202 '''sub formatter to store nested data in the specified field'''
203 203 self._item[field] = data = []
204 204 return _nestedformatter(self._ui, self._converter, data)
205 205 def end(self):
206 206 '''end output for the formatter'''
207 207 if self._item is not None:
208 208 self._showitem()
209 209
210 210 def nullformatter(ui, topic):
211 211 '''formatter that prints nothing'''
212 212 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
213 213
214 214 class _nestedformatter(baseformatter):
215 215 '''build sub items and store them in the parent formatter'''
216 216 def __init__(self, ui, converter, data):
217 217 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
218 218 self._data = data
219 219 def _showitem(self):
220 220 self._data.append(self._item)
221 221
222 222 def _iteritems(data):
223 223 '''iterate key-value pairs in stable order'''
224 224 if isinstance(data, dict):
225 225 return sorted(data.iteritems())
226 226 return data
227 227
228 228 class _plainconverter(object):
229 229 '''convert non-primitive data types to text'''
230 230 @staticmethod
231 231 def formatdate(date, fmt):
232 232 '''stringify date tuple in the given format'''
233 233 return util.datestr(date, fmt)
234 234 @staticmethod
235 235 def formatdict(data, key, value, fmt, sep):
236 236 '''stringify key-value pairs separated by sep'''
237 237 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
238 238 @staticmethod
239 239 def formatlist(data, name, fmt, sep):
240 240 '''stringify iterable separated by sep'''
241 241 return sep.join(fmt % e for e in data)
242 242
243 243 class plainformatter(baseformatter):
244 244 '''the default text output scheme'''
245 245 def __init__(self, ui, out, topic, opts):
246 246 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
247 247 if ui.debugflag:
248 248 self.hexfunc = hex
249 249 else:
250 250 self.hexfunc = short
251 251 if ui is out:
252 252 self._write = ui.write
253 253 else:
254 254 self._write = lambda s, **opts: out.write(s)
255 255 def startitem(self):
256 256 pass
257 257 def data(self, **data):
258 258 pass
259 259 def write(self, fields, deftext, *fielddata, **opts):
260 260 self._write(deftext % fielddata, **opts)
261 261 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
262 262 '''do conditional write'''
263 263 if cond:
264 264 self._write(deftext % fielddata, **opts)
265 265 def plain(self, text, **opts):
266 266 self._write(text, **opts)
267 267 def isplain(self):
268 268 return True
269 269 def nested(self, field):
270 270 # nested data will be directly written to ui
271 271 return self
272 272 def end(self):
273 273 pass
274 274
275 275 class debugformatter(baseformatter):
276 276 def __init__(self, ui, out, topic, opts):
277 277 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
278 278 self._out = out
279 279 self._out.write("%s = [\n" % self._topic)
280 280 def _showitem(self):
281 281 self._out.write(" " + repr(self._item) + ",\n")
282 282 def end(self):
283 283 baseformatter.end(self)
284 284 self._out.write("]\n")
285 285
286 286 class pickleformatter(baseformatter):
287 287 def __init__(self, ui, out, topic, opts):
288 288 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
289 289 self._out = out
290 290 self._data = []
291 291 def _showitem(self):
292 292 self._data.append(self._item)
293 293 def end(self):
294 294 baseformatter.end(self)
295 295 self._out.write(pickle.dumps(self._data))
296 296
297 297 class jsonformatter(baseformatter):
298 298 def __init__(self, ui, out, topic, opts):
299 299 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
300 300 self._out = out
301 301 self._out.write("[")
302 302 self._first = True
303 303 def _showitem(self):
304 304 if self._first:
305 305 self._first = False
306 306 else:
307 307 self._out.write(",")
308 308
309 309 self._out.write("\n {\n")
310 310 first = True
311 311 for k, v in sorted(self._item.items()):
312 312 if first:
313 313 first = False
314 314 else:
315 315 self._out.write(",\n")
316 316 u = templatefilters.json(v, paranoid=False)
317 317 self._out.write(' "%s": %s' % (k, u))
318 318 self._out.write("\n }")
319 319 def end(self):
320 320 baseformatter.end(self)
321 321 self._out.write("\n]\n")
322 322
323 323 class _templateconverter(object):
324 324 '''convert non-primitive data types to be processed by templater'''
325 325 @staticmethod
326 326 def formatdate(date, fmt):
327 327 '''return date tuple'''
328 328 return date
329 329 @staticmethod
330 330 def formatdict(data, key, value, fmt, sep):
331 331 '''build object that can be evaluated as either plain string or dict'''
332 332 data = util.sortdict(_iteritems(data))
333 333 def f():
334 334 yield _plainconverter.formatdict(data, key, value, fmt, sep)
335 335 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt,
336 336 gen=f())
337 337 @staticmethod
338 338 def formatlist(data, name, fmt, sep):
339 339 '''build object that can be evaluated as either plain string or list'''
340 340 data = list(data)
341 341 def f():
342 342 yield _plainconverter.formatlist(data, name, fmt, sep)
343 343 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f())
344 344
345 345 class templateformatter(baseformatter):
346 346 def __init__(self, ui, out, topic, opts):
347 347 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
348 348 self._out = out
349 349 self._topic = topic
350 350 self._t = gettemplater(ui, topic, opts.get('template', ''),
351 351 cache=templatekw.defaulttempl)
352 352 self._counter = itertools.count()
353 353 self._cache = {} # for templatekw/funcs to store reusable data
354 354 def context(self, **ctxs):
355 355 '''insert context objects to be used to render template keywords'''
356 356 assert all(k == 'ctx' for k in ctxs)
357 357 self._item.update(ctxs)
358 358 def _showitem(self):
359 359 # TODO: add support for filectx. probably each template keyword or
360 360 # function will have to declare dependent resources. e.g.
361 361 # @templatekeyword(..., requires=('ctx',))
362 362 props = {}
363 363 if 'ctx' in self._item:
364 364 props.update(templatekw.keywords)
365 365 props['index'] = next(self._counter)
366 366 # explicitly-defined fields precede templatekw
367 367 props.update(self._item)
368 368 if 'ctx' in self._item:
369 369 # but template resources must be always available
370 370 props['templ'] = self._t
371 371 props['repo'] = props['ctx'].repo()
372 372 props['revcache'] = {}
373 373 g = self._t(self._topic, ui=self._ui, cache=self._cache, **props)
374 374 self._out.write(templater.stringify(g))
375 375
376 376 def lookuptemplate(ui, topic, tmpl):
377 377 # looks like a literal template?
378 378 if '{' in tmpl:
379 379 return tmpl, None
380 380
381 381 # perhaps a stock style?
382 382 if not os.path.split(tmpl)[0]:
383 383 mapname = (templater.templatepath('map-cmdline.' + tmpl)
384 384 or templater.templatepath(tmpl))
385 385 if mapname and os.path.isfile(mapname):
386 386 return None, mapname
387 387
388 388 # perhaps it's a reference to [templates]
389 389 t = ui.config('templates', tmpl)
390 390 if t:
391 391 return templater.unquotestring(t), None
392 392
393 393 if tmpl == 'list':
394 394 ui.write(_("available styles: %s\n") % templater.stylelist())
395 395 raise error.Abort(_("specify a template"))
396 396
397 397 # perhaps it's a path to a map or a template
398 398 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
399 399 # is it a mapfile for a style?
400 400 if os.path.basename(tmpl).startswith("map-"):
401 401 return None, os.path.realpath(tmpl)
402 with open(tmpl, 'rb') as f:
402 with util.posixfile(tmpl, 'rb') as f:
403 403 tmpl = f.read()
404 404 return tmpl, None
405 405
406 406 # constant string?
407 407 return tmpl, None
408 408
409 409 def gettemplater(ui, topic, spec, cache=None):
410 410 tmpl, mapfile = lookuptemplate(ui, topic, spec)
411 411 assert not (tmpl and mapfile)
412 412 if mapfile:
413 413 return templater.templater.frommapfile(mapfile, cache=cache)
414 414 return maketemplater(ui, topic, tmpl, cache=cache)
415 415
416 416 def maketemplater(ui, topic, tmpl, cache=None):
417 417 """Create a templater from a string template 'tmpl'"""
418 418 aliases = ui.configitems('templatealias')
419 419 t = templater.templater(cache=cache, aliases=aliases)
420 420 if tmpl:
421 421 t.cache[topic] = tmpl
422 422 return t
423 423
424 424 def formatter(ui, out, topic, opts):
425 425 template = opts.get("template", "")
426 426 if template == "json":
427 427 return jsonformatter(ui, out, topic, opts)
428 428 elif template == "pickle":
429 429 return pickleformatter(ui, out, topic, opts)
430 430 elif template == "debug":
431 431 return debugformatter(ui, out, topic, opts)
432 432 elif template != "":
433 433 return templateformatter(ui, out, topic, opts)
434 434 # developer config: ui.formatdebug
435 435 elif ui.configbool('ui', 'formatdebug'):
436 436 return debugformatter(ui, out, topic, opts)
437 437 # deprecated config: ui.formatjson
438 438 elif ui.configbool('ui', 'formatjson'):
439 439 return jsonformatter(ui, out, topic, opts)
440 440 return plainformatter(ui, out, topic, opts)
441 441
442 442 @contextlib.contextmanager
443 443 def openformatter(ui, filename, topic, opts):
444 444 """Create a formatter that writes outputs to the specified file
445 445
446 446 Must be invoked using the 'with' statement.
447 447 """
448 448 with util.posixfile(filename, 'wb') as out:
449 449 with formatter(ui, out, topic, opts) as fm:
450 450 yield fm
451 451
452 452 @contextlib.contextmanager
453 453 def _neverending(fm):
454 454 yield fm
455 455
456 456 def maybereopen(fm, filename, opts):
457 457 """Create a formatter backed by file if filename specified, else return
458 458 the given formatter
459 459
460 460 Must be invoked using the 'with' statement. This will never call fm.end()
461 461 of the given formatter.
462 462 """
463 463 if filename:
464 464 return openformatter(fm._ui, filename, fm._topic, opts)
465 465 else:
466 466 return _neverending(fm)
General Comments 0
You need to be logged in to leave comments. Login now