Patchwork [7,of,8] templatekw: add new-style template expansion to {manifest}

login
register
mail settings
Submitter Yuya Nishihara
Date Sept. 24, 2017, 12:21 p.m.
Message ID <a0047461f18c7d3d341b.1506255716@mimosa>
Download mbox | patch
Permalink /patch/24133/
State Accepted
Headers show

Comments

Yuya Nishihara - Sept. 24, 2017, 12:21 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1461490883 -32400
#      Sun Apr 24 18:41:23 2016 +0900
# Node ID a0047461f18c7d3d341bcecda6975bb58b53651b
# Parent  078f9566d9a9cdab1f4c9c116f12ce5cec5a392a
templatekw: add new-style template expansion to {manifest}

The goal is to allow us to easily access to nested data. The dot operator
will be introduced later so we can write '{p1.files}' instead of
'{revset("p1()") % "{files}"}' for example.

In the example above, 'p1' needs to carry a mapping dict along with its
string representation. If it were a list or a dict, it could be wrapped
semi-transparently with the _hybrid class, but for non-list/dict types,
it would be difficult to proxy all necessary functions to underlying value
type because several core operations may conflict with the ones of the
underlying value:

 - hash(value) should be different from hash(wrapped(value)), which means
   dict[wrapped(value)] would be invalid
 - 'value == wrapped(value)' would be false, breaks 'ifcontains'
 - len(wrapped(value)) may be either len(value) or len(iter(wrapped(value)))

So the wrapper has no proxy functions and its scope designed to be minimal.
It's unwrapped at eval*() functions so we don't have to care for a wrapped
object unless it's really needed:

  # most template functions just call evalfuncarg()
  unwrapped_value = evalfuncarg(context, mapping, args[n])
  # if wrapped value is needed, use evalrawexp()
  maybe_wrapped_value = evalrawexp(context, mapping, args[n])

Another idea was to wrap every template variable with a tagging class, but
which seemed uneasy without a static type checker.

This patch updates {manifest} to a mappable as an example.
Augie Fackler - Sept. 29, 2017, 10:47 a.m.
On Sun, Sep 24, 2017 at 09:21:56PM +0900, Yuya Nishihara wrote:
> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1461490883 -32400
> #      Sun Apr 24 18:41:23 2016 +0900
> # Node ID a0047461f18c7d3d341bcecda6975bb58b53651b
> # Parent  078f9566d9a9cdab1f4c9c116f12ce5cec5a392a
> templatekw: add new-style template expansion to {manifest}

queued, thanks

Patch

diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -70,6 +70,28 @@  class _hybrid(object):
             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}'}"
+
+    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, value, makemap):
+        self.gen = gen
+        self._value = value  # may be generator of strings
+        self._makemap = makemap
+
+    def tomap(self):
+        return self._makemap()
+
+    def itermaps(self):
+        yield self.tomap()
+
 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
     """Wrap data to support both dict-like and string-like operations"""
     return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
@@ -86,6 +108,12 @@  def unwraphybrid(thing):
         return thing
     return thing.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 showdict(name, data, mapping, plural=None, key='key', value='value',
              fmt='%s=%s', separator=' '):
     c = [{key: k, value: v} for k, v in data.iteritems()]
@@ -543,10 +571,14 @@  def showmanifest(**args):
     if mnode is None:
         # just avoid crash, we might want to use the 'ff...' hash in future
         return
+    mrev = repo.manifestlog._revlog.rev(mnode)
+    mhex = hex(mnode)
     args = args.copy()
-    args.update({r'rev': repo.manifestlog._revlog.rev(mnode),
-                 r'node': hex(mnode)})
-    return templ('manifest', **args)
+    args.update({r'rev': mrev, r'node': mhex})
+    f = templ('manifest', **args)
+    # TODO: perhaps 'ctx' should be dropped from mapping because manifest
+    # rev and node are completely different from changeset's.
+    return _mappable(f, f, lambda: {'rev': mrev, 'node': mhex})
 
 def shownames(namespace, **args):
     """helper method to generate a template keyword for a namespace"""
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -307,6 +307,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)
     # 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):
@@ -323,6 +324,7 @@  def evalboolean(context, mapping, arg):
             thing = util.parsebool(data)
     else:
         thing = func(context, mapping, data)
+    thing = templatekw.unwrapvalue(thing)
     if isinstance(thing, bool):
         return thing
     # other objects are evaluated as strings, which means 0 is True, but
@@ -768,6 +770,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)
     joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
     joiner = " "
     if len(args) > 1:
diff --git a/tests/test-command-template.t b/tests/test-command-template.t
--- a/tests/test-command-template.t
+++ b/tests/test-command-template.t
@@ -3119,6 +3119,20 @@  Test new-style inline templating:
   hg: parse error: None is not iterable
   [255]
 
+Test new-style inline templating of non-list/dict type:
+
+  $ hg log -R latesttag -r tip -T '{manifest}\n'
+  11:2bc6e9006ce2
+  $ hg log -R latesttag -r tip -T 'string length: {manifest|count}\n'
+  string length: 15
+  $ hg log -R latesttag -r tip -T '{manifest % "{rev}:{node}"}\n'
+  11:2bc6e9006ce29882383a22d39fd1f4e66dd3e2fc
+
+Test manifest can be join()-ed as before, though it's silly:
+
+  $ hg log -R latesttag -r tip -T '{join(manifest, "")}\n'
+  11:2bc6e9006ce2
+
 Test the sub function of templating for expansion:
 
   $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'