Patchwork [4,of,5,V2] templater: add support for keyword arguments

login
register
mail settings
Submitter Yuya Nishihara
Date April 8, 2017, 1:06 p.m.
Message ID <05be4b2679b557c9fc87.1491656799@mimosa>
Download mbox | patch
Permalink /patch/20024/
State Accepted
Headers show

Comments

Yuya Nishihara - April 8, 2017, 1:06 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1491222159 -32400
#      Mon Apr 03 21:22:39 2017 +0900
# Node ID 05be4b2679b557c9fc8711d9b258e3667cec0b88
# Parent  0a547405070213e075162d45f37a2d82877c61c3
templater: add support for keyword arguments

Unlike revset, function arguments are pre-processed in templater. That's why
we need to define argspec per function. An argspec field looks somewhat
redundant in @templatefunc definition as a name field contains human-readable
list of arguments. I'll make function doc be built from argspec later.

Ported separate() function as an example.

Patch

diff --git a/mercurial/registrar.py b/mercurial/registrar.py
--- a/mercurial/registrar.py
+++ b/mercurial/registrar.py
@@ -234,7 +234,7 @@  class templatefunc(_templateregistrarbas
 
         templatefunc = registrar.templatefunc()
 
-        @templatefunc('myfunc(arg1, arg2[, arg3])')
+        @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3')
         def myfuncfunc(context, mapping, args):
             '''Explanation of this template function ....
             '''
@@ -242,6 +242,10 @@  class templatefunc(_templateregistrarbas
 
     The first string argument is used also in online help.
 
+    If optional 'argspec' is defined, the function will receive 'args' as
+    a dict of named arguments. Otherwise 'args' is a list of positional
+    arguments.
+
     'templatefunc' instance in example above can be used to
     decorate multiple functions.
 
@@ -252,3 +256,6 @@  class templatefunc(_templateregistrarbas
     Otherwise, explicit 'templater.loadfunction()' is needed.
     """
     _getname = _funcregistrarbase._parsefuncdecl
+
+    def _extrasetup(self, name, func, argspec=None):
+        func._argspec = argspec
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -370,14 +370,15 @@  def runtemplate(context, mapping, templa
         yield func(context, mapping, data)
 
 def buildfilter(exp, context):
-    arg = compileexp(exp[1], context, methods)
     n = getsymbol(exp[2])
     if n in context._filters:
         filt = context._filters[n]
+        arg = compileexp(exp[1], context, methods)
         return (runfilter, (arg, filt))
     if n in funcs:
         f = funcs[n]
-        return (f, [arg])
+        args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
+        return (f, args)
     raise error.ParseError(_("unknown function '%s'") % n)
 
 def runfilter(context, mapping, data):
@@ -452,17 +453,41 @@  def runarithmetic(context, mapping, data
 
 def buildfunc(exp, context):
     n = getsymbol(exp[1])
-    args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
     if n in funcs:
         f = funcs[n]
+        args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
         return (f, args)
     if n in context._filters:
+        args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
         if len(args) != 1:
             raise error.ParseError(_("filter %s expects one argument") % n)
         f = context._filters[n]
         return (runfilter, (args[0], f))
     raise error.ParseError(_("unknown function '%s'") % n)
 
+def _buildfuncargs(exp, context, curmethods, funcname, argspec):
+    """Compile parsed tree of function arguments into list or dict of
+    (func, data) pairs"""
+    def compiledict(xs):
+        return dict((k, compileexp(x, context, curmethods))
+                    for k, x in xs.iteritems())
+    def compilelist(xs):
+        return [compileexp(x, context, curmethods) for x in xs]
+
+    if not argspec:
+        # filter or function with no argspec: return list of positional args
+        return compilelist(getlist(exp))
+
+    # function with argspec: return dict of named args
+    _poskeys, varkey, _keys = argspec = parser.splitargspec(argspec)
+    treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
+                                    keyvaluenode='keyvalue', keynode='symbol')
+    compargs = {}
+    if varkey:
+        compargs[varkey] = compilelist(treeargs.pop(varkey))
+    compargs.update(compiledict(treeargs))
+    return compargs
+
 def buildkeyvaluepair(exp, content):
     raise error.ParseError(_("can't use a key-value pair in this context"))
 
@@ -830,16 +855,16 @@  def rstdoc(context, mapping, args):
 
     return minirst.format(text, style=style, keep=['verbose'])
 
-@templatefunc('separate(sep, args)')
+@templatefunc('separate(sep, args)', argspec='sep *args')
 def separate(context, mapping, args):
     """Add a separator between non-empty arguments."""
-    if not args:
+    if 'sep' not in args:
         # i18n: "separate" is a keyword
         raise error.ParseError(_("separate expects at least one argument"))
 
-    sep = evalstring(context, mapping, args[0])
+    sep = evalstring(context, mapping, args['sep'])
     first = True
-    for arg in args[1:]:
+    for arg in args['args']:
         argstr = evalstring(context, mapping, arg)
         if not argstr:
             continue
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
@@ -146,6 +146,13 @@  Keyword arguments:
   hg: parse error: can't use a key-value pair in this context
   [255]
 
+Call function which takes named arguments by filter syntax:
+
+  $ hg debugtemplate '{" "|separate}'
+  $ hg debugtemplate '{("not", "an", "argument", "list")|separate}'
+  hg: parse error: unknown method 'list'
+  [255]
+
 Second branch starting at nullrev:
 
   $ hg update null