templater: promote getmember() to an interface of wrapped types
authorYuya Nishihara <yuya@tcha.org>
Wed, 21 Mar 2018 11:30:21 +0900
changeset 38243 06d11cd90516
parent 38242 12b6ee9e88f3
child 38244 688fbb758ba9
templater: promote getmember() to an interface of wrapped types
mercurial/hgweb/webutil.py
mercurial/templatefuncs.py
mercurial/templateutil.py
tests/test-command-template.t
--- a/mercurial/hgweb/webutil.py	Wed Mar 21 01:39:44 2018 +0900
+++ b/mercurial/hgweb/webutil.py	Wed Mar 21 11:30:21 2018 +0900
@@ -713,6 +713,9 @@
     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()):
--- a/mercurial/templatefuncs.py	Wed Mar 21 01:39:44 2018 +0900
+++ b/mercurial/templatefuncs.py	Wed Mar 21 11:30:21 2018 +0900
@@ -262,12 +262,13 @@
         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):
--- a/mercurial/templateutil.py	Wed Mar 21 01:39:44 2018 +0900
+++ b/mercurial/templateutil.py	Wed Mar 21 11:30:21 2018 +0900
@@ -38,6 +38,14 @@
     __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 @@
     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 @@
     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 @@
     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 @@
         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 @@
     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 @@
         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,
--- a/tests/test-command-template.t	Wed Mar 21 01:39:44 2018 +0900
+++ b/tests/test-command-template.t	Wed Mar 21 11:30:21 2018 +0900
@@ -3345,10 +3345,11 @@
   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 @@
   $ 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: