Patchwork [1,of,6] templater: abstract min/max away

login
register
mail settings
Submitter Yuya Nishihara
Date June 8, 2018, 2:51 p.m.
Message ID <9fcd58363baccf1e3f82.1528469507@mimosa>
Download mbox | patch
Permalink /patch/32029/
State Accepted
Headers show

Comments

Yuya Nishihara - June 8, 2018, 2:51 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1521386172 -32400
#      Mon Mar 19 00:16:12 2018 +0900
# Node ID 9fcd58363baccf1e3f828b64e7cc9d110ff65fb6
# Parent  7f94ae59fcad67ba6754ba0b7164cb6891f3e3a8
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.

Patch

diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py
--- a/mercurial/hgweb/webutil.py
+++ b/mercurial/hgweb/webutil.py
@@ -717,6 +717,12 @@  class sessionvars(templateutil.wrapped):
         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()):
diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -20,7 +20,6 @@  from . import (
     error,
     minirst,
     obsutil,
-    pycompat,
     registrar,
     revset as revsetmod,
     revsetlang,
@@ -404,13 +403,13 @@  def max_(context, mapping, args, **kwarg
         # 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 @@  def min_(context, mapping, args, **kwarg
         # 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):
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -47,6 +47,16 @@  class wrapped(object):
         """
 
     @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 @@  class wrappedbytes(wrapped):
         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 @@  class wrappedvalue(wrapped):
     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 @@  class hybrid(wrapped):
         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 @@  class mappable(wrapped):
         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 @@  class _mappingsequence(wrapped):
     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 @@  class mappedgenerator(wrapped):
     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'))
 
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
@@ -3274,6 +3274,51 @@  Test min/max over map operation:
   $ 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