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