templater: abstract truth testing to fix {if(list_of_empty_strings)}
authorYuya Nishihara <yuya@tcha.org>
Sat, 09 Jun 2018 13:34:47 +0900
changeset 38301 f9c426385853
parent 38300 a9de1d28681c
child 38302 b5651ae53127
templater: abstract truth testing to fix {if(list_of_empty_strings)} Non-empty list should always be True even if it's stringified to ''. Spotted by Martin von Zweigbergk.
mercurial/hgweb/webutil.py
mercurial/templateutil.py
tests/test-command-template.t
--- a/mercurial/hgweb/webutil.py	Tue Jun 12 23:17:38 2018 +0900
+++ b/mercurial/hgweb/webutil.py	Sat Jun 09 13:34:47 2018 +0900
@@ -743,6 +743,9 @@
     def show(self, context, mapping):
         return self.join(context, '')
 
+    def tobool(self, context, mapping):
+        return bool(self._vars)
+
     def tovalue(self, context, mapping):
         return self._vars
 
--- a/mercurial/templateutil.py	Tue Jun 12 23:17:38 2018 +0900
+++ b/mercurial/templateutil.py	Sat Jun 09 13:34:47 2018 +0900
@@ -85,6 +85,10 @@
         """
 
     @abc.abstractmethod
+    def tobool(self, context, mapping):
+        """Return a boolean representation of the inner value"""
+
+    @abc.abstractmethod
     def tovalue(self, context, mapping):
         """Move the inner value object out or create a value representation
 
@@ -136,6 +140,9 @@
     def show(self, context, mapping):
         return self._value
 
+    def tobool(self, context, mapping):
+        return bool(self._value)
+
     def tovalue(self, context, mapping):
         return self._value
 
@@ -169,6 +176,14 @@
             return b''
         return pycompat.bytestr(self._value)
 
+    def tobool(self, context, mapping):
+        if self._value is None:
+            return False
+        if isinstance(self._value, bool):
+            return self._value
+        # otherwise evaluate as string, which means 0 is True
+        return bool(pycompat.bytestr(self._value))
+
     def tovalue(self, context, mapping):
         return self._value
 
@@ -201,6 +216,9 @@
     def tomap(self, context):
         return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset}
 
+    def tobool(self, context, mapping):
+        return True
+
     def tovalue(self, context, mapping):
         return (self._unixtime, self._tzoffset)
 
@@ -272,6 +290,9 @@
             return gen()
         return gen
 
+    def tobool(self, context, mapping):
+        return bool(self._values)
+
     def tovalue(self, context, mapping):
         # TODO: make it non-recursive for trivial lists/dicts
         xs = self._values
@@ -327,6 +348,9 @@
             return gen()
         return gen
 
+    def tobool(self, context, mapping):
+        return bool(self.tovalue(context, mapping))
+
     def tovalue(self, context, mapping):
         return _unthunk(context, mapping, self._value)
 
@@ -396,6 +420,9 @@
     def itermaps(self, context):
         return self._make(context, *self._args)
 
+    def tobool(self, context, mapping):
+        return _nonempty(self.itermaps(context))
+
 class mappinglist(_mappingsequence):
     """Wrapper for list of template mappings"""
 
@@ -406,6 +433,9 @@
     def itermaps(self, context):
         return iter(self._mappings)
 
+    def tobool(self, context, mapping):
+        return bool(self._mappings)
+
 class mappedgenerator(wrapped):
     """Wrapper for generator of strings which acts as a list
 
@@ -449,6 +479,9 @@
     def show(self, context, mapping):
         return self.join(context, mapping, '')
 
+    def tobool(self, context, mapping):
+        return _nonempty(self._gen(context))
+
     def tovalue(self, context, mapping):
         return [stringify(context, mapping, x) for x in self._gen(context)]
 
@@ -607,6 +640,13 @@
         else:
             return None
 
+def _nonempty(xiter):
+    try:
+        next(xiter)
+        return True
+    except StopIteration:
+        return False
+
 def _unthunk(context, mapping, thing):
     """Evaluate a lazy byte string into value"""
     if not isinstance(thing, types.GeneratorType):
@@ -655,13 +695,7 @@
             thing = stringutil.parsebool(data)
     else:
         thing = func(context, mapping, data)
-    if isinstance(thing, wrapped):
-        thing = thing.tovalue(context, mapping)
-    if isinstance(thing, bool):
-        return thing
-    # other objects are evaluated as strings, which means 0 is True, but
-    # empty dict/list should be False as they are expected to be ''
-    return bool(stringify(context, mapping, thing))
+    return makewrapped(context, mapping, thing).tobool(context, mapping)
 
 def evaldate(context, mapping, arg, err=None):
     """Evaluate given argument as a date tuple or a date string; returns
--- a/tests/test-command-template.t	Tue Jun 12 23:17:38 2018 +0900
+++ b/tests/test-command-template.t	Sat Jun 09 13:34:47 2018 +0900
@@ -4151,6 +4151,10 @@
   empty string is False
   $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
   empty list is False
+  $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
+  non-empty list is True
+  $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
+  list of empty strings is True
   $ hg log -r 0 -T '{if(true, "true is True")}\n'
   true is True
   $ hg log -r 0 -T '{if(false, "", "false is False")}\n'