Patchwork [4,of,5] templater: move hybrid class and functions to templateutil module

login
register
mail settings
Submitter Yuya Nishihara
Date March 9, 2018, 12:45 p.m.
Message ID <c22e2d75938bc761554c.1520599533@mimosa>
Download mbox | patch
Permalink /patch/29172/
State Accepted
Headers show

Comments

Yuya Nishihara - March 9, 2018, 12:45 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1520518509 -32400
#      Thu Mar 08 23:15:09 2018 +0900
# Node ID c22e2d75938bc761554c8da98a49b85aa6bff0de
# Parent  dc412296cebd7c33cbae3b7ed5b0974186105387
templater: move hybrid class and functions to templateutil module

And make _hybrid and _mappable classes public. _showlist() is still marked
as private since it's weird and third-party codes shouldn't depend on it.

Patch

diff --git a/hgext/lfs/__init__.py b/hgext/lfs/__init__.py
--- a/hgext/lfs/__init__.py
+++ b/hgext/lfs/__init__.py
@@ -143,7 +143,7 @@  from mercurial import (
     registrar,
     revlog,
     scmutil,
-    templatekw,
+    templateutil,
     upgrade,
     util,
     vfs as vfsmod,
@@ -375,12 +375,12 @@  def lfsfiles(context, mapping):
     makemap = lambda v: {
         'file': v,
         'lfsoid': pointers[v].oid() if pointers[v] else None,
-        'lfspointer': templatekw.hybriddict(pointer(v)),
+        'lfspointer': templateutil.hybriddict(pointer(v)),
     }
 
     # TODO: make the separator ', '?
-    f = templatekw._showlist('lfs_file', files, templ, mapping)
-    return templatekw._hybrid(f, files, makemap, pycompat.identity)
+    f = templateutil._showlist('lfs_file', files, templ, mapping)
+    return templateutil.hybrid(f, files, makemap, pycompat.identity)
 
 @command('debuglfsupload',
          [('r', 'rev', [], _('upload large files introduced by REV'))])
diff --git a/hgext/remotenames.py b/hgext/remotenames.py
--- a/hgext/remotenames.py
+++ b/hgext/remotenames.py
@@ -35,7 +35,7 @@  from mercurial import (
     registrar,
     revsetlang,
     smartset,
-    templatekw,
+    templateutil,
 )
 
 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
@@ -237,8 +237,8 @@  def remotenameskw(context, mapping):
     if 'remotebranches' in repo.names:
         remotenames += repo.names['remotebranches'].names(repo, ctx.node())
 
-    return templatekw.compatlist(context, mapping, 'remotename', remotenames,
-                                 plural='remotenames')
+    return templateutil.compatlist(context, mapping, 'remotename', remotenames,
+                                   plural='remotenames')
 
 @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'})
 def remotebookmarkskw(context, mapping):
@@ -250,8 +250,8 @@  def remotebookmarkskw(context, mapping):
     if 'remotebookmarks' in repo.names:
         remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
 
-    return templatekw.compatlist(context, mapping, 'remotebookmark',
-                                 remotebmarks, plural='remotebookmarks')
+    return templateutil.compatlist(context, mapping, 'remotebookmark',
+                                   remotebmarks, plural='remotebookmarks')
 
 @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'})
 def remotebrancheskw(context, mapping):
@@ -263,8 +263,8 @@  def remotebrancheskw(context, mapping):
     if 'remotebranches' in repo.names:
         remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
 
-    return templatekw.compatlist(context, mapping, 'remotebranch',
-                                 remotebranches, plural='remotebranches')
+    return templateutil.compatlist(context, mapping, 'remotebranch',
+                                   remotebranches, plural='remotebranches')
 
 def _revsetutil(repo, subset, x, rtypes):
     """utility function to return a set of revs based on the rtypes"""
diff --git a/mercurial/formatter.py b/mercurial/formatter.py
--- a/mercurial/formatter.py
+++ b/mercurial/formatter.py
@@ -359,14 +359,15 @@  class _templateconverter(object):
         data = util.sortdict(_iteritems(data))
         def f():
             yield _plainconverter.formatdict(data, key, value, fmt, sep)
-        return templatekw.hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
+        return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
+                                       gen=f)
     @staticmethod
     def formatlist(data, name, fmt, sep):
         '''build object that can be evaluated as either plain string or list'''
         data = list(data)
         def f():
             yield _plainconverter.formatlist(data, name, fmt, sep)
-        return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f)
+        return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
 
 class templateformatter(baseformatter):
     def __init__(self, ui, out, topic, opts):
diff --git a/mercurial/templatefilters.py b/mercurial/templatefilters.py
--- a/mercurial/templatefilters.py
+++ b/mercurial/templatefilters.py
@@ -18,7 +18,6 @@  from . import (
     node,
     pycompat,
     registrar,
-    templatekw,
     templateutil,
     url,
     util,
@@ -365,7 +364,7 @@  def slashpath(path):
 @templatefilter('splitlines')
 def splitlines(text):
     """Any text. Split text into a list of lines."""
-    return templatekw.hybridlist(text.splitlines(), name='line')
+    return templateutil.hybridlist(text.splitlines(), name='line')
 
 @templatefilter('stringescape')
 def stringescape(text):
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -23,156 +23,24 @@  from . import (
     pycompat,
     registrar,
     scmutil,
+    templateutil,
     util,
 )
 
-class _hybrid(object):
-    """Wrapper for list or dict to support legacy template
-
-    This class allows us to handle both:
-    - "{files}" (legacy command-line-specific list hack) and
-    - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
-    and to access raw values:
-    - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
-    - "{get(extras, key)}"
-    - "{files|json}"
-    """
-
-    def __init__(self, gen, values, makemap, joinfmt, keytype=None):
-        if gen is not None:
-            self.gen = gen  # generator or function returning generator
-        self._values = values
-        self._makemap = makemap
-        self.joinfmt = joinfmt
-        self.keytype = keytype  # hint for 'x in y' where type(x) is unresolved
-    def gen(self):
-        """Default generator to stringify this as {join(self, ' ')}"""
-        for i, x in enumerate(self._values):
-            if i > 0:
-                yield ' '
-            yield self.joinfmt(x)
-    def itermaps(self):
-        makemap = self._makemap
-        for x in self._values:
-            yield makemap(x)
-    def __contains__(self, x):
-        return x in self._values
-    def __getitem__(self, key):
-        return self._values[key]
-    def __len__(self):
-        return len(self._values)
-    def __iter__(self):
-        return iter(self._values)
-    def __getattr__(self, name):
-        if name not in (r'get', r'items', r'iteritems', r'iterkeys',
-                        r'itervalues', r'keys', r'values'):
-            raise AttributeError(name)
-        return getattr(self._values, name)
-
-class _mappable(object):
-    """Wrapper for non-list/dict object to support map operation
-
-    This class allows us to handle both:
-    - "{manifest}"
-    - "{manifest % '{rev}:{node}'}"
-    - "{manifest.rev}"
-
-    Unlike a _hybrid, this does not simulate the behavior of the underling
-    value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
-    """
-
-    def __init__(self, gen, key, value, makemap):
-        if gen is not None:
-            self.gen = gen  # generator or function returning generator
-        self._key = key
-        self._value = value  # may be generator of strings
-        self._makemap = makemap
-
-    def gen(self):
-        yield pycompat.bytestr(self._value)
-
-    def tomap(self):
-        return self._makemap(self._key)
-
-    def itermaps(self):
-        yield self.tomap()
-
-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
-    if fmt is None:
-        fmt = '%s=%s'
-        prefmt = pycompat.bytestr
-    return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
-                   lambda k: fmt % (prefmt(k), prefmt(data[k])))
-
-def hybridlist(data, name, fmt=None, gen=None):
-    """Wrap data to support both list-like and string-like operations"""
-    prefmt = pycompat.identity
-    if fmt is None:
-        fmt = '%s'
-        prefmt = pycompat.bytestr
-    return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
-
-def unwraphybrid(thing):
-    """Return an object which can be stringified possibly by using a legacy
-    template"""
-    gen = getattr(thing, 'gen', None)
-    if gen is None:
-        return thing
-    if callable(gen):
-        return gen()
-    return gen
-
-def unwrapvalue(thing):
-    """Move the inner value object out of the wrapper"""
-    if not util.safehasattr(thing, '_value'):
-        return thing
-    return thing._value
-
-def wraphybridvalue(container, key, value):
-    """Wrap an element of hybrid container to be mappable
-
-    The key is passed to the makemap function of the given container, which
-    should be an item generated by iter(container).
-    """
-    makemap = getattr(container, '_makemap', None)
-    if makemap is None:
-        return value
-    if util.safehasattr(value, '_makemap'):
-        # a nested hybrid list/dict, which has its own way of map operation
-        return value
-    return _mappable(None, key, value, makemap)
-
-def compatdict(context, mapping, name, data, key='key', value='value',
-               fmt=None, plural=None, separator=' '):
-    """Wrap data like hybriddict(), but also supports old-style list template
-
-    This exists for backward compatibility with the old-style template. Use
-    hybriddict() for new template keywords.
-    """
-    c = [{key: k, value: v} for k, v in data.iteritems()]
-    t = context.resource(mapping, 'templ')
-    f = _showlist(name, c, t, mapping, plural, separator)
-    return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
-
-def compatlist(context, mapping, name, data, element=None, fmt=None,
-               plural=None, separator=' '):
-    """Wrap data like hybridlist(), but also supports old-style list template
-
-    This exists for backward compatibility with the old-style template. Use
-    hybridlist() for new template keywords.
-    """
-    t = context.resource(mapping, 'templ')
-    f = _showlist(name, data, t, mapping, plural, separator)
-    return hybridlist(data, name=element or name, fmt=fmt, gen=f)
+_hybrid = templateutil.hybrid
+_mappable = templateutil.mappable
+_showlist = templateutil._showlist
+hybriddict = templateutil.hybriddict
+hybridlist = templateutil.hybridlist
+compatdict = templateutil.compatdict
+compatlist = templateutil.compatlist
 
 def showdict(name, data, mapping, plural=None, key='key', value='value',
              fmt=None, separator=' '):
     ui = mapping.get('ui')
     if ui:
-        ui.deprecwarn("templatekw.showdict() is deprecated, use compatdict()",
-                      '4.6')
+        ui.deprecwarn("templatekw.showdict() is deprecated, use "
+                      "templateutil.compatdict()", '4.6')
     c = [{key: k, value: v} for k, v in data.iteritems()]
     f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
     return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
@@ -180,82 +48,13 @@  def showdict(name, data, mapping, plural
 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
     ui = mapping.get('ui')
     if ui:
-        ui.deprecwarn("templatekw.showlist() is deprecated, use compatlist()",
-                      '4.6')
+        ui.deprecwarn("templatekw.showlist() is deprecated, use "
+                      "templateutil.compatlist()", '4.6')
     if not element:
         element = name
     f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
     return hybridlist(values, name=element, gen=f)
 
-def _showlist(name, values, templ, mapping, plural=None, separator=' '):
-    '''expand set of values.
-    name is name of key in template map.
-    values is list of strings or dicts.
-    plural is plural of name, if not simply name + 's'.
-    separator is used to join values as a string
-
-    expansion works like this, given name 'foo'.
-
-    if values is empty, expand 'no_foos'.
-
-    if 'foo' not in template map, return values as a string,
-    joined by 'separator'.
-
-    expand 'start_foos'.
-
-    for each value, expand 'foo'. if 'last_foo' in template
-    map, expand it instead of 'foo' for last key.
-
-    expand 'end_foos'.
-    '''
-    strmapping = pycompat.strkwargs(mapping)
-    if not plural:
-        plural = name + 's'
-    if not values:
-        noname = 'no_' + plural
-        if noname in templ:
-            yield templ(noname, **strmapping)
-        return
-    if name not in templ:
-        if isinstance(values[0], bytes):
-            yield separator.join(values)
-        else:
-            for v in values:
-                r = dict(v)
-                r.update(mapping)
-                yield r
-        return
-    startname = 'start_' + plural
-    if startname in templ:
-        yield templ(startname, **strmapping)
-    vmapping = mapping.copy()
-    def one(v, tag=name):
-        try:
-            vmapping.update(v)
-        # Python 2 raises ValueError if the type of v is wrong. Python
-        # 3 raises TypeError.
-        except (AttributeError, TypeError, ValueError):
-            try:
-                # Python 2 raises ValueError trying to destructure an e.g.
-                # bytes. Python 3 raises TypeError.
-                for a, b in v:
-                    vmapping[a] = b
-            except (TypeError, ValueError):
-                vmapping[name] = v
-        return templ(tag, **pycompat.strkwargs(vmapping))
-    lastname = 'last_' + name
-    if lastname in templ:
-        last = values.pop()
-    else:
-        last = None
-    for v in values:
-        yield one(v)
-    if last is not None:
-        yield one(last, tag=lastname)
-    endname = 'end_' + plural
-    if endname in templ:
-        yield templ(endname, **strmapping)
-
 def getlatesttags(context, mapping, pattern=None):
     '''return date, distance and name for the latest tag of rev'''
     repo = context.resource(mapping, 'repo')
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -498,7 +498,7 @@  def dict_(context, mapping, args):
 
     data.update((k, evalfuncarg(context, mapping, v))
                 for k, v in args['kwargs'].iteritems())
-    return templatekw.hybriddict(data)
+    return templateutil.hybriddict(data)
 
 @templatefunc('diff([includepattern [, excludepattern]])')
 def diff(context, mapping, args):
@@ -548,7 +548,7 @@  def files(context, mapping, args):
     ctx = context.resource(mapping, 'ctx')
     m = ctx.match([raw])
     files = list(ctx.matches(m))
-    return templatekw.compatlist(context, mapping, "file", files)
+    return templateutil.compatlist(context, mapping, "file", files)
 
 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
 def fill(context, mapping, args):
@@ -718,7 +718,7 @@  def join(context, mapping, args):
     # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
     # abuses generator as a keyword that returns a list of dicts.
     joinset = evalrawexp(context, mapping, args[0])
-    joinset = templatekw.unwrapvalue(joinset)
+    joinset = templateutil.unwrapvalue(joinset)
     joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
     joiner = " "
     if len(args) > 1:
@@ -808,7 +808,7 @@  def max_(context, mapping, args, **kwarg
     except (TypeError, ValueError):
         # i18n: "max" is a keyword
         raise error.ParseError(_("max first argument should be an iterable"))
-    return templatekw.wraphybridvalue(iterable, x, x)
+    return templateutil.wraphybridvalue(iterable, x, x)
 
 @templatefunc('min(iterable)')
 def min_(context, mapping, args, **kwargs):
@@ -823,7 +823,7 @@  def min_(context, mapping, args, **kwarg
     except (TypeError, ValueError):
         # i18n: "min" is a keyword
         raise error.ParseError(_("min first argument should be an iterable"))
-    return templatekw.wraphybridvalue(iterable, x, x)
+    return templateutil.wraphybridvalue(iterable, x, x)
 
 @templatefunc('mod(a, b)')
 def mod(context, mapping, args):
@@ -847,7 +847,7 @@  def obsfateoperations(context, mapping, 
 
     try:
         data = obsutil.markersoperations(markers)
-        return templatekw.hybridlist(data, name='operation')
+        return templateutil.hybridlist(data, name='operation')
     except (TypeError, KeyError):
         # i18n: "obsfateoperations" is a keyword
         errmsg = _("obsfateoperations first argument should be an iterable")
@@ -864,7 +864,7 @@  def obsfatedate(context, mapping, args):
 
     try:
         data = obsutil.markersdates(markers)
-        return templatekw.hybridlist(data, name='date', fmt='%d %d')
+        return templateutil.hybridlist(data, name='date', fmt='%d %d')
     except (TypeError, KeyError):
         # i18n: "obsfatedate" is a keyword
         errmsg = _("obsfatedate first argument should be an iterable")
@@ -881,7 +881,7 @@  def obsfateusers(context, mapping, args)
 
     try:
         data = obsutil.markersusers(markers)
-        return templatekw.hybridlist(data, name='user')
+        return templateutil.hybridlist(data, name='user')
     except (TypeError, KeyError, ValueError):
         # i18n: "obsfateusers" is a keyword
         msg = _("obsfateusers first argument should be an iterable of "
@@ -1120,7 +1120,7 @@  def expandaliases(tree, aliases):
 
 def _flatten(thing):
     '''yield a single stream from a possibly nested set of iterators'''
-    thing = templatekw.unwraphybrid(thing)
+    thing = templateutil.unwraphybrid(thing)
     if isinstance(thing, bytes):
         yield thing
     elif isinstance(thing, str):
@@ -1134,7 +1134,7 @@  def _flatten(thing):
         yield pycompat.bytestr(thing)
     else:
         for i in thing:
-            i = templatekw.unwraphybrid(i)
+            i = templateutil.unwraphybrid(i)
             if isinstance(i, bytes):
                 yield i
             elif i is None:
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -13,7 +13,6 @@  from .i18n import _
 from . import (
     error,
     pycompat,
-    templatekw,
     util,
 )
 
@@ -23,9 +22,219 @@  class ResourceUnavailable(error.Abort):
 class TemplateNotFound(error.Abort):
     pass
 
+class hybrid(object):
+    """Wrapper for list or dict to support legacy template
+
+    This class allows us to handle both:
+    - "{files}" (legacy command-line-specific list hack) and
+    - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
+    and to access raw values:
+    - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
+    - "{get(extras, key)}"
+    - "{files|json}"
+    """
+
+    def __init__(self, gen, values, makemap, joinfmt, keytype=None):
+        if gen is not None:
+            self.gen = gen  # generator or function returning generator
+        self._values = values
+        self._makemap = makemap
+        self.joinfmt = joinfmt
+        self.keytype = keytype  # hint for 'x in y' where type(x) is unresolved
+    def gen(self):
+        """Default generator to stringify this as {join(self, ' ')}"""
+        for i, x in enumerate(self._values):
+            if i > 0:
+                yield ' '
+            yield self.joinfmt(x)
+    def itermaps(self):
+        makemap = self._makemap
+        for x in self._values:
+            yield makemap(x)
+    def __contains__(self, x):
+        return x in self._values
+    def __getitem__(self, key):
+        return self._values[key]
+    def __len__(self):
+        return len(self._values)
+    def __iter__(self):
+        return iter(self._values)
+    def __getattr__(self, name):
+        if name not in (r'get', r'items', r'iteritems', r'iterkeys',
+                        r'itervalues', r'keys', r'values'):
+            raise AttributeError(name)
+        return getattr(self._values, name)
+
+class mappable(object):
+    """Wrapper for non-list/dict object to support map operation
+
+    This class allows us to handle both:
+    - "{manifest}"
+    - "{manifest % '{rev}:{node}'}"
+    - "{manifest.rev}"
+
+    Unlike a hybrid, this does not simulate the behavior of the underling
+    value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
+    """
+
+    def __init__(self, gen, key, value, makemap):
+        if gen is not None:
+            self.gen = gen  # generator or function returning generator
+        self._key = key
+        self._value = value  # may be generator of strings
+        self._makemap = makemap
+
+    def gen(self):
+        yield pycompat.bytestr(self._value)
+
+    def tomap(self):
+        return self._makemap(self._key)
+
+    def itermaps(self):
+        yield self.tomap()
+
+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
+    if fmt is None:
+        fmt = '%s=%s'
+        prefmt = pycompat.bytestr
+    return hybrid(gen, data, lambda k: {key: k, value: data[k]},
+                  lambda k: fmt % (prefmt(k), prefmt(data[k])))
+
+def hybridlist(data, name, fmt=None, gen=None):
+    """Wrap data to support both list-like and string-like operations"""
+    prefmt = pycompat.identity
+    if fmt is None:
+        fmt = '%s'
+        prefmt = pycompat.bytestr
+    return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
+
+def unwraphybrid(thing):
+    """Return an object which can be stringified possibly by using a legacy
+    template"""
+    gen = getattr(thing, 'gen', None)
+    if gen is None:
+        return thing
+    if callable(gen):
+        return gen()
+    return gen
+
+def unwrapvalue(thing):
+    """Move the inner value object out of the wrapper"""
+    if not util.safehasattr(thing, '_value'):
+        return thing
+    return thing._value
+
+def wraphybridvalue(container, key, value):
+    """Wrap an element of hybrid container to be mappable
+
+    The key is passed to the makemap function of the given container, which
+    should be an item generated by iter(container).
+    """
+    makemap = getattr(container, '_makemap', None)
+    if makemap is None:
+        return value
+    if util.safehasattr(value, '_makemap'):
+        # a nested hybrid list/dict, which has its own way of map operation
+        return value
+    return mappable(None, key, value, makemap)
+
+def compatdict(context, mapping, name, data, key='key', value='value',
+               fmt=None, plural=None, separator=' '):
+    """Wrap data like hybriddict(), but also supports old-style list template
+
+    This exists for backward compatibility with the old-style template. Use
+    hybriddict() for new template keywords.
+    """
+    c = [{key: k, value: v} for k, v in data.iteritems()]
+    t = context.resource(mapping, 'templ')
+    f = _showlist(name, c, t, mapping, plural, separator)
+    return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
+
+def compatlist(context, mapping, name, data, element=None, fmt=None,
+               plural=None, separator=' '):
+    """Wrap data like hybridlist(), but also supports old-style list template
+
+    This exists for backward compatibility with the old-style template. Use
+    hybridlist() for new template keywords.
+    """
+    t = context.resource(mapping, 'templ')
+    f = _showlist(name, data, t, mapping, plural, separator)
+    return hybridlist(data, name=element or name, fmt=fmt, gen=f)
+
+def _showlist(name, values, templ, mapping, plural=None, separator=' '):
+    '''expand set of values.
+    name is name of key in template map.
+    values is list of strings or dicts.
+    plural is plural of name, if not simply name + 's'.
+    separator is used to join values as a string
+
+    expansion works like this, given name 'foo'.
+
+    if values is empty, expand 'no_foos'.
+
+    if 'foo' not in template map, return values as a string,
+    joined by 'separator'.
+
+    expand 'start_foos'.
+
+    for each value, expand 'foo'. if 'last_foo' in template
+    map, expand it instead of 'foo' for last key.
+
+    expand 'end_foos'.
+    '''
+    strmapping = pycompat.strkwargs(mapping)
+    if not plural:
+        plural = name + 's'
+    if not values:
+        noname = 'no_' + plural
+        if noname in templ:
+            yield templ(noname, **strmapping)
+        return
+    if name not in templ:
+        if isinstance(values[0], bytes):
+            yield separator.join(values)
+        else:
+            for v in values:
+                r = dict(v)
+                r.update(mapping)
+                yield r
+        return
+    startname = 'start_' + plural
+    if startname in templ:
+        yield templ(startname, **strmapping)
+    vmapping = mapping.copy()
+    def one(v, tag=name):
+        try:
+            vmapping.update(v)
+        # Python 2 raises ValueError if the type of v is wrong. Python
+        # 3 raises TypeError.
+        except (AttributeError, TypeError, ValueError):
+            try:
+                # Python 2 raises ValueError trying to destructure an e.g.
+                # bytes. Python 3 raises TypeError.
+                for a, b in v:
+                    vmapping[a] = b
+            except (TypeError, ValueError):
+                vmapping[name] = v
+        return templ(tag, **pycompat.strkwargs(vmapping))
+    lastname = 'last_' + name
+    if lastname in templ:
+        last = values.pop()
+    else:
+        last = None
+    for v in values:
+        yield one(v)
+    if last is not None:
+        yield one(last, tag=lastname)
+    endname = 'end_' + plural
+    if endname in templ:
+        yield templ(endname, **strmapping)
+
 def stringify(thing):
     """Turn values into bytes by converting into text and concatenating them"""
-    thing = templatekw.unwraphybrid(thing)
+    thing = unwraphybrid(thing)
     if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
         if isinstance(thing, str):
             # This is only reachable on Python 3 (otherwise
@@ -59,7 +268,7 @@  def evalrawexp(context, mapping, arg):
 def evalfuncarg(context, mapping, arg):
     """Evaluate given argument as value type"""
     thing = evalrawexp(context, mapping, arg)
-    thing = templatekw.unwrapvalue(thing)
+    thing = unwrapvalue(thing)
     # evalrawexp() may return string, generator of strings or arbitrary object
     # such as date tuple, but filter does not want generator.
     if isinstance(thing, types.GeneratorType):
@@ -76,7 +285,7 @@  def evalboolean(context, mapping, arg):
             thing = util.parsebool(data)
     else:
         thing = func(context, mapping, data)
-    thing = templatekw.unwrapvalue(thing)
+    thing = unwrapvalue(thing)
     if isinstance(thing, bool):
         return thing
     # other objects are evaluated as strings, which means 0 is True, but
@@ -236,4 +445,4 @@  def getdictitem(dictarg, key):
     val = dictarg.get(key)
     if val is None:
         return
-    return templatekw.wraphybridvalue(dictarg, key, val)
+    return wraphybridvalue(dictarg, key, val)