Patchwork [2,of,4] templater: introduce filter() function to remove empty items from list

login
register
mail settings
Submitter Yuya Nishihara
Date June 23, 2018, 9:51 a.m.
Message ID <f14965009644ab63cbe1.1529747499@mimosa>
Download mbox | patch
Permalink /patch/32389/
State New
Headers show

Comments

Yuya Nishihara - June 23, 2018, 9:51 a.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1528983206 -32400
#      Thu Jun 14 22:33:26 2018 +0900
# Node ID f14965009644ab63cbe1990081493e8d490a30eb
# Parent  720fc7b592ef6dd5dc11a7ec48a715ee39c1006a
templater: introduce filter() function to remove empty items from list

The primary use case is to filter out "tip" from a list of tags.

Patch

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'