mercurial/templater.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43089 c59eb1560c44
--- a/mercurial/templater.py	Sun Oct 06 09:45:02 2019 -0400
+++ b/mercurial/templater.py	Sun Oct 06 09:48:39 2019 -0400
@@ -85,22 +85,22 @@
 
 elements = {
     # token-type: binding-strength, primary, prefix, infix, suffix
-    "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
-    ".": (18, None, None, (".", 18), None),
-    "%": (15, None, None, ("%", 15), None),
-    "|": (15, None, None, ("|", 15), None),
-    "*": (5, None, None, ("*", 5), None),
-    "/": (5, None, None, ("/", 5), None),
-    "+": (4, None, None, ("+", 4), None),
-    "-": (4, None, ("negate", 19), ("-", 4), None),
-    "=": (3, None, None, ("keyvalue", 3), None),
-    ",": (2, None, None, ("list", 2), None),
-    ")": (0, None, None, None, None),
-    "integer": (0, "integer", None, None, None),
-    "symbol": (0, "symbol", None, None, None),
-    "string": (0, "string", None, None, None),
-    "template": (0, "template", None, None, None),
-    "end": (0, None, None, None, None),
+    b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
+    b".": (18, None, None, (b".", 18), None),
+    b"%": (15, None, None, (b"%", 15), None),
+    b"|": (15, None, None, (b"|", 15), None),
+    b"*": (5, None, None, (b"*", 5), None),
+    b"/": (5, None, None, (b"/", 5), None),
+    b"+": (4, None, None, (b"+", 4), None),
+    b"-": (4, None, (b"negate", 19), (b"-", 4), None),
+    b"=": (3, None, None, (b"keyvalue", 3), None),
+    b",": (2, None, None, (b"list", 2), None),
+    b")": (0, None, None, None, None),
+    b"integer": (0, b"integer", None, None, None),
+    b"symbol": (0, b"symbol", None, None, None),
+    b"string": (0, b"string", None, None, None),
+    b"template": (0, b"template", None, None, None),
+    b"end": (0, None, None, None, None),
 }
 
 
@@ -113,28 +113,28 @@
         c = program[pos]
         if c.isspace():  # skip inter-token whitespace
             pass
-        elif c in "(=,).%|+-*/":  # handle simple operators
+        elif c in b"(=,).%|+-*/":  # handle simple operators
             yield (c, None, pos)
-        elif c in '"\'':  # handle quoted templates
+        elif c in b'"\'':  # handle quoted templates
             s = pos + 1
             data, pos = _parsetemplate(program, s, end, c)
-            yield ('template', data, s)
+            yield (b'template', data, s)
             pos -= 1
-        elif c == 'r' and program[pos : pos + 2] in ("r'", 'r"'):
+        elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
             # handle quoted strings
             c = program[pos + 1]
             s = pos = pos + 2
             while pos < end:  # find closing quote
                 d = program[pos]
-                if d == '\\':  # skip over escaped characters
+                if d == b'\\':  # skip over escaped characters
                     pos += 2
                     continue
                 if d == c:
-                    yield ('string', program[s:pos], s)
+                    yield (b'string', program[s:pos], s)
                     break
                 pos += 1
             else:
-                raise error.ParseError(_("unterminated string"), s)
+                raise error.ParseError(_(b"unterminated string"), s)
         elif c.isdigit():
             s = pos
             while pos < end:
@@ -142,12 +142,12 @@
                 if not d.isdigit():
                     break
                 pos += 1
-            yield ('integer', program[s:pos], s)
+            yield (b'integer', program[s:pos], s)
             pos -= 1
         elif (
-            c == '\\'
+            c == b'\\'
             and program[pos : pos + 2] in (br"\'", br'\"')
-            or c == 'r'
+            or c == b'r'
             and program[pos : pos + 3] in (br"r\'", br'r\"')
         ):
             # handle escaped quoted strings for compatibility with 2.9.2-3.4,
@@ -160,51 +160,51 @@
             # {f("\\\\ {g(\"\\\"\")}"}    \\ {g("\"")}    [r'\\', {g("\"")}]
             #             ~~~~~~~~
             #             escaped quoted string
-            if c == 'r':
+            if c == b'r':
                 pos += 1
-                token = 'string'
+                token = b'string'
             else:
-                token = 'template'
+                token = b'template'
             quote = program[pos : pos + 2]
             s = pos = pos + 2
             while pos < end:  # find closing escaped quote
-                if program.startswith('\\\\\\', pos, end):
+                if program.startswith(b'\\\\\\', pos, end):
                     pos += 4  # skip over double escaped characters
                     continue
                 if program.startswith(quote, pos, end):
                     # interpret as if it were a part of an outer string
                     data = parser.unescapestr(program[s:pos])
-                    if token == 'template':
+                    if token == b'template':
                         data = _parsetemplate(data, 0, len(data))[0]
                     yield (token, data, s)
                     pos += 1
                     break
                 pos += 1
             else:
-                raise error.ParseError(_("unterminated string"), s)
-        elif c.isalnum() or c in '_':
+                raise error.ParseError(_(b"unterminated string"), s)
+        elif c.isalnum() or c in b'_':
             s = pos
             pos += 1
             while pos < end:  # find end of symbol
                 d = program[pos]
-                if not (d.isalnum() or d == "_"):
+                if not (d.isalnum() or d == b"_"):
                     break
                 pos += 1
             sym = program[s:pos]
-            yield ('symbol', sym, s)
+            yield (b'symbol', sym, s)
             pos -= 1
         elif c == term:
-            yield ('end', None, pos)
+            yield (b'end', None, pos)
             return
         else:
-            raise error.ParseError(_("syntax error"), pos)
+            raise error.ParseError(_(b"syntax error"), pos)
         pos += 1
     if term:
-        raise error.ParseError(_("unterminated template expansion"), start)
-    yield ('end', None, pos)
+        raise error.ParseError(_(b"unterminated template expansion"), start)
+    yield (b'end', None, pos)
 
 
-def _parsetemplate(tmpl, start, stop, quote=''):
+def _parsetemplate(tmpl, start, stop, quote=b''):
     r"""
     >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
     ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
@@ -219,15 +219,15 @@
     """
     parsed = []
     for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
-        if typ == 'string':
+        if typ == b'string':
             parsed.append((typ, val))
-        elif typ == 'template':
+        elif typ == b'template':
             parsed.append(val)
-        elif typ == 'end':
+        elif typ == b'end':
             return parsed, pos
         else:
-            raise error.ProgrammingError('unexpected type: %s' % typ)
-    raise error.ProgrammingError('unterminated scanning of template')
+            raise error.ProgrammingError(b'unexpected type: %s' % typ)
+    raise error.ProgrammingError(b'unterminated scanning of template')
 
 
 def scantemplate(tmpl, raw=False):
@@ -252,16 +252,16 @@
     for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
         if last:
             yield last + (pos,)
-        if typ == 'end':
+        if typ == b'end':
             return
         else:
             last = (typ, pos)
-    raise error.ProgrammingError('unterminated scanning of template')
+    raise error.ProgrammingError(b'unterminated scanning of template')
 
 
-def _scantemplate(tmpl, start, stop, quote='', raw=False):
+def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
     """Parse template string into chunks of strings and template expressions"""
-    sepchars = '{' + quote
+    sepchars = b'{' + quote
     unescape = [parser.unescapestr, pycompat.identity][raw]
     pos = start
     p = parser.parser(elements)
@@ -272,49 +272,49 @@
                 key=lambda n: (n < 0, n),
             )
             if n < 0:
-                yield ('string', unescape(tmpl[pos:stop]), pos)
+                yield (b'string', unescape(tmpl[pos:stop]), pos)
                 pos = stop
                 break
             c = tmpl[n : n + 1]
             bs = 0  # count leading backslashes
             if not raw:
-                bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
+                bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
             if bs % 2 == 1:
                 # escaped (e.g. '\{', '\\\{', but not '\\{')
-                yield ('string', unescape(tmpl[pos : n - 1]) + c, pos)
+                yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
                 pos = n + 1
                 continue
             if n > pos:
-                yield ('string', unescape(tmpl[pos:n]), pos)
+                yield (b'string', unescape(tmpl[pos:n]), pos)
             if c == quote:
-                yield ('end', None, n + 1)
+                yield (b'end', None, n + 1)
                 return
 
-            parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
-            if not tmpl.startswith('}', pos):
-                raise error.ParseError(_("invalid token"), pos)
-            yield ('template', parseres, n)
+            parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
+            if not tmpl.startswith(b'}', pos):
+                raise error.ParseError(_(b"invalid token"), pos)
+            yield (b'template', parseres, n)
             pos += 1
 
         if quote:
-            raise error.ParseError(_("unterminated string"), start)
+            raise error.ParseError(_(b"unterminated string"), start)
     except error.ParseError as inst:
         if len(inst.args) > 1:  # has location
             loc = inst.args[1]
             # Offset the caret location by the number of newlines before the
             # location of the error, since we will replace one-char newlines
             # with the two-char literal r'\n'.
-            offset = tmpl[:loc].count('\n')
-            tmpl = tmpl.replace('\n', br'\n')
+            offset = tmpl[:loc].count(b'\n')
+            tmpl = tmpl.replace(b'\n', br'\n')
             # We want the caret to point to the place in the template that
             # failed to parse, but in a hint we get a open paren at the
             # start. Therefore, we print "loc + 1" spaces (instead of "loc")
             # to line up the caret with the location of the error.
             inst.hint = (
-                tmpl + '\n' + ' ' * (loc + 1 + offset) + '^ ' + _('here')
+                tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
             )
         raise
-    yield ('end', None, pos)
+    yield (b'end', None, pos)
 
 
 def _unnesttemplatelist(tree):
@@ -339,14 +339,14 @@
     if not isinstance(tree, tuple):
         return tree
     op = tree[0]
-    if op != 'template':
+    if op != b'template':
         return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
 
     assert len(tree) == 2
     xs = tuple(_unnesttemplatelist(x) for x in tree[1])
     if not xs:
-        return ('string', '')  # empty template ""
-    elif len(xs) == 1 and xs[0][0] == 'string':
+        return (b'string', b'')  # empty template ""
+    elif len(xs) == 1 and xs[0][0] == b'string':
         return xs[0]  # fast path for string with no template fragment "x"
     else:
         return (op,) + xs
@@ -355,8 +355,8 @@
 def parse(tmpl):
     """Parse template string into tree"""
     parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
-    assert pos == len(tmpl), 'unquoted template should be consumed'
-    return _unnesttemplatelist(('template', parsed))
+    assert pos == len(tmpl), b'unquoted template should be consumed'
+    return _unnesttemplatelist((b'template', parsed))
 
 
 def _parseexpr(expr):
@@ -378,18 +378,18 @@
     p = parser.parser(elements)
     tree, pos = p.parse(tokenize(expr, 0, len(expr)))
     if pos != len(expr):
-        raise error.ParseError(_('invalid token'), pos)
+        raise error.ParseError(_(b'invalid token'), pos)
     return _unnesttemplatelist(tree)
 
 
 def prettyformat(tree):
-    return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
+    return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
 
 
 def compileexp(exp, context, curmethods):
     """Compile parsed template tree to (func, data) pair"""
     if not exp:
-        raise error.ParseError(_("missing argument"))
+        raise error.ParseError(_(b"missing argument"))
     t = exp[0]
     return curmethods[t](exp, context)
 
@@ -398,15 +398,15 @@
 
 
 def getsymbol(exp):
-    if exp[0] == 'symbol':
+    if exp[0] == b'symbol':
         return exp[1]
-    raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
+    raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
 
 
 def getlist(x):
     if not x:
         return []
-    if x[0] == 'list':
+    if x[0] == b'list':
         return getlist(x[1]) + [x[2]]
     return [x]
 
@@ -414,18 +414,18 @@
 def gettemplate(exp, context):
     """Compile given template tree or load named template from map file;
     returns (func, data) pair"""
-    if exp[0] in ('template', 'string'):
+    if exp[0] in (b'template', b'string'):
         return compileexp(exp, context, methods)
-    if exp[0] == 'symbol':
+    if exp[0] == b'symbol':
         # unlike runsymbol(), here 'symbol' is always taken as template name
         # even if it exists in mapping. this allows us to override mapping
         # by web templates, e.g. 'changelogtag' is redefined in map file.
         return context._load(exp[1])
-    raise error.ParseError(_("expected template specifier"))
+    raise error.ParseError(_(b"expected template specifier"))
 
 
 def _runrecursivesymbol(context, mapping, key):
-    raise error.Abort(_("recursive reference '%s' in template") % key)
+    raise error.Abort(_(b"recursive reference '%s' in template") % key)
 
 
 def buildtemplate(exp, context):
@@ -443,7 +443,7 @@
         f = context._funcs[n]
         args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
         return (f, args)
-    raise error.ParseError(_("unknown function '%s'") % n)
+    raise error.ParseError(_(b"unknown function '%s'") % n)
 
 
 def buildmap(exp, context):
@@ -478,10 +478,10 @@
     if n in context._filters:
         args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
         if len(args) != 1:
-            raise error.ParseError(_("filter %s expects one argument") % n)
+            raise error.ParseError(_(b"filter %s expects one argument") % n)
         f = context._filters[n]
         return (templateutil.runfilter, (args[0], f))
-    raise error.ParseError(_("unknown function '%s'") % n)
+    raise error.ParseError(_(b"unknown function '%s'") % n)
 
 
 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
@@ -518,8 +518,8 @@
         getlist(exp),
         funcname,
         argspec,
-        keyvaluenode='keyvalue',
-        keynode='symbol',
+        keyvaluenode=b'keyvalue',
+        keynode=b'symbol',
     )
     compargs = util.sortdict()
     if varkey:
@@ -531,54 +531,54 @@
 
 
 def buildkeyvaluepair(exp, content):
-    raise error.ParseError(_("can't use a key-value pair in this context"))
+    raise error.ParseError(_(b"can't use a key-value pair in this context"))
 
 
 def buildlist(exp, context):
     raise error.ParseError(
-        _("can't use a list in this context"),
-        hint=_('check place of comma and parens'),
+        _(b"can't use a list in this context"),
+        hint=_(b'check place of comma and parens'),
     )
 
 
 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
 exprmethods = {
-    "integer": lambda e, c: (templateutil.runinteger, e[1]),
-    "string": lambda e, c: (templateutil.runstring, e[1]),
-    "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
-    "template": buildtemplate,
-    "group": lambda e, c: compileexp(e[1], c, exprmethods),
-    ".": buildmember,
-    "|": buildfilter,
-    "%": buildmap,
-    "func": buildfunc,
-    "keyvalue": buildkeyvaluepair,
-    "list": buildlist,
-    "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
-    "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
-    "negate": buildnegate,
-    "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
-    "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
+    b"integer": lambda e, c: (templateutil.runinteger, e[1]),
+    b"string": lambda e, c: (templateutil.runstring, e[1]),
+    b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
+    b"template": buildtemplate,
+    b"group": lambda e, c: compileexp(e[1], c, exprmethods),
+    b".": buildmember,
+    b"|": buildfilter,
+    b"%": buildmap,
+    b"func": buildfunc,
+    b"keyvalue": buildkeyvaluepair,
+    b"list": buildlist,
+    b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
+    b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
+    b"negate": buildnegate,
+    b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
+    b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
 }
 
 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
 methods = exprmethods.copy()
-methods["integer"] = exprmethods["symbol"]  # '{1}' as variable
+methods[b"integer"] = exprmethods[b"symbol"]  # '{1}' as variable
 
 
 class _aliasrules(parser.basealiasrules):
     """Parsing and expansion rule set of template aliases"""
 
-    _section = _('template alias')
+    _section = _(b'template alias')
     _parse = staticmethod(_parseexpr)
 
     @staticmethod
     def _trygetfunc(tree):
         """Return (name, args) if tree is func(...) or ...|filter; otherwise
         None"""
-        if tree[0] == 'func' and tree[1][0] == 'symbol':
+        if tree[0] == b'func' and tree[1][0] == b'symbol':
             return tree[1][1], getlist(tree[2])
-        if tree[0] == '|' and tree[2][0] == 'symbol':
+        if tree[0] == b'|' and tree[2][0] == b'symbol':
             return tree[2][1], [tree[1]]
 
 
@@ -593,7 +593,7 @@
 
 def unquotestring(s):
     '''unwrap quotes if any; otherwise returns unmodified string'''
-    if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
+    if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
         return s
     return s[1:-1]
 
@@ -721,7 +721,7 @@
         v = self._resources.lookup(mapping, key)
         if v is None:
             raise templateutil.ResourceUnavailable(
-                _('template resource not available: %s') % key
+                _(b'template resource not available: %s') % key
             )
         return v
 
@@ -783,36 +783,36 @@
 def stylelist():
     paths = templatepaths()
     if not paths:
-        return _('no templates found, try `hg debuginstall` for more info')
+        return _(b'no templates found, try `hg debuginstall` for more info')
     dirlist = os.listdir(paths[0])
     stylelist = []
     for file in dirlist:
-        split = file.split(".")
-        if split[-1] in ('orig', 'rej'):
+        split = file.split(b".")
+        if split[-1] in (b'orig', b'rej'):
             continue
-        if split[0] == "map-cmdline":
+        if split[0] == b"map-cmdline":
             stylelist.append(split[1])
-    return ", ".join(sorted(stylelist))
+    return b", ".join(sorted(stylelist))
 
 
 def _readmapfile(mapfile):
     """Load template elements from the given map file"""
     if not os.path.exists(mapfile):
         raise error.Abort(
-            _("style '%s' not found") % mapfile,
-            hint=_("available styles: %s") % stylelist(),
+            _(b"style '%s' not found") % mapfile,
+            hint=_(b"available styles: %s") % stylelist(),
         )
 
     base = os.path.dirname(mapfile)
     conf = config.config(includepaths=templatepaths())
-    conf.read(mapfile, remap={'': 'templates'})
+    conf.read(mapfile, remap={b'': b'templates'})
 
     cache = {}
     tmap = {}
     aliases = []
 
-    val = conf.get('templates', '__base__')
-    if val and val[0] not in "'\"":
+    val = conf.get(b'templates', b'__base__')
+    if val and val[0] not in b"'\"":
         # treat as a pointer to a base class for this style
         path = util.normpath(os.path.join(base, val))
 
@@ -823,27 +823,27 @@
                 if os.path.isfile(p2):
                     path = p2
                     break
-                p3 = util.normpath(os.path.join(p2, "map"))
+                p3 = util.normpath(os.path.join(p2, b"map"))
                 if os.path.isfile(p3):
                     path = p3
                     break
 
         cache, tmap, aliases = _readmapfile(path)
 
-    for key, val in conf['templates'].items():
+    for key, val in conf[b'templates'].items():
         if not val:
             raise error.ParseError(
-                _('missing value'), conf.source('templates', key)
+                _(b'missing value'), conf.source(b'templates', key)
             )
-        if val[0] in "'\"":
+        if val[0] in b"'\"":
             if val[0] != val[-1]:
                 raise error.ParseError(
-                    _('unmatched quotes'), conf.source('templates', key)
+                    _(b'unmatched quotes'), conf.source(b'templates', key)
                 )
             cache[key] = unquotestring(val)
-        elif key != '__base__':
+        elif key != b'__base__':
             tmap[key] = os.path.join(base, val)
-    aliases.extend(conf['templatealias'].items())
+    aliases.extend(conf[b'templatealias'].items())
     return cache, tmap, aliases
 
 
@@ -867,10 +867,10 @@
                 self.cache[t] = util.readfile(self._map[t])
             except KeyError as inst:
                 raise templateutil.TemplateNotFound(
-                    _('"%s" not in template map') % inst.args[0]
+                    _(b'"%s" not in template map') % inst.args[0]
                 )
             except IOError as inst:
-                reason = _('template file %s: %s') % (
+                reason = _(b'template file %s: %s') % (
                     self._map[t],
                     stringutil.forcebytestr(inst.args[1]),
                 )
@@ -887,7 +887,7 @@
         if not tree:
             return
         op = tree[0]
-        if op == 'symbol':
+        if op == b'symbol':
             s = tree[1]
             if s in syms[0]:
                 return  # avoid recursion: s -> cache[s] -> s
@@ -896,14 +896,14 @@
                 # s may be a reference for named template
                 self._findsymbolsused(self.load(s), syms)
             return
-        if op in {'integer', 'string'}:
+        if op in {b'integer', b'string'}:
             return
         # '{arg|func}' == '{func(arg)}'
-        if op == '|':
+        if op == b'|':
             syms[1].add(getsymbol(tree[2]))
             self._findsymbolsused(tree[1], syms)
             return
-        if op == 'func':
+        if op == b'func':
             syms[1].add(getsymbol(tree[1]))
             self._findsymbolsused(tree[2], syms)
             return
@@ -997,7 +997,7 @@
 
         This may load additional templates from the map file.
         """
-        return self.symbolsused('')
+        return self.symbolsused(b'')
 
     def symbolsused(self, t):
         """Look up (keywords, filters/functions) referenced from the name
@@ -1009,7 +1009,7 @@
 
     def renderdefault(self, mapping):
         """Render the default unnamed template and return result as string"""
-        return self.render('', mapping)
+        return self.render(b'', mapping)
 
     def render(self, t, mapping):
         """Render the specified named template and return result as string"""
@@ -1028,7 +1028,7 @@
 
 def templatepaths():
     '''return locations used for template files.'''
-    pathsrel = ['templates']
+    pathsrel = [b'templates']
     paths = [os.path.normpath(os.path.join(util.datapath, f)) for f in pathsrel]
     return [p for p in paths if os.path.isdir(p)]
 
@@ -1069,8 +1069,8 @@
             and pycompat.osaltsep in style
         ):
             continue
-        locations = [os.path.join(style, 'map'), 'map-' + style]
-        locations.append('map')
+        locations = [os.path.join(style, b'map'), b'map-' + style]
+        locations.append(b'map')
 
         for path in paths:
             for location in locations:
@@ -1078,4 +1078,4 @@
                 if os.path.isfile(mapfile):
                     return style, mapfile
 
-    raise RuntimeError("No hgweb templates found in %r" % paths)
+    raise RuntimeError(b"No hgweb templates found in %r" % paths)