Patchwork [4,of,5] templater: promote getmember() to an interface of wrapped types

login
register
mail settings
Submitter Yuya Nishihara
Date June 5, 2018, 1:18 p.m.
Message ID <c30ef16679c1fa6fead7.1528204738@mimosa>
Download mbox | patch
Permalink /patch/31984/
State Accepted
Headers show

Comments

Yuya Nishihara - June 5, 2018, 1:18 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1521599421 -32400
#      Wed Mar 21 11:30:21 2018 +0900
# Node ID c30ef16679c1fa6fead795a0ccadd7dced41f408
# Parent  8d50ec6e8b832fec6b9dc42cd1d7be2ff53199b9
templater: promote getmember() to an interface of wrapped types

Patch

diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -713,6 +713,9 @@  class sessionvars(templateutil.wrapped):
     def __copy__(self):
         return sessionvars(copy.copy(self._vars), self._start)
 
+    def getmember(self, context, mapping, key):
+        return self._vars.get(key)
+
     def itermaps(self, context):
         separator = self._start
         for key, value in sorted(self._vars.iteritems()):
diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -262,12 +262,13 @@  def get(context, mapping, args):
         raise error.ParseError(_("get() expects two arguments"))
 
     dictarg = evalwrapped(context, mapping, args[0])
-    if not util.safehasattr(dictarg, 'getmember'):
+    key = evalfuncarg(context, mapping, args[1])
+    try:
+        return dictarg.getmember(context, mapping, key)
+    except error.ParseError as err:
         # i18n: "get" is a keyword
-        raise error.ParseError(_("get() expects a dict as first argument"))
-
-    key = evalfuncarg(context, mapping, args[1])
-    return dictarg.getmember(context, mapping, key)
+        hint = _("get() expects a dict as first argument")
+        raise error.ParseError(bytes(err), hint=hint)
 
 @templatefunc('if(expr, then[, else])')
 def if_(context, mapping, args):
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -38,6 +38,14 @@  class wrapped(object):
     __metaclass__ = abc.ABCMeta
 
     @abc.abstractmethod
+    def getmember(self, context, mapping, key):
+        """Return a member item for the specified key
+
+        A returned object may be either a wrapped object or a pure value
+        depending on the self type.
+        """
+
+    @abc.abstractmethod
     def itermaps(self, context):
         """Yield each template mapping"""
 
@@ -72,6 +80,10 @@  class wrappedbytes(wrapped):
     def __init__(self, value):
         self._value = value
 
+    def getmember(self, context, mapping, key):
+        raise error.ParseError(_('%r is not a dictionary')
+                               % pycompat.bytestr(self._value))
+
     def itermaps(self, context):
         raise error.ParseError(_('%r is not iterable of mappings')
                                % pycompat.bytestr(self._value))
@@ -91,6 +103,9 @@  class wrappedvalue(wrapped):
     def __init__(self, value):
         self._value = value
 
+    def getmember(self, context, mapping, key):
+        raise error.ParseError(_('%r is not a dictionary') % self._value)
+
     def itermaps(self, context):
         raise error.ParseError(_('%r is not iterable of mappings')
                                % self._value)
@@ -196,6 +211,10 @@  class mappable(wrapped):
     def tomap(self):
         return self._makemap(self._key)
 
+    def getmember(self, context, mapping, key):
+        w = makewrapped(context, mapping, self._value)
+        return w.getmember(context, mapping, key)
+
     def itermaps(self, context):
         yield self.tomap()
 
@@ -231,6 +250,9 @@  class _mappingsequence(wrapped):
         self._tmpl = tmpl
         self._defaultsep = sep
 
+    def getmember(self, context, mapping, key):
+        raise error.ParseError(_('not a dictionary'))
+
     def join(self, context, mapping, sep):
         mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
         if self._name:
@@ -294,6 +316,9 @@  class mappedgenerator(wrapped):
     def _gen(self, context):
         return self._make(context, *self._args)
 
+    def getmember(self, context, mapping, key):
+        raise error.ParseError(_('not a dictionary'))
+
     def itermaps(self, context):
         raise error.ParseError(_('list of strings is not mappable'))
 
@@ -678,15 +703,13 @@  def runmember(context, mapping, data):
         lm = context.overlaymap(mapping, d.tomap())
         return runsymbol(context, lm, memb)
     try:
-        if util.safehasattr(d, 'getmember'):
-            return d.getmember(context, mapping, memb)
-        raise error.ParseError
-    except error.ParseError:
+        return d.getmember(context, mapping, memb)
+    except error.ParseError as err:
         sym = findsymbolicname(darg)
-        if sym:
-            raise error.ParseError(_("keyword '%s' has no member") % sym)
-        else:
-            raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
+        if not sym:
+            raise
+        hint = _("keyword '%s' does not support member operation") % sym
+        raise error.ParseError(bytes(err), hint=hint)
 
 def runnegate(context, mapping, data):
     data = evalinteger(context, mapping, data,
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
@@ -3345,10 +3345,11 @@  Test evaluation of dot operator:
   default
 
   $ hg log -R latesttag -l1 -T '{author.invalid}\n'
-  hg: parse error: keyword 'author' has no member
+  hg: parse error: 'test' is not a dictionary
+  (keyword 'author' does not support member operation)
   [255]
   $ hg log -R latesttag -l1 -T '{min("abc").invalid}\n'
-  hg: parse error: 'a' has no member
+  hg: parse error: 'a' is not a dictionary
   [255]
 
 Test the sub function of templating for expansion:
@@ -3851,7 +3852,8 @@  Test get function:
   $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
   default
   $ hg log -r 0 --template '{get(files, "should_fail")}\n'
-  hg: parse error: get() expects a dict as first argument
+  hg: parse error: not a dictionary
+  (get() expects a dict as first argument)
   [255]
 
 Test json filter applied to hybrid object: