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