# HG changeset patch # User Yuya Nishihara # Date 2018-06-14 13:33:26 # Node ID dae829b4de78bd8331bb8ce802ae733919d22c59 # Parent b6294c113794794b6379d8520aeca86208f27658 templater: introduce filter() function to remove empty items from list The primary use case is to filter out "tip" from a list of tags. diff --git a/mercurial/hgweb/webutil.py b/mercurial/hgweb/webutil.py --- a/mercurial/hgweb/webutil.py +++ b/mercurial/hgweb/webutil.py @@ -727,6 +727,10 @@ class sessionvars(templateutil.wrapped): def getmax(self, context, mapping): raise error.ParseError(_('not comparable')) + def filter(self, context, mapping, select): + # implement if necessary + raise error.ParseError(_('not filterable')) + 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 @@ -166,6 +166,17 @@ def fill(context, mapping, args): return templatefilters.fill(text, width, initindent, hangindent) +@templatefunc('filter(iterable)') +def filter_(context, mapping, args): + """Remove empty elements from a list or a dict.""" + if len(args) != 1: + # i18n: "filter" is a keyword + raise error.ParseError(_("filter expects one argument")) + iterable = evalwrapped(context, mapping, args[0]) + def select(w): + return w.tobool(context, mapping) + return iterable.filter(context, mapping, select) + @templatefunc('formatnode(node)', requires={'ui'}) def formatnode(context, mapping, args): """Obtain the preferred form of a changeset hash. (DEPRECATED)""" diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py --- a/mercurial/templateutil.py +++ b/mercurial/templateutil.py @@ -63,6 +63,14 @@ class wrapped(object): value depending on the self type""" @abc.abstractmethod + def filter(self, context, mapping, select): + """Return new container of the same type which includes only the + selected elements + + select() takes each item as a wrapped object and returns True/False. + """ + + @abc.abstractmethod def itermaps(self, context): """Yield each template mapping""" @@ -130,6 +138,10 @@ class wrappedbytes(wrapped): raise error.ParseError(_('empty string')) return func(pycompat.iterbytestr(self._value)) + def filter(self, context, mapping, select): + raise error.ParseError(_('%r is not filterable') + % pycompat.bytestr(self._value)) + def itermaps(self, context): raise error.ParseError(_('%r is not iterable of mappings') % pycompat.bytestr(self._value)) @@ -164,6 +176,9 @@ class wrappedvalue(wrapped): def getmax(self, context, mapping): raise error.ParseError(_("%r is not iterable") % self._value) + def filter(self, context, mapping, select): + raise error.ParseError(_("%r is not iterable") % self._value) + def itermaps(self, context): raise error.ParseError(_('%r is not iterable of mappings') % self._value) @@ -208,6 +223,9 @@ class date(mappable, wrapped): def getmax(self, context, mapping): raise error.ParseError(_('date is not iterable')) + def filter(self, context, mapping, select): + raise error.ParseError(_('date is not iterable')) + def join(self, context, mapping, sep): raise error.ParseError(_("date is not iterable")) @@ -273,6 +291,14 @@ class hybrid(wrapped): return val return hybriditem(None, key, val, self._makemap) + def filter(self, context, mapping, select): + if util.safehasattr(self._values, 'get'): + values = {k: v for k, v in self._values.iteritems() + if select(self._wrapvalue(k, v))} + else: + values = [v for v in self._values if select(self._wrapvalue(v, v))] + return hybrid(None, values, self._makemap, self._joinfmt, self._keytype) + def itermaps(self, context): makemap = self._makemap for x in self._values: @@ -336,6 +362,10 @@ class hybriditem(mappable, wrapped): w = makewrapped(context, mapping, self._value) return w.getmax(context, mapping) + def filter(self, context, mapping, select): + w = makewrapped(context, mapping, self._value) + return w.filter(context, mapping, select) + def join(self, context, mapping, sep): w = makewrapped(context, mapping, self._value) return w.join(context, mapping, sep) @@ -384,6 +414,9 @@ class _mappingsequence(wrapped): def getmax(self, context, mapping): raise error.ParseError(_('not comparable')) + def filter(self, context, mapping, select): + raise error.ParseError(_('not filterable without template')) + def join(self, context, mapping, sep): mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context)) if self._name: @@ -472,6 +505,17 @@ class mappedgenerator(wrapped): raise error.ParseError(_('empty sequence')) return func(xs) + @staticmethod + def _filteredgen(context, mapping, make, args, select): + for x in make(context, *args): + s = stringify(context, mapping, x) + if select(wrappedbytes(s)): + yield s + + def filter(self, context, mapping, select): + args = (mapping, self._make, self._args, select) + return mappedgenerator(self._filteredgen, args) + def itermaps(self, context): raise error.ParseError(_('list of strings is not mappable')) diff --git a/tests/test-template-functions.t b/tests/test-template-functions.t --- a/tests/test-template-functions.t +++ b/tests/test-template-functions.t @@ -435,6 +435,48 @@ latesttag() function: $ cd .. +Test filter() empty values: + + $ hg log -R a -r 1 -T '{filter(desc|splitlines) % "{line}\n"}' + other 1 + other 2 + other 3 + $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1) % "{ifeq(key, "a", "{value}\n")}")}' + 0 + + 0 should not be falsy + + $ hg log -R a -r 0 -T '{filter(revset("0:2"))}\n' + 0 1 2 + +Test filter() shouldn't crash: + + $ hg log -R a -r 0 -T '{filter(extras)}\n' + branch=default + $ hg log -R a -r 0 -T '{filter(files)}\n' + a + +Test filter() unsupported arguments: + + $ hg log -R a -r 0 -T '{filter()}\n' + hg: parse error: filter expects one argument + [255] + $ hg log -R a -r 0 -T '{filter(date)}\n' + hg: parse error: date is not iterable + [255] + $ hg log -R a -r 0 -T '{filter(rev)}\n' + hg: parse error: 0 is not iterable + [255] + $ hg log -R a -r 0 -T '{filter(desc|firstline)}\n' + hg: parse error: 'line 1' is not filterable + [255] + $ hg log -R a -r 0 -T '{filter(manifest)}\n' + hg: parse error: '0:a0c8bcbbb45c' is not filterable + [255] + $ hg log -R a -r 0 -T '{filter(succsandmarkers)}\n' + hg: parse error: not filterable without template + [255] + Test manifest/get() can be join()-ed as string, though it's silly: $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'