Patchwork [7,of,7] templatekw: add 'requires' flag to switch to exception-safe interface

login
register
mail settings
Submitter Yuya Nishihara
Date Feb. 26, 2018, 2:29 p.m.
Message ID <e426dc16137c931668ce.1519655380@mimosa>
Download mbox | patch
Permalink /patch/28413/
State Accepted
Headers show

Comments

Yuya Nishihara - Feb. 26, 2018, 2:29 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1519532675 -32400
#      Sun Feb 25 13:24:35 2018 +0900
# Node ID e426dc16137c931668ce4a4c3b5fec0edae8107b
# Parent  02afa73e633146722c187090fd73885fceb552d9
templatekw: add 'requires' flag to switch to exception-safe interface

The current templatekw interface, f(repo, ctx, templ, **args), is horrible
because it's quite easy to encounter TypeError, ValueError, etc. It's also
bad for Python 3 porting due to the **kwargs issue.

This patch introduces a flag to switch to new f(context, mapping) API seen
in templater functions. The requirement spec isn't verified (yet) because
context.resource() can gracefully raise a ResourceUnavailable exception,
but it's planned to be used as a filter in the help, such as "Revision
Keywords" (if 'ctx' in requires), "File Keywords" (if 'fctx' in requires),
etc.

showauthor() is ported to the new API as an example. 20 more follows.

Patch

diff --git a/mercurial/formatter.py b/mercurial/formatter.py
--- a/mercurial/formatter.py
+++ b/mercurial/formatter.py
@@ -383,9 +383,7 @@  class templateformatter(baseformatter):
             return
         ref = self._parts[part]
 
-        # TODO: add support for filectx. probably each template keyword or
-        # function will have to declare dependent resources. e.g.
-        # @templatekeyword(..., requires=('ctx',))
+        # TODO: add support for filectx
         props = {}
         # explicitly-defined fields precede templatekw
         props.update(item)
diff --git a/mercurial/registrar.py b/mercurial/registrar.py
--- a/mercurial/registrar.py
+++ b/mercurial/registrar.py
@@ -283,6 +283,14 @@  class templatekeyword(_templateregistrar
 
         templatekeyword = registrar.templatekeyword()
 
+        # new API (since Mercurial 4.6)
+        @templatekeyword('mykeyword', requires={'repo', 'ctx'})
+        def mykeywordfunc(context, mapping):
+            '''Explanation of this template keyword ....
+            '''
+            pass
+
+        # old API
         @templatekeyword('mykeyword')
         def mykeywordfunc(repo, ctx, templ, cache, revcache, **args):
             '''Explanation of this template keyword ....
@@ -291,6 +299,11 @@  class templatekeyword(_templateregistrar
 
     The first string argument is used also in online help.
 
+    Optional argument 'requires' should be a collection of resource names
+    which the template keyword depends on. This also serves as a flag to
+    switch to the new API. If 'requires' is unspecified, all template
+    keywords and resources are expanded to the function arguments.
+
     'templatekeyword' instance in example above can be used to
     decorate multiple functions.
 
@@ -301,6 +314,9 @@  class templatekeyword(_templateregistrar
     Otherwise, explicit 'templatekw.loadkeyword()' is needed.
     """
 
+    def _extrasetup(self, name, func, requires=None):
+        func._requires = requires
+
 class templatefilter(_templateregistrarbase):
     """Decorator to register template filer
 
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -341,21 +341,14 @@  defaulttempl = {
 # filecopy is preserved for compatibility reasons
 defaulttempl['filecopy'] = defaulttempl['file_copy']
 
-# keywords are callables like:
-# fn(repo, ctx, templ, cache, revcache, **args)
-# with:
-# repo - current repository instance
-# ctx - the changectx being displayed
-# templ - the templater instance
-# cache - a cache dictionary for the whole templater run
-# revcache - a cache dictionary for the current revision
+# keywords are callables (see registrar.templatekeyword for details)
 keywords = {}
-
 templatekeyword = registrar.templatekeyword(keywords)
 
-@templatekeyword('author')
-def showauthor(repo, ctx, templ, **args):
+@templatekeyword('author', requires={'ctx'})
+def showauthor(context, mapping):
     """String. The unmodified author of the changeset."""
+    ctx = context.resource(mapping, 'ctx')
     return ctx.user()
 
 @templatekeyword('bisect')
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -436,12 +436,18 @@  def runsymbol(context, mapping, key, def
             v = context.process(key, safemapping)
         except TemplateNotFound:
             v = default
-    if callable(v):
-        # TODO: templatekw functions will be updated to take (context, mapping)
-        # pair instead of **props
+    if callable(v) and getattr(v, '_requires', None) is None:
+        # old templatekw: expand all keywords and resources
         props = context._resources.copy()
         props.update(mapping)
         return v(**pycompat.strkwargs(props))
+    if callable(v):
+        # new templatekw
+        try:
+            return v(context, mapping)
+        except ResourceUnavailable:
+            # unsupported keyword is mapped to empty just like unknown keyword
+            return None
     return v
 
 def buildtemplate(exp, context):
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
@@ -214,6 +214,8 @@  Never crash on internal resource not ava
   abort: template resource not available: ctx
   [255]
 
+  $ hg config -T '{author}'
+
 Quoting for ui.logtemplate
 
   $ hg tip --config "ui.logtemplate={rev}\n"
diff --git a/tests/test-template-engine.t b/tests/test-template-engine.t
--- a/tests/test-template-engine.t
+++ b/tests/test-template-engine.t
@@ -9,6 +9,15 @@ 
   >         self._defaults = defaults
   >         self._resources = resources
   > 
+  >     def symbol(self, mapping, key):
+  >         return mapping[key]
+  > 
+  >     def resource(self, mapping, key):
+  >         v = self._resources[key]
+  >         if v is None:
+  >             v = mapping[key]
+  >         return v
+  > 
   >     def process(self, t, map):
   >         tmpl = self.loader(t)
   >         props = self._defaults.copy()
@@ -16,10 +25,12 @@ 
   >         for k, v in props.items():
   >             if k in ('templ', 'ctx', 'repo', 'revcache', 'cache', 'troubles'):
   >                 continue
-  >             if hasattr(v, '__call__'):
+  >             if callable(v) and getattr(v, '_requires', None) is None:
   >                 props = self._resources.copy()
   >                 props.update(map)
   >                 v = v(**props)
+  >             elif callable(v):
+  >                 v = v(self, props)
   >             v = templater.stringify(v)
   >             tmpl = tmpl.replace('{{%s}}' % k, v)
   >         yield tmpl