changeset 38265:41ae9b3cbfb9

templater: abstract min/max away I'm not certain how many get*() functions I'll add to the wrapped types, but getmin() and getmax() will allow us to optimize a revset wrapper.
author Yuya Nishihara <yuya@tcha.org>
date Mon, 19 Mar 2018 00:16:12 +0900
parents fbb2eddea4d2
children 80f423a14c90
files mercurial/hgweb/webutil.py mercurial/templatefuncs.py mercurial/templateutil.py tests/test-command-template.t
diffstat 4 files changed, 126 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/hgweb/webutil.py	Sun Jun 10 12:24:53 2018 +0900
+++ b/mercurial/hgweb/webutil.py	Mon Mar 19 00:16:12 2018 +0900
@@ -717,6 +717,12 @@
         key = templateutil.unwrapvalue(context, mapping, key)
         return self._vars.get(key)
 
+    def getmin(self, context, mapping):
+        raise error.ParseError(_('not comparable'))
+
+    def getmax(self, context, mapping):
+        raise error.ParseError(_('not comparable'))
+
     def itermaps(self, context):
         separator = self._start
         for key, value in sorted(self._vars.iteritems()):
--- a/mercurial/templatefuncs.py	Sun Jun 10 12:24:53 2018 +0900
+++ b/mercurial/templatefuncs.py	Mon Mar 19 00:16:12 2018 +0900
@@ -20,7 +20,6 @@
     error,
     minirst,
     obsutil,
-    pycompat,
     registrar,
     revset as revsetmod,
     revsetlang,
@@ -404,13 +403,13 @@
         # i18n: "max" is a keyword
         raise error.ParseError(_("max expects one argument"))
 
-    iterable = evalfuncarg(context, mapping, args[0])
+    iterable = evalwrapped(context, mapping, args[0])
     try:
-        x = max(pycompat.maybebytestr(iterable))
-    except (TypeError, ValueError):
+        return iterable.getmax(context, mapping)
+    except error.ParseError as err:
         # i18n: "max" is a keyword
-        raise error.ParseError(_("max first argument should be an iterable"))
-    return templateutil.wraphybridvalue(iterable, x, x)
+        hint = _("max first argument should be an iterable")
+        raise error.ParseError(bytes(err), hint=hint)
 
 @templatefunc('min(iterable)')
 def min_(context, mapping, args, **kwargs):
@@ -419,13 +418,13 @@
         # i18n: "min" is a keyword
         raise error.ParseError(_("min expects one argument"))
 
-    iterable = evalfuncarg(context, mapping, args[0])
+    iterable = evalwrapped(context, mapping, args[0])
     try:
-        x = min(pycompat.maybebytestr(iterable))
-    except (TypeError, ValueError):
+        return iterable.getmin(context, mapping)
+    except error.ParseError as err:
         # i18n: "min" is a keyword
-        raise error.ParseError(_("min first argument should be an iterable"))
-    return templateutil.wraphybridvalue(iterable, x, x)
+        hint = _("min first argument should be an iterable")
+        raise error.ParseError(bytes(err), hint=hint)
 
 @templatefunc('mod(a, b)')
 def mod(context, mapping, args):
--- a/mercurial/templateutil.py	Sun Jun 10 12:24:53 2018 +0900
+++ b/mercurial/templateutil.py	Mon Mar 19 00:16:12 2018 +0900
@@ -47,6 +47,16 @@
         """
 
     @abc.abstractmethod
+    def getmin(self, context, mapping):
+        """Return the smallest item, which may be either a wrapped or a pure
+        value depending on the self type"""
+
+    @abc.abstractmethod
+    def getmax(self, context, mapping):
+        """Return the largest item, which may be either a wrapped or a pure
+        value depending on the self type"""
+
+    @abc.abstractmethod
     def itermaps(self, context):
         """Yield each template mapping"""
 
@@ -85,6 +95,17 @@
         raise error.ParseError(_('%r is not a dictionary')
                                % pycompat.bytestr(self._value))
 
+    def getmin(self, context, mapping):
+        return self._getby(context, mapping, min)
+
+    def getmax(self, context, mapping):
+        return self._getby(context, mapping, max)
+
+    def _getby(self, context, mapping, func):
+        if not self._value:
+            raise error.ParseError(_('empty string'))
+        return func(pycompat.iterbytestr(self._value))
+
     def itermaps(self, context):
         raise error.ParseError(_('%r is not iterable of mappings')
                                % pycompat.bytestr(self._value))
@@ -107,6 +128,12 @@
     def getmember(self, context, mapping, key):
         raise error.ParseError(_('%r is not a dictionary') % self._value)
 
+    def getmin(self, context, mapping):
+        raise error.ParseError(_("%r is not iterable") % self._value)
+
+    def getmax(self, context, mapping):
+        raise error.ParseError(_("%r is not iterable") % self._value)
+
     def itermaps(self, context):
         raise error.ParseError(_('%r is not iterable of mappings')
                                % self._value)
@@ -151,6 +178,18 @@
         key = unwrapastype(context, mapping, key, self.keytype)
         return self._wrapvalue(key, self._values.get(key))
 
+    def getmin(self, context, mapping):
+        return self._getby(context, mapping, min)
+
+    def getmax(self, context, mapping):
+        return self._getby(context, mapping, max)
+
+    def _getby(self, context, mapping, func):
+        if not self._values:
+            raise error.ParseError(_('empty sequence'))
+        val = func(self._values)
+        return self._wrapvalue(val, val)
+
     def _wrapvalue(self, key, val):
         if val is None:
             return
@@ -217,6 +256,14 @@
         w = makewrapped(context, mapping, self._value)
         return w.getmember(context, mapping, key)
 
+    def getmin(self, context, mapping):
+        w = makewrapped(context, mapping, self._value)
+        return w.getmin(context, mapping)
+
+    def getmax(self, context, mapping):
+        w = makewrapped(context, mapping, self._value)
+        return w.getmax(context, mapping)
+
     def itermaps(self, context):
         yield self.tomap()
 
@@ -255,6 +302,12 @@
     def getmember(self, context, mapping, key):
         raise error.ParseError(_('not a dictionary'))
 
+    def getmin(self, context, mapping):
+        raise error.ParseError(_('not comparable'))
+
+    def getmax(self, context, mapping):
+        raise error.ParseError(_('not comparable'))
+
     def join(self, context, mapping, sep):
         mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
         if self._name:
@@ -321,6 +374,18 @@
     def getmember(self, context, mapping, key):
         raise error.ParseError(_('not a dictionary'))
 
+    def getmin(self, context, mapping):
+        return self._getby(context, mapping, min)
+
+    def getmax(self, context, mapping):
+        return self._getby(context, mapping, max)
+
+    def _getby(self, context, mapping, func):
+        xs = self.tovalue(context, mapping)
+        if not xs:
+            raise error.ParseError(_('empty sequence'))
+        return func(xs)
+
     def itermaps(self, context):
         raise error.ParseError(_('list of strings is not mappable'))
 
--- a/tests/test-command-template.t	Sun Jun 10 12:24:53 2018 +0900
+++ b/tests/test-command-template.t	Mon Mar 19 00:16:12 2018 +0900
@@ -3274,6 +3274,51 @@
   $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
   t3
 
+Test min/max of strings:
+
+  $ hg log -R latesttag -l1 -T '{min(desc)}\n'
+  3
+  $ hg log -R latesttag -l1 -T '{max(desc)}\n'
+  t
+
+Test min/max of non-iterable:
+
+  $ hg debugtemplate '{min(1)}'
+  hg: parse error: 1 is not iterable
+  (min first argument should be an iterable)
+  [255]
+  $ hg debugtemplate '{max(2)}'
+  hg: parse error: 2 is not iterable
+  (max first argument should be an iterable)
+  [255]
+
+Test min/max of empty sequence:
+
+  $ hg debugtemplate '{min("")}'
+  hg: parse error: empty string
+  (min first argument should be an iterable)
+  [255]
+  $ hg debugtemplate '{max("")}'
+  hg: parse error: empty string
+  (max first argument should be an iterable)
+  [255]
+  $ hg debugtemplate '{min(dict())}'
+  hg: parse error: empty sequence
+  (min first argument should be an iterable)
+  [255]
+  $ hg debugtemplate '{max(dict())}'
+  hg: parse error: empty sequence
+  (max first argument should be an iterable)
+  [255]
+  $ hg debugtemplate '{min(dict() % "")}'
+  hg: parse error: empty sequence
+  (min first argument should be an iterable)
+  [255]
+  $ hg debugtemplate '{max(dict() % "")}'
+  hg: parse error: empty sequence
+  (max first argument should be an iterable)
+  [255]
+
 Test min/max of if() result
 
   $ cd latesttag