Patchwork [1,of,5] templatekw: use decorator to mark a function as template keyword

login
register
mail settings
Submitter Katsunori FUJIWARA
Date Dec. 23, 2015, 4:46 p.m.
Message ID <e49528b9e652dc0e5d25.1450889197@feefifofum>
Download mbox | patch
Permalink /patch/12297/
State Changes Requested
Delegated to: Yuya Nishihara
Headers show

Comments

Katsunori FUJIWARA - Dec. 23, 2015, 4:46 p.m.
# HG changeset patch
# User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
# Date 1450888882 -32400
#      Thu Dec 24 01:41:22 2015 +0900
# Node ID e49528b9e652dc0e5d2543ea34de3068f78ae9ed
# Parent  fe376159a58d9b3d748b669ac011b0eed0346fea
templatekw: use decorator to mark a function as template keyword

Using decorator can localize changes for adding (or removing) a
template keyword function in source code.

It is also useful to pick keywords up for specific purpose. For
example, I'm planning to make "hg help templates" show the list of
keywords referring each element (or information of it) of a list in
"expr % '{template}'" form (e.g. '{name}' and '{source}' for
'file_copies' keyword).

To avoid (1) redundancy between "keyword name" string and (the
beginning of) help document, and (2) accidental typo of help document,
this patch also makes decorator put keyword name into the beginning of
help.

For similarity to decorator functions introduced by subsequent
patches, this patch uses 'templatekeyword' instead of 'keyword', even
though the former is a little redundant in 'templatekw.py'.

    file                name              reason
    =================== ================= ==================================
    templatekw.py       templatekeyword   for similarity to others
    templatefilters.py  templatefilter    'filter' hides Python built-in one
    templaters.py       templatefunc      'func' is too generic
Yuya Nishihara - Dec. 24, 2015, 12:54 p.m.
On Thu, 24 Dec 2015 01:46:37 +0900, FUJIWARA Katsunori wrote:
> # HG changeset patch
> # User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
> # Date 1450888882 -32400
> #      Thu Dec 24 01:41:22 2015 +0900
> # Node ID e49528b9e652dc0e5d2543ea34de3068f78ae9ed
> # Parent  fe376159a58d9b3d748b669ac011b0eed0346fea
> templatekw: use decorator to mark a function as template keyword

> +@templatekw.templatekeyword('svnrev')
>  def kwsvnrev(repo, ctx, **args):
> -    """:svnrev: String. Converted subversion revision number."""
> +    """String. Converted subversion revision number."""
>      return kwconverted(ctx, 'svnrev')
>  
> +@templatekw.templatekeyword('svnpath')
>  def kwsvnpath(repo, ctx, **args):
> -    """:svnpath: String. Converted subversion revision project path."""
> +    """String. Converted subversion revision project path."""
>      return kwconverted(ctx, 'svnpath')
>  
> +@templatekw.templatekeyword('svnuuid')
>  def kwsvnuuid(repo, ctx, **args):
> -    """:svnuuid: String. Converted subversion revision repository identifier."""
> +    """String. Converted subversion revision repository identifier."""
>      return kwconverted(ctx, 'svnuuid')
>  
> -def extsetup(ui):
> -    templatekw.keywords['svnrev'] = kwsvnrev
> -    templatekw.keywords['svnpath'] = kwsvnpath
> -    templatekw.keywords['svnuuid'] = kwsvnuuid

Extensions shouldn't change the keywords table just by importing. It should be
delayed until uisetup() or extsetup().

> +keywords = {}
>  
> +def templatekeyword(name):
> +    """Return a decorator for template keyword function
> +
> +    'name' argument is the keyword name.
> +    """
> +    def decorator(func):
> +        keywords[name] = func
> +        if func.__doc__:
> +            func.__doc__ = ":%s: %s" % (name, func.__doc__.strip())
> +        return func
> +    return decorator

Perhaps, we'll need a decorator that can handle common cases? And the hgweb
will eventually use it.
Katsunori FUJIWARA - Dec. 26, 2015, 11:23 a.m.
At Thu, 24 Dec 2015 21:54:17 +0900,
Yuya Nishihara wrote:
> 
> On Thu, 24 Dec 2015 01:46:37 +0900, FUJIWARA Katsunori wrote:
> > # HG changeset patch
> > # User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
> > # Date 1450888882 -32400
> > #      Thu Dec 24 01:41:22 2015 +0900
> > # Node ID e49528b9e652dc0e5d2543ea34de3068f78ae9ed
> > # Parent  fe376159a58d9b3d748b669ac011b0eed0346fea
> > templatekw: use decorator to mark a function as template keyword
> 
> > +@templatekw.templatekeyword('svnrev')
> >  def kwsvnrev(repo, ctx, **args):
> > -    """:svnrev: String. Converted subversion revision number."""
> > +    """String. Converted subversion revision number."""
> >      return kwconverted(ctx, 'svnrev')
> >  
> > +@templatekw.templatekeyword('svnpath')
> >  def kwsvnpath(repo, ctx, **args):
> > -    """:svnpath: String. Converted subversion revision project path."""
> > +    """String. Converted subversion revision project path."""
> >      return kwconverted(ctx, 'svnpath')
> >  
> > +@templatekw.templatekeyword('svnuuid')
> >  def kwsvnuuid(repo, ctx, **args):
> > -    """:svnuuid: String. Converted subversion revision repository identifier."""
> > +    """String. Converted subversion revision repository identifier."""
> >      return kwconverted(ctx, 'svnuuid')
> >  
> > -def extsetup(ui):
> > -    templatekw.keywords['svnrev'] = kwsvnrev
> > -    templatekw.keywords['svnpath'] = kwsvnpath
> > -    templatekw.keywords['svnuuid'] = kwsvnuuid
> 
> Extensions shouldn't change the keywords table just by importing. It should be
> delayed until uisetup() or extsetup().

In fact, once I wrote decorators delaying registration until uisetup()
or extsetup() in 'cmdutil.command' style.

But I discarded them, because (1) 'cmdutil.command' style isn't
reasonable to hide internal implementation (like
_statuscallers/_existingcallers of fileset or safesymbols of revset)
and (2) convert extension adds template filters in module top level
code path :-)

I'll post revised ones.

> > +keywords = {}
> >  
> > +def templatekeyword(name):
> > +    """Return a decorator for template keyword function
> > +
> > +    'name' argument is the keyword name.
> > +    """
> > +    def decorator(func):
> > +        keywords[name] = func
> > +        if func.__doc__:
> > +            func.__doc__ = ":%s: %s" % (name, func.__doc__.strip())
> > +        return func
> > +    return decorator
> 
> Perhaps, we'll need a decorator that can handle common cases? And the hgweb
> will eventually use it.
> 

OK, I'll try to define generic decorator to centralize similar logic
into it.

----------------------------------------------------------------------
[FUJIWARA Katsunori]                             foozy@lares.dti.ne.jp
Yuya Nishihara - Dec. 26, 2015, 11:51 a.m.
On Sat, 26 Dec 2015 20:23:52 +0900, FUJIWARA Katsunori wrote:
> At Thu, 24 Dec 2015 21:54:17 +0900,
> Yuya Nishihara wrote:
> > > +@templatekw.templatekeyword('svnuuid')
> > >  def kwsvnuuid(repo, ctx, **args):
> > > -    """:svnuuid: String. Converted subversion revision repository identifier."""
> > > +    """String. Converted subversion revision repository identifier."""
> > >      return kwconverted(ctx, 'svnuuid')
> > >  
> > > -def extsetup(ui):
> > > -    templatekw.keywords['svnrev'] = kwsvnrev
> > > -    templatekw.keywords['svnpath'] = kwsvnpath
> > > -    templatekw.keywords['svnuuid'] = kwsvnuuid
> > 
> > Extensions shouldn't change the keywords table just by importing. It should be
> > delayed until uisetup() or extsetup().
> 
> In fact, once I wrote decorators delaying registration until uisetup()
> or extsetup() in 'cmdutil.command' style.
> 
> But I discarded them, because (1) 'cmdutil.command' style isn't
> reasonable to hide internal implementation (like
> _statuscallers/_existingcallers of fileset or safesymbols of revset)
> and

You could attach attributes to function or build a table of (func, attrs):

  def predicate(name, safe=False):
      def decorator(func):
          func._safe = safe
          return func

I don't know if people like it, but it works.

> (2) convert extension adds template filters in module top level
> code path :-)

s/convert/keyword/ ?

Yep, that should be fixed.
Gregory Szorc - Dec. 26, 2015, 5:47 p.m.
> On Dec 26, 2015, at 04:23, FUJIWARA Katsunori <foozy@lares.dti.ne.jp> wrote:
> 
> At Thu, 24 Dec 2015 21:54:17 +0900,
> Yuya Nishihara wrote:
>> 
>>> On Thu, 24 Dec 2015 01:46:37 +0900, FUJIWARA Katsunori wrote:
>>> # HG changeset patch
>>> # User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
>>> # Date 1450888882 -32400
>>> #      Thu Dec 24 01:41:22 2015 +0900
>>> # Node ID e49528b9e652dc0e5d2543ea34de3068f78ae9ed
>>> # Parent  fe376159a58d9b3d748b669ac011b0eed0346fea
>>> templatekw: use decorator to mark a function as template keyword
>> 
>>> +@templatekw.templatekeyword('svnrev')
>>> def kwsvnrev(repo, ctx, **args):
>>> -    """:svnrev: String. Converted subversion revision number."""
>>> +    """String. Converted subversion revision number."""
>>>     return kwconverted(ctx, 'svnrev')
>>> 
>>> +@templatekw.templatekeyword('svnpath')
>>> def kwsvnpath(repo, ctx, **args):
>>> -    """:svnpath: String. Converted subversion revision project path."""
>>> +    """String. Converted subversion revision project path."""
>>>     return kwconverted(ctx, 'svnpath')
>>> 
>>> +@templatekw.templatekeyword('svnuuid')
>>> def kwsvnuuid(repo, ctx, **args):
>>> -    """:svnuuid: String. Converted subversion revision repository identifier."""
>>> +    """String. Converted subversion revision repository identifier."""
>>>     return kwconverted(ctx, 'svnuuid')
>>> 
>>> -def extsetup(ui):
>>> -    templatekw.keywords['svnrev'] = kwsvnrev
>>> -    templatekw.keywords['svnpath'] = kwsvnpath
>>> -    templatekw.keywords['svnuuid'] = kwsvnuuid
>> 
>> Extensions shouldn't change the keywords table just by importing. It should be
>> delayed until uisetup() or extsetup().
> 
> In fact, once I wrote decorators delaying registration until uisetup()
> or extsetup() in 'cmdutil.command' style.

Relatedly, cmdutil and commands have a cycle-like issue that is making it difficult to convert to absolute import. I've been contemplating the idea of adding registrar.py or similar to hold registrations for commands, templates, revsets, etc.

> 
> But I discarded them, because (1) 'cmdutil.command' style isn't
> reasonable to hide internal implementation (like
> _statuscallers/_existingcallers of fileset or safesymbols of revset)
> and (2) convert extension adds template filters in module top level
> code path :-)
> 
> I'll post revised ones.
> 
>>> +keywords = {}
>>> 
>>> +def templatekeyword(name):
>>> +    """Return a decorator for template keyword function
>>> +
>>> +    'name' argument is the keyword name.
>>> +    """
>>> +    def decorator(func):
>>> +        keywords[name] = func
>>> +        if func.__doc__:
>>> +            func.__doc__ = ":%s: %s" % (name, func.__doc__.strip())
>>> +        return func
>>> +    return decorator
>> 
>> Perhaps, we'll need a decorator that can handle common cases? And the hgweb
>> will eventually use it.
> 
> OK, I'll try to define generic decorator to centralize similar logic
> into it.
> 
> ----------------------------------------------------------------------
> [FUJIWARA Katsunori]                             foozy@lares.dti.ne.jp
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@selenic.com
> https://selenic.com/mailman/listinfo/mercurial-devel
Katsunori FUJIWARA - Dec. 27, 2015, 6:18 a.m.
At Sat, 26 Dec 2015 20:51:41 +0900,
Yuya Nishihara wrote:
> 
> On Sat, 26 Dec 2015 20:23:52 +0900, FUJIWARA Katsunori wrote:
> > At Thu, 24 Dec 2015 21:54:17 +0900,
> > Yuya Nishihara wrote:
> > > > +@templatekw.templatekeyword('svnuuid')
> > > >  def kwsvnuuid(repo, ctx, **args):
> > > > -    """:svnuuid: String. Converted subversion revision repository identifier."""
> > > > +    """String. Converted subversion revision repository identifier."""
> > > >      return kwconverted(ctx, 'svnuuid')
> > > >  
> > > > -def extsetup(ui):
> > > > -    templatekw.keywords['svnrev'] = kwsvnrev
> > > > -    templatekw.keywords['svnpath'] = kwsvnpath
> > > > -    templatekw.keywords['svnuuid'] = kwsvnuuid
> > > 
> > > Extensions shouldn't change the keywords table just by importing. It should be
> > > delayed until uisetup() or extsetup().
> > 
> > In fact, once I wrote decorators delaying registration until uisetup()
> > or extsetup() in 'cmdutil.command' style.
> > 
> > But I discarded them, because (1) 'cmdutil.command' style isn't
> > reasonable to hide internal implementation (like
> > _statuscallers/_existingcallers of fileset or safesymbols of revset)
> > and
> 
> You could attach attributes to function or build a table of (func, attrs):
> 
>   def predicate(name, safe=False):
>       def decorator(func):
>           func._safe = safe
>           return func
> 
> I don't know if people like it, but it works.

I'm revising this series with class-base decorator like 'webcommand'
in webcommands.py. It'll resolve problem (1) above and avoid scatter
of similar logic.


> > (2) convert extension adds template filters in module top level
> > code path :-)
> 
> s/convert/keyword/ ?

Oops, you're right !

> Yep, that should be fixed.

I'll fix it.

----------------------------------------------------------------------
[FUJIWARA Katsunori]                             foozy@lares.dti.ne.jp
Katsunori FUJIWARA - Dec. 27, 2015, 6:25 a.m.
At Sat, 26 Dec 2015 10:47:54 -0700,
Gregory Szorc wrote:
> 
> 
> 
> > On Dec 26, 2015, at 04:23, FUJIWARA Katsunori <foozy@lares.dti.ne.jp> wrote:
> > 
> > At Thu, 24 Dec 2015 21:54:17 +0900,
> > Yuya Nishihara wrote:
> >> 
> >>> On Thu, 24 Dec 2015 01:46:37 +0900, FUJIWARA Katsunori wrote:
> >>> # HG changeset patch
> >>> # User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
> >>> # Date 1450888882 -32400
> >>> #      Thu Dec 24 01:41:22 2015 +0900
> >>> # Node ID e49528b9e652dc0e5d2543ea34de3068f78ae9ed
> >>> # Parent  fe376159a58d9b3d748b669ac011b0eed0346fea
> >>> templatekw: use decorator to mark a function as template keyword
> >> 
> >>> +@templatekw.templatekeyword('svnrev')
> >>> def kwsvnrev(repo, ctx, **args):
> >>> -    """:svnrev: String. Converted subversion revision number."""
> >>> +    """String. Converted subversion revision number."""
> >>>     return kwconverted(ctx, 'svnrev')
> >>> 
> >>> +@templatekw.templatekeyword('svnpath')
> >>> def kwsvnpath(repo, ctx, **args):
> >>> -    """:svnpath: String. Converted subversion revision project path."""
> >>> +    """String. Converted subversion revision project path."""
> >>>     return kwconverted(ctx, 'svnpath')
> >>> 
> >>> +@templatekw.templatekeyword('svnuuid')
> >>> def kwsvnuuid(repo, ctx, **args):
> >>> -    """:svnuuid: String. Converted subversion revision repository identifier."""
> >>> +    """String. Converted subversion revision repository identifier."""
> >>>     return kwconverted(ctx, 'svnuuid')
> >>> 
> >>> -def extsetup(ui):
> >>> -    templatekw.keywords['svnrev'] = kwsvnrev
> >>> -    templatekw.keywords['svnpath'] = kwsvnpath
> >>> -    templatekw.keywords['svnuuid'] = kwsvnuuid
> >> 
> >> Extensions shouldn't change the keywords table just by importing. It should be
> >> delayed until uisetup() or extsetup().
> > 
> > In fact, once I wrote decorators delaying registration until uisetup()
> > or extsetup() in 'cmdutil.command' style.
> 
> Relatedly, cmdutil and commands have a cycle-like issue that is
> making it difficult to convert to absolute import. I've been
> contemplating the idea of adding registrar.py or similar to hold
> registrations for commands, templates, revsets, etc.
> 

I'm just worried about where utilities should be placed to avoid
cyclic importing, too !

'registrar.py' seems reasonable (at least) for me.

> > 
> > But I discarded them, because (1) 'cmdutil.command' style isn't
> > reasonable to hide internal implementation (like
> > _statuscallers/_existingcallers of fileset or safesymbols of revset)
> > and (2) convert extension adds template filters in module top level
> > code path :-)
> > 
> > I'll post revised ones.
> > 
> >>> +keywords = {}
> >>> 
> >>> +def templatekeyword(name):
> >>> +    """Return a decorator for template keyword function
> >>> +
> >>> +    'name' argument is the keyword name.
> >>> +    """
> >>> +    def decorator(func):
> >>> +        keywords[name] = func
> >>> +        if func.__doc__:
> >>> +            func.__doc__ = ":%s: %s" % (name, func.__doc__.strip())
> >>> +        return func
> >>> +    return decorator
> >> 
> >> Perhaps, we'll need a decorator that can handle common cases? And the hgweb
> >> will eventually use it.
> > 
> > OK, I'll try to define generic decorator to centralize similar logic
> > into it.
> > 
> > ----------------------------------------------------------------------
> > [FUJIWARA Katsunori]                             foozy@lares.dti.ne.jp
> > _______________________________________________
> > Mercurial-devel mailing list
> > Mercurial-devel@selenic.com
> > https://selenic.com/mailman/listinfo/mercurial-devel
> 

----------------------------------------------------------------------
[FUJIWARA Katsunori]                             foozy@lares.dti.ne.jp

Patch

diff --git a/hgext/convert/__init__.py b/hgext/convert/__init__.py
--- a/hgext/convert/__init__.py
+++ b/hgext/convert/__init__.py
@@ -429,22 +429,20 @@  def kwconverted(ctx, name):
             return subversion.revsplit(rev)[0]
     return rev
 
+@templatekw.templatekeyword('svnrev')
 def kwsvnrev(repo, ctx, **args):
-    """:svnrev: String. Converted subversion revision number."""
+    """String. Converted subversion revision number."""
     return kwconverted(ctx, 'svnrev')
 
+@templatekw.templatekeyword('svnpath')
 def kwsvnpath(repo, ctx, **args):
-    """:svnpath: String. Converted subversion revision project path."""
+    """String. Converted subversion revision project path."""
     return kwconverted(ctx, 'svnpath')
 
+@templatekw.templatekeyword('svnuuid')
 def kwsvnuuid(repo, ctx, **args):
-    """:svnuuid: String. Converted subversion revision repository identifier."""
+    """String. Converted subversion revision repository identifier."""
     return kwconverted(ctx, 'svnuuid')
 
-def extsetup(ui):
-    templatekw.keywords['svnrev'] = kwsvnrev
-    templatekw.keywords['svnpath'] = kwsvnpath
-    templatekw.keywords['svnuuid'] = kwsvnuuid
-
 # tell hggettext to extract docstrings from these functions:
 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
diff --git a/hgext/transplant.py b/hgext/transplant.py
--- a/hgext/transplant.py
+++ b/hgext/transplant.py
@@ -704,6 +704,7 @@  def revsettransplanted(repo, subset, x):
     return revset.baseset([r for r in s if
         repo[r].extra().get('transplant_source')])
 
+@templatekw.templatekeyword('transplanted')
 def kwtransplanted(repo, ctx, **args):
     """:transplanted: String. The node identifier of the transplanted
     changeset if any."""
@@ -712,7 +713,6 @@  def kwtransplanted(repo, ctx, **args):
 
 def extsetup(ui):
     revset.symbols['transplanted'] = revsettransplanted
-    templatekw.keywords['transplanted'] = kwtransplanted
     cmdutil.unfinishedstates.append(
         ['series', True, False, _('transplant in progress'),
          _("use 'hg transplant --continue' or 'hg update' to abort")])
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -195,23 +195,48 @@  def getrenamedfn(repo, endrev=None):
 
     return getrenamed
 
+# 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 = {}
 
+def templatekeyword(name):
+    """Return a decorator for template keyword function
+
+    'name' argument is the keyword name.
+    """
+    def decorator(func):
+        keywords[name] = func
+        if func.__doc__:
+            func.__doc__ = ":%s: %s" % (name, func.__doc__.strip())
+        return func
+    return decorator
+
+@templatekeyword('author')
 def showauthor(repo, ctx, templ, **args):
-    """:author: String. The unmodified author of the changeset."""
+    """String. The unmodified author of the changeset."""
     return ctx.user()
 
+@templatekeyword('bisect')
 def showbisect(repo, ctx, templ, **args):
-    """:bisect: String. The changeset bisection status."""
+    """String. The changeset bisection status."""
     return hbisect.label(repo, ctx.node())
 
+@templatekeyword('branch')
 def showbranch(**args):
-    """:branch: String. The name of the branch on which the changeset was
+    """String. The name of the branch on which the changeset was
     committed.
     """
     return args['ctx'].branch()
 
+@templatekeyword('branches')
 def showbranches(**args):
-    """:branches: List of strings. The name of the branch on which the
+    """List of strings. The name of the branch on which the
     changeset was committed. Will be empty if the branch name was
     default. (DEPRECATED)
     """
@@ -220,8 +245,9 @@  def showbranches(**args):
         return showlist('branch', [branch], plural='branches', **args)
     return showlist('branch', [], plural='branches', **args)
 
+@templatekeyword('bookmarks')
 def showbookmarks(**args):
-    """:bookmarks: List of strings. Any bookmarks associated with the
+    """List of strings. Any bookmarks associated with the
     changeset. Also sets 'active', the name of the active bookmark.
     """
     repo = args['ctx']._repo
@@ -231,44 +257,51 @@  def showbookmarks(**args):
     f = _showlist('bookmark', bookmarks, **args)
     return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
 
+@templatekeyword('children')
 def showchildren(**args):
-    """:children: List of strings. The children of the changeset."""
+    """List of strings. The children of the changeset."""
     ctx = args['ctx']
     childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
     return showlist('children', childrevs, element='child', **args)
 
 # Deprecated, but kept alive for help generation a purpose.
+@templatekeyword('currentbookmark')
 def showcurrentbookmark(**args):
-    """:currentbookmark: String. The active bookmark, if it is
+    """String. The active bookmark, if it is
     associated with the changeset (DEPRECATED)"""
     return showactivebookmark(**args)
 
+@templatekeyword('activebookmark')
 def showactivebookmark(**args):
-    """:activebookmark: String. The active bookmark, if it is
+    """String. The active bookmark, if it is
     associated with the changeset"""
     active = args['repo']._activebookmark
     if active and active in args['ctx'].bookmarks():
         return active
     return ''
 
+@templatekeyword('date')
 def showdate(repo, ctx, templ, **args):
-    """:date: Date information. The date when the changeset was committed."""
+    """Date information. The date when the changeset was committed."""
     return ctx.date()
 
+@templatekeyword('desc')
 def showdescription(repo, ctx, templ, **args):
-    """:desc: String. The text of the changeset description."""
+    """String. The text of the changeset description."""
     return ctx.description().strip()
 
+@templatekeyword('diffstat')
 def showdiffstat(repo, ctx, templ, **args):
-    """:diffstat: String. Statistics of changes with the following format:
+    """String. Statistics of changes with the following format:
     "modified files: +added/-removed lines"
     """
     stats = patch.diffstatdata(util.iterlines(ctx.diff()))
     maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
     return '%s: +%s/-%s' % (len(stats), adds, removes)
 
+@templatekeyword('extras')
 def showextras(**args):
-    """:extras: List of dicts with key, value entries of the 'extras'
+    """List of dicts with key, value entries of the 'extras'
     field of this changeset."""
     extras = args['ctx'].extra()
     extras = util.sortdict((k, extras[k]) for k in sorted(extras))
@@ -278,14 +311,16 @@  def showextras(**args):
     return _hybrid(f, extras, makemap,
                    lambda x: '%s=%s' % (x['key'], x['value']))
 
+@templatekeyword('file_adds')
 def showfileadds(**args):
-    """:file_adds: List of strings. Files added by this changeset."""
+    """List of strings. Files added by this changeset."""
     repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
     return showlist('file_add', getfiles(repo, ctx, revcache)[1],
                     element='file', **args)
 
+@templatekeyword('file_copies')
 def showfilecopies(**args):
-    """:file_copies: List of strings. Files copied in this changeset with
+    """List of strings. Files copied in this changeset with
     their sources.
     """
     cache, ctx = args['cache'], args['ctx']
@@ -310,8 +345,9 @@  def showfilecopies(**args):
 # showfilecopiesswitch() displays file copies only if copy records are
 # provided before calling the templater, usually with a --copies
 # command line switch.
+@templatekeyword('file_copies_switch')
 def showfilecopiesswitch(**args):
-    """:file_copies_switch: List of strings. Like "file_copies" but displayed
+    """List of strings. Like "file_copies" but displayed
     only if the --copied switch is set.
     """
     copies = args['revcache'].get('copies') or []
@@ -322,26 +358,30 @@  def showfilecopiesswitch(**args):
     return _hybrid(f, copies, makemap,
                    lambda x: '%s (%s)' % (x['name'], x['source']))
 
+@templatekeyword('file_dels')
 def showfiledels(**args):
-    """:file_dels: List of strings. Files removed by this changeset."""
+    """List of strings. Files removed by this changeset."""
     repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
     return showlist('file_del', getfiles(repo, ctx, revcache)[2],
                     element='file', **args)
 
+@templatekeyword('file_mods')
 def showfilemods(**args):
-    """:file_mods: List of strings. Files modified by this changeset."""
+    """List of strings. Files modified by this changeset."""
     repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
     return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
                     element='file', **args)
 
+@templatekeyword('files')
 def showfiles(**args):
-    """:files: List of strings. All files modified, added, or removed by this
+    """List of strings. All files modified, added, or removed by this
     changeset.
     """
     return showlist('file', args['ctx'].files(), **args)
 
+@templatekeyword('graphnode')
 def showgraphnode(repo, ctx, **args):
-    """:graphnode: String. The character representing the changeset node in
+    """String. The character representing the changeset node in
     an ASCII revision graph"""
     wpnodes = repo.dirstate.parents()
     if wpnodes[1] == nullid:
@@ -355,8 +395,9 @@  def showgraphnode(repo, ctx, **args):
     else:
         return 'o'
 
+@templatekeyword('latesttag')
 def showlatesttag(**args):
-    """:latesttag: List of strings. The global tags on the most recent globally
+    """List of strings. The global tags on the most recent globally
     tagged ancestor of this changeset.
     """
     return showlatesttags(None, **args)
@@ -381,12 +422,14 @@  def showlatesttags(pattern, **args):
     f = _showlist('latesttag', tags, separator=':', **args)
     return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
 
+@templatekeyword('latesttagdistance')
 def showlatesttagdistance(repo, ctx, templ, cache, **args):
-    """:latesttagdistance: Integer. Longest path to the latest tag."""
+    """Integer. Longest path to the latest tag."""
     return getlatesttags(repo, ctx, cache)[1]
 
+@templatekeyword('changessincelatesttag')
 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
-    """:changessincelatesttag: Integer. All ancestors not in the latest tag."""
+    """Integer. All ancestors not in the latest tag."""
     latesttag = getlatesttags(repo, ctx, cache)[2][0]
 
     return _showchangessincetag(repo, ctx, tag=latesttag, **args)
@@ -403,6 +446,7 @@  def _showchangessincetag(repo, ctx, **ar
 
     return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
 
+@templatekeyword('manifest')
 def showmanifest(**args):
     repo, ctx, templ = args['repo'], args['ctx'], args['templ']
     mnode = ctx.manifestnode()
@@ -413,36 +457,42 @@  def showmanifest(**args):
     args.update({'rev': repo.manifest.rev(mnode), 'node': hex(mnode)})
     return templ('manifest', **args)
 
+@templatekeyword('node')
 def shownode(repo, ctx, templ, **args):
-    """:node: String. The changeset identification hash, as a 40 hexadecimal
+    """String. The changeset identification hash, as a 40 hexadecimal
     digit string.
     """
     return ctx.hex()
 
+@templatekeyword('p1rev')
 def showp1rev(repo, ctx, templ, **args):
-    """:p1rev: Integer. The repository-local revision number of the changeset's
+    """Integer. The repository-local revision number of the changeset's
     first parent, or -1 if the changeset has no parents."""
     return ctx.p1().rev()
 
+@templatekeyword('p2rev')
 def showp2rev(repo, ctx, templ, **args):
-    """:p2rev: Integer. The repository-local revision number of the changeset's
+    """Integer. The repository-local revision number of the changeset's
     second parent, or -1 if the changeset has no second parent."""
     return ctx.p2().rev()
 
+@templatekeyword('p1node')
 def showp1node(repo, ctx, templ, **args):
-    """:p1node: String. The identification hash of the changeset's first parent,
+    """String. The identification hash of the changeset's first parent,
     as a 40 digit hexadecimal string. If the changeset has no parents, all
     digits are 0."""
     return ctx.p1().hex()
 
+@templatekeyword('p2node')
 def showp2node(repo, ctx, templ, **args):
-    """:p2node: String. The identification hash of the changeset's second
+    """String. The identification hash of the changeset's second
     parent, as a 40 digit hexadecimal string. If the changeset has no second
     parent, all digits are 0."""
     return ctx.p2().hex()
 
+@templatekeyword('parents')
 def showparents(**args):
-    """:parents: List of strings. The parents of the changeset in "rev:node"
+    """List of strings. The parents of the changeset in "rev:node"
     format. If the changeset has only one "natural" parent (the predecessor
     revision) nothing is shown."""
     repo = args['repo']
@@ -453,16 +503,19 @@  def showparents(**args):
                for p in scmutil.meaningfulparents(repo, ctx)]
     return showlist('parent', parents, **args)
 
+@templatekeyword('phase')
 def showphase(repo, ctx, templ, **args):
-    """:phase: String. The changeset phase name."""
+    """String. The changeset phase name."""
     return ctx.phasestr()
 
+@templatekeyword('phaseidx')
 def showphaseidx(repo, ctx, templ, **args):
-    """:phaseidx: Integer. The changeset phase index."""
+    """Integer. The changeset phase index."""
     return ctx.phase()
 
+@templatekeyword('rev')
 def showrev(repo, ctx, templ, **args):
-    """:rev: Integer. The repository-local changeset revision number."""
+    """Integer. The repository-local changeset revision number."""
     return scmutil.intrev(ctx.rev())
 
 def showrevslist(name, revs, **args):
@@ -473,8 +526,9 @@  def showrevslist(name, revs, **args):
     return _hybrid(f, revs,
                    lambda x: {name: x, 'ctx': repo[x], 'revcache': {}})
 
+@templatekeyword('subrepos')
 def showsubrepos(**args):
-    """:subrepos: List of strings. Updated subrepositories in the changeset."""
+    """List of strings. Updated subrepositories in the changeset."""
     ctx = args['ctx']
     substate = ctx.substate
     if not substate:
@@ -500,55 +554,10 @@  def shownames(namespace, **args):
 # don't remove "showtags" definition, even though namespaces will put
 # a helper function for "tags" keyword into "keywords" map automatically,
 # because online help text is built without namespaces initialization
+@templatekeyword('tags')
 def showtags(**args):
-    """:tags: List of strings. Any tags associated with the changeset."""
+    """List of strings. Any tags associated with the changeset."""
     return shownames('tags', **args)
 
-# 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 = {
-    'activebookmark': showactivebookmark,
-    'author': showauthor,
-    'bisect': showbisect,
-    'branch': showbranch,
-    'branches': showbranches,
-    'bookmarks': showbookmarks,
-    'changessincelatesttag': showchangessincelatesttag,
-    'children': showchildren,
-    # currentbookmark is deprecated
-    'currentbookmark': showcurrentbookmark,
-    'date': showdate,
-    'desc': showdescription,
-    'diffstat': showdiffstat,
-    'extras': showextras,
-    'file_adds': showfileadds,
-    'file_copies': showfilecopies,
-    'file_copies_switch': showfilecopiesswitch,
-    'file_dels': showfiledels,
-    'file_mods': showfilemods,
-    'files': showfiles,
-    'graphnode': showgraphnode,
-    'latesttag': showlatesttag,
-    'latesttagdistance': showlatesttagdistance,
-    'manifest': showmanifest,
-    'node': shownode,
-    'p1rev': showp1rev,
-    'p1node': showp1node,
-    'p2rev': showp2rev,
-    'p2node': showp2node,
-    'parents': showparents,
-    'phase': showphase,
-    'phaseidx': showphaseidx,
-    'rev': showrev,
-    'subrepos': showsubrepos,
-    'tags': showtags,
-}
-
 # tell hggettext to extract docstrings from these functions:
 i18nfunctions = keywords.values()