Patchwork [3,of,5] templater: use decorator to mark a function as template function

login
register
mail settings
Submitter Katsunori FUJIWARA
Date Dec. 23, 2015, 4:46 p.m.
Message ID <1946805b8237bbfbfe8c.1450889199@feefifofum>
Download mbox | patch
Permalink /patch/12298/
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 1946805b8237bbfbfe8c83196c00d9e41751d4f3
# Parent  1f82fc126080883bc931a26648e53298cbdbf047
templater: use decorator to mark a function as template function

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

It is also useful to pick functions 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. '{revision}' for 'revset()').

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

This uses not 'func' but 'templatefunc', because the former is too
generic one, even though the latter is a little redundant in
'templater.py'.

Patch

diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -310,8 +310,31 @@  def buildfunc(exp, context):
         return (runfilter, (args[0], f))
     raise error.ParseError(_("unknown function '%s'") % n)
 
+# dict of template built-in functions
+funcs = {}
+
+def templatefunc(decl):
+    """Return a decorator for built-in template function
+
+    'decl' argument is the declaration (including '(' like
+    'date(date[, fmt])') or the name (for internal use only) of a
+    function.
+    """
+    def decorator(func):
+        i = decl.find('(')
+        if i > 0:
+            name = decl[:i]
+        else:
+            name = decl
+        funcs[name] = func
+        if func.__doc__:
+            func.__doc__ = ":%s: %s" % (decl, func.__doc__.strip())
+        return func
+    return decorator
+
+@templatefunc('date(date[, fmt])')
 def date(context, mapping, args):
-    """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting
+    """Format a date. See :hg:`help dates` for formatting
     strings. The default is a Unix date format, including the timezone:
     "Mon Sep 04 15:13:13 2006 0700"."""
     if not (1 <= len(args) <= 2):
@@ -331,8 +354,9 @@  def date(context, mapping, args):
         # i18n: "date" is a keyword
         raise error.ParseError(_("date expects a date information"))
 
+@templatefunc('diff([includepattern [, excludepattern]])')
 def diff(context, mapping, args):
-    """:diff([includepattern [, excludepattern]]): Show a diff, optionally
+    """Show a diff, optionally
     specifying files to include or exclude."""
     if len(args) > 2:
         # i18n: "diff" is a keyword
@@ -350,8 +374,9 @@  def diff(context, mapping, args):
 
     return ''.join(chunks)
 
+@templatefunc('fill(text[, width[, initialident[, hangindent]]])')
 def fill(context, mapping, args):
-    """:fill(text[, width[, initialident[, hangindent]]]): Fill many
+    """Fill many
     paragraphs with optional indentation. See the "fill" filter."""
     if not (1 <= len(args) <= 4):
         # i18n: "fill" is a keyword
@@ -375,8 +400,9 @@  def fill(context, mapping, args):
 
     return templatefilters.fill(text, width, initindent, hangindent)
 
+@templatefunc('pad(text, width[, fillchar=\' \'[, right=False]])')
 def pad(context, mapping, args):
-    """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a
+    """Pad text with a
     fill character."""
     if not (2 <= len(args) <= 4):
         # i18n: "pad" is a keyword
@@ -398,8 +424,9 @@  def pad(context, mapping, args):
     else:
         return text.ljust(width, fillchar)
 
+@templatefunc('indent(text, indentchars[, firstline])')
 def indent(context, mapping, args):
-    """:indent(text, indentchars[, firstline]): Indents all non-empty lines
+    """Indents all non-empty lines
     with the characters given in the indentchars string. An optional
     third parameter will override the indent for the first line only
     if present."""
@@ -418,8 +445,9 @@  def indent(context, mapping, args):
     # the indent function doesn't indent the first line, so we do it here
     return templatefilters.indent(firstline + text, indent)
 
+@templatefunc('get(dict, key)')
 def get(context, mapping, args):
-    """:get(dict, key): Get an attribute/key from an object. Some keywords
+    """Get an attribute/key from an object. Some keywords
     are complex types. This function allows you to obtain the value of an
     attribute on these types."""
     if len(args) != 2:
@@ -434,8 +462,9 @@  def get(context, mapping, args):
     key = args[1][0](context, mapping, args[1][1])
     yield dictarg.get(key)
 
+@templatefunc('if(expr, then[, else])')
 def if_(context, mapping, args):
-    """:if(expr, then[, else]): Conditionally execute based on the result of
+    """Conditionally execute based on the result of
     an expression."""
     if not (2 <= len(args) <= 3):
         # i18n: "if" is a keyword
@@ -447,8 +476,9 @@  def if_(context, mapping, args):
     elif len(args) == 3:
         yield args[2][0](context, mapping, args[2][1])
 
+@templatefunc('ifcontains(search, thing, then[, else])')
 def ifcontains(context, mapping, args):
-    """:ifcontains(search, thing, then[, else]): Conditionally execute based
+    """Conditionally execute based
     on whether the item "search" is in "thing"."""
     if not (3 <= len(args) <= 4):
         # i18n: "ifcontains" is a keyword
@@ -462,8 +492,9 @@  def ifcontains(context, mapping, args):
     elif len(args) == 4:
         yield args[3][0](context, mapping, args[3][1])
 
+@templatefunc('ifeq(expr1, expr2, then[, else])')
 def ifeq(context, mapping, args):
-    """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on
+    """Conditionally execute based on
     whether 2 items are equivalent."""
     if not (3 <= len(args) <= 4):
         # i18n: "ifeq" is a keyword
@@ -476,8 +507,9 @@  def ifeq(context, mapping, args):
     elif len(args) == 4:
         yield args[3][0](context, mapping, args[3][1])
 
+@templatefunc('join(list, sep)')
 def join(context, mapping, args):
-    """:join(list, sep): Join items in a list with a delimiter."""
+    """Join items in a list with a delimiter."""
     if not (1 <= len(args) <= 2):
         # i18n: "join" is a keyword
         raise error.ParseError(_("join expects one or two arguments"))
@@ -499,8 +531,9 @@  def join(context, mapping, args):
             yield joiner
         yield x
 
+@templatefunc('label(label, expr)')
 def label(context, mapping, args):
-    """:label(label, expr): Apply a label to generated content. Content with
+    """Apply a label to generated content. Content with
     a label applied can result in additional post-processing, such as
     automatic colorization."""
     if len(args) != 2:
@@ -510,8 +543,9 @@  def label(context, mapping, args):
     # ignore args[0] (the label string) since this is supposed to be a a no-op
     yield args[1][0](context, mapping, args[1][1])
 
+@templatefunc('latesttag([pattern])')
 def latesttag(context, mapping, args):
-    """:latesttag([pattern]): The global tags matching the given pattern on the
+    """The global tags matching the given pattern on the
     most recent globally tagged ancestor of this changeset."""
     if len(args) > 1:
         # i18n: "latesttag" is a keyword
@@ -523,8 +557,9 @@  def latesttag(context, mapping, args):
 
     return templatekw.showlatesttags(pattern, **mapping)
 
+@templatefunc('localdate(date[, tz])')
 def localdate(context, mapping, args):
-    """:localdate(date[, tz]): Converts a date to the specified timezone.
+    """Converts a date to the specified timezone.
     The default is local date."""
     if not (1 <= len(args) <= 2):
         # i18n: "localdate" is a keyword
@@ -551,8 +586,9 @@  def localdate(context, mapping, args):
         tzoffset = util.makedate()[1]
     return (date[0], tzoffset)
 
+@templatefunc('revset(query[, formatargs...])')
 def revset(context, mapping, args):
-    """:revset(query[, formatargs...]): Execute a revision set query. See
+    """Execute a revision set query. See
     :hg:`help revset`."""
     if not len(args) > 0:
         # i18n: "revset" is a keyword
@@ -581,8 +617,9 @@  def revset(context, mapping, args):
 
     return templatekw.showrevslist("revision", revs, **mapping)
 
+@templatefunc('rstdoc(text, style)')
 def rstdoc(context, mapping, args):
-    """:rstdoc(text, style): Format ReStructuredText."""
+    """Format ReStructuredText."""
     if len(args) != 2:
         # i18n: "rstdoc" is a keyword
         raise error.ParseError(_("rstdoc expects two arguments"))
@@ -592,8 +629,9 @@  def rstdoc(context, mapping, args):
 
     return minirst.format(text, style=style, keep=['verbose'])
 
+@templatefunc('shortest(node, minlength=4)')
 def shortest(context, mapping, args):
-    """:shortest(node, minlength=4): Obtain the shortest representation of
+    """Obtain the shortest representation of
     a node."""
     if not (1 <= len(args) <= 2):
         # i18n: "shortest" is a keyword
@@ -644,8 +682,9 @@  def shortest(context, mapping, args):
             if len(shortest) <= length:
                 return shortest
 
+@templatefunc('strip(text[, chars])')
 def strip(context, mapping, args):
-    """:strip(text[, chars]): Strip characters from a string. By default,
+    """Strip characters from a string. By default,
     strips all leading and trailing whitespace."""
     if not (1 <= len(args) <= 2):
         # i18n: "strip" is a keyword
@@ -657,8 +696,9 @@  def strip(context, mapping, args):
         return text.strip(chars)
     return text.strip()
 
+@templatefunc('sub(pattern, replacement, expression)')
 def sub(context, mapping, args):
-    """:sub(pattern, replacement, expression): Perform text substitution
+    """Perform text substitution
     using regular expressions."""
     if len(args) != 3:
         # i18n: "sub" is a keyword
@@ -678,8 +718,9 @@  def sub(context, mapping, args):
         # i18n: "sub" is a keyword
         raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
 
+@templatefunc('startswith(pattern, text)')
 def startswith(context, mapping, args):
-    """:startswith(pattern, text): Returns the value from the "text" argument
+    """Returns the value from the "text" argument
     if it begins with the content from the "pattern" argument."""
     if len(args) != 2:
         # i18n: "startswith" is a keyword
@@ -691,9 +732,9 @@  def startswith(context, mapping, args):
         return text
     return ''
 
-
+@templatefunc('word(number, text[, separator])')
 def word(context, mapping, args):
-    """:word(number, text[, separator]): Return the nth word from a string."""
+    """Return the nth word from a string."""
     if not (2 <= len(args) <= 3):
         # i18n: "word" is a keyword
         raise error.ParseError(_("word expects two or three arguments, got %d")
@@ -733,29 +774,6 @@  exprmethods = {
 methods = exprmethods.copy()
 methods["integer"] = exprmethods["symbol"]  # '{1}' as variable
 
-funcs = {
-    "date": date,
-    "diff": diff,
-    "fill": fill,
-    "get": get,
-    "if": if_,
-    "ifcontains": ifcontains,
-    "ifeq": ifeq,
-    "indent": indent,
-    "join": join,
-    "label": label,
-    "latesttag": latesttag,
-    "localdate": localdate,
-    "pad": pad,
-    "revset": revset,
-    "rstdoc": rstdoc,
-    "shortest": shortest,
-    "startswith": startswith,
-    "strip": strip,
-    "sub": sub,
-    "word": word,
-}
-
 # template engine
 
 stringify = templatefilters.stringify