changeset 31886:bdda942f4b9c

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.
author Yuya Nishihara <yuya@tcha.org>
date Mon, 03 Apr 2017 21:22:39 +0900
parents d18b624c1c06
children f7b3677f66cd
files mercurial/registrar.py mercurial/templater.py tests/test-command-template.t
diffstat 3 files changed, 47 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/registrar.py	Mon Apr 03 20:55:55 2017 +0900
+++ b/mercurial/registrar.py	Mon Apr 03 21:22:39 2017 +0900
@@ -234,7 +234,7 @@
 
         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 @@
 
     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 @@
     Otherwise, explicit 'templater.loadfunction()' is needed.
     """
     _getname = _funcregistrarbase._parsefuncdecl
+
+    def _extrasetup(self, name, func, argspec=None):
+        func._argspec = argspec
--- a/mercurial/templater.py	Mon Apr 03 20:55:55 2017 +0900
+++ b/mercurial/templater.py	Mon Apr 03 21:22:39 2017 +0900
@@ -370,14 +370,15 @@
         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 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"))
 
@@ -832,16 +857,16 @@
 
     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
--- a/tests/test-command-template.t	Mon Apr 03 20:55:55 2017 +0900
+++ b/tests/test-command-template.t	Mon Apr 03 21:22:39 2017 +0900
@@ -146,6 +146,13 @@
   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