Patchwork [2,of,2] templatefuncs: specialize "no match" value of search() to allow % operation

login
register
mail settings
Submitter Yuya Nishihara
Date Dec. 13, 2018, 1:35 p.m.
Message ID <d64fec2ec72303c86d51.1544708117@mimosa>
Download mbox | patch
Permalink /patch/37134/
State Accepted
Headers show

Comments

Yuya Nishihara - Dec. 13, 2018, 1:35 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1544622302 -32400
#      Wed Dec 12 22:45:02 2018 +0900
# Node ID d64fec2ec72303c86d51f342a7b8d22143e069d4
# Parent  eeb0c3a62092414713689e9dce6e8949bd27692b
templatefuncs: specialize "no match" value of search() to allow % operation

If Python had Maybe or Option, the type of the search() result would be
Option<Mapping>, which can be considered as a 0/1 container of a Mapping.
So it makes sense that {search(r'no match pattern', x) % "whatever"} is
mapped to an empty string.

Patch

diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -609,7 +609,7 @@  def search(context, mapping, args):
 
     match = patre.search(src)
     if not match:
-        return
+        return templateutil.mappingnone()
 
     lm = {b'0': match.group(0)}
     lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -53,6 +53,10 @@  mappingdict
     represents a single mapping (i.e. a dict), which may have default output
     format.
 
+mappingnone
+    represents None of Optional[mappable], which will be mapped to an empty
+    string by % operation.
+
 mappedgenerator
     a lazily-evaluated list of byte strings, which is e.g. a result of %
     operation.
diff --git a/mercurial/templateutil.py b/mercurial/templateutil.py
--- a/mercurial/templateutil.py
+++ b/mercurial/templateutil.py
@@ -495,6 +495,19 @@  class mappingdict(mappable, _mappingsequ
     def tovalue(self, context, mapping):
         return super(mappingdict, self).tovalue(context, mapping)[0]
 
+class mappingnone(wrappedvalue):
+    """Wrapper for None, but supports map operation
+
+    This represents None of Optional[mappable]. It's similar to
+    mapplinglist([]), but the underlying value is not [], but None.
+    """
+
+    def __init__(self):
+        super(mappingnone, self).__init__(None)
+
+    def itermaps(self, context):
+        return iter([])
+
 class mappedgenerator(wrapped):
     """Wrapper for generator of strings which acts as a list
 
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
@@ -632,11 +632,9 @@  Test search() function:
   no
 
  group reference with no match
- (TODO: we'll probably want to map it to an empty value)
 
   $ hg log -R a -r2 -T '{search(r"q", desc) % "match: {0}"}\n'
-  hg: parse error: None is not iterable of mappings
-  [255]
+  
 
  bad group names