templater: wrap result of '%' operation so it never looks like a thunk
This fixes min/max()/json() of map result. Before, it was taken as a lazy
byte string and stringified by evalfuncarg().
--- a/mercurial/templater.py Thu Jan 18 12:54:01 2018 +0100
+++ b/mercurial/templater.py Sun Mar 18 23:36:52 2018 +0900
@@ -48,6 +48,10 @@
mappinggenerator, mappinglist
represents mappings (i.e. a list of dicts), which may have default
output format.
+
+mappedgenerator
+ a lazily-evaluated list of byte strings, which is e.g. a result of %
+ operation.
"""
from __future__ import absolute_import, print_function
--- a/mercurial/templateutil.py Thu Jan 18 12:54:01 2018 +0100
+++ b/mercurial/templateutil.py Sun Mar 18 23:36:52 2018 +0900
@@ -227,6 +227,33 @@
def itermaps(self, context):
return iter(self._mappings)
+class mappedgenerator(wrapped):
+ """Wrapper for generator of strings which acts as a list
+
+ The function ``make(context, *args)`` should return a generator of
+ byte strings, or a generator of (possibly nested) generators of byte
+ strings (i.e. a generator for a list of byte strings.)
+ """
+
+ def __init__(self, make, args=()):
+ self._make = make
+ self._args = args
+
+ def _gen(self, context):
+ return self._make(context, *self._args)
+
+ def itermaps(self, context):
+ raise error.ParseError(_('list of strings is not mappable'))
+
+ def join(self, context, mapping, sep):
+ return joinitems(self._gen(context), sep)
+
+ def show(self, context, mapping):
+ return self.join(context, mapping, '')
+
+ def tovalue(self, context, mapping):
+ return [stringify(context, mapping, x) for x in self._gen(context)]
+
def hybriddict(data, key='key', value='value', fmt=None, gen=None):
"""Wrap data to support both dict-like and string-like operations"""
prefmt = pycompat.identity
@@ -589,18 +616,23 @@
lm['index'] = i
yield lm
+def _applymap(context, mapping, diter, targ):
+ for lm in _iteroverlaymaps(context, mapping, diter):
+ yield evalrawexp(context, lm, targ)
+
def runmap(context, mapping, data):
darg, targ = data
d = evalrawexp(context, mapping, darg)
# TODO: a generator should be rejected because it is a thunk of lazy
# string, but we can't because hgweb abuses generator as a keyword
# that returns a list of dicts.
+ # TODO: drop _checkeditermaps() and pass 'd' to mappedgenerator so it
+ # can be restarted.
if isinstance(d, wrapped):
diter = d.itermaps(context)
else:
diter = _checkeditermaps(darg, d)
- for lm in _iteroverlaymaps(context, mapping, diter):
- yield evalrawexp(context, lm, targ)
+ return mappedgenerator(_applymap, args=(mapping, diter, targ))
def runmember(context, mapping, data):
darg, memb = data
--- a/tests/test-command-template.t Thu Jan 18 12:54:01 2018 +0100
+++ b/tests/test-command-template.t Sun Mar 18 23:36:52 2018 +0900
@@ -3217,7 +3217,7 @@
hg: parse error: None is not iterable of mappings
[255]
$ hg log -R latesttag -r tip -T '{extras % "{key}\n" % "{key}\n"}'
- hg: parse error: <generator *> is not iterable of mappings (glob)
+ hg: parse error: list of strings is not mappable
[255]
Test new-style inline templating of non-list/dict type:
@@ -3255,6 +3255,13 @@
$ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
10
+Test min/max over map operation:
+
+ $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
+ at3
+ $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
+ t3
+
Test min/max of if() result
$ cd latesttag
@@ -3842,6 +3849,11 @@
$ hg log -r0 -T '{extras|json}\n'
{"branch": "default"}
+Test json filter applied to map result:
+
+ $ hg log -r0 -T '{json(extras % "{key}")}\n'
+ ["branch"]
+
Test localdate(date, tz) function:
$ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'