--- a/mercurial/templateutil.py Thu Mar 08 23:10:46 2018 +0900
+++ b/mercurial/templateutil.py Thu Mar 08 23:15:09 2018 +0900
@@ -13,7 +13,6 @@
from . import (
error,
pycompat,
- templatekw,
util,
)
@@ -23,9 +22,219 @@
class TemplateNotFound(error.Abort):
pass
+class hybrid(object):
+ """Wrapper for list or dict to support legacy template
+
+ This class allows us to handle both:
+ - "{files}" (legacy command-line-specific list hack) and
+ - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
+ and to access raw values:
+ - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
+ - "{get(extras, key)}"
+ - "{files|json}"
+ """
+
+ def __init__(self, gen, values, makemap, joinfmt, keytype=None):
+ if gen is not None:
+ self.gen = gen # generator or function returning generator
+ self._values = values
+ self._makemap = makemap
+ self.joinfmt = joinfmt
+ self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
+ def gen(self):
+ """Default generator to stringify this as {join(self, ' ')}"""
+ for i, x in enumerate(self._values):
+ if i > 0:
+ yield ' '
+ yield self.joinfmt(x)
+ def itermaps(self):
+ makemap = self._makemap
+ for x in self._values:
+ yield makemap(x)
+ def __contains__(self, x):
+ return x in self._values
+ def __getitem__(self, key):
+ return self._values[key]
+ def __len__(self):
+ return len(self._values)
+ def __iter__(self):
+ return iter(self._values)
+ def __getattr__(self, name):
+ if name not in (r'get', r'items', r'iteritems', r'iterkeys',
+ r'itervalues', r'keys', r'values'):
+ raise AttributeError(name)
+ return getattr(self._values, name)
+
+class mappable(object):
+ """Wrapper for non-list/dict object to support map operation
+
+ This class allows us to handle both:
+ - "{manifest}"
+ - "{manifest % '{rev}:{node}'}"
+ - "{manifest.rev}"
+
+ Unlike a hybrid, this does not simulate the behavior of the underling
+ value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
+ """
+
+ def __init__(self, gen, key, value, makemap):
+ if gen is not None:
+ self.gen = gen # generator or function returning generator
+ self._key = key
+ self._value = value # may be generator of strings
+ self._makemap = makemap
+
+ def gen(self):
+ yield pycompat.bytestr(self._value)
+
+ def tomap(self):
+ return self._makemap(self._key)
+
+ def itermaps(self):
+ yield self.tomap()
+
+def hybriddict(data, key='key', value='value', fmt=None, gen=None):
+ """Wrap data to support both dict-like and string-like operations"""
+ prefmt = pycompat.identity
+ if fmt is None:
+ fmt = '%s=%s'
+ prefmt = pycompat.bytestr
+ return hybrid(gen, data, lambda k: {key: k, value: data[k]},
+ lambda k: fmt % (prefmt(k), prefmt(data[k])))
+
+def hybridlist(data, name, fmt=None, gen=None):
+ """Wrap data to support both list-like and string-like operations"""
+ prefmt = pycompat.identity
+ if fmt is None:
+ fmt = '%s'
+ prefmt = pycompat.bytestr
+ return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
+
+def unwraphybrid(thing):
+ """Return an object which can be stringified possibly by using a legacy
+ template"""
+ gen = getattr(thing, 'gen', None)
+ if gen is None:
+ return thing
+ if callable(gen):
+ return gen()
+ return gen
+
+def unwrapvalue(thing):
+ """Move the inner value object out of the wrapper"""
+ if not util.safehasattr(thing, '_value'):
+ return thing
+ return thing._value
+
+def wraphybridvalue(container, key, value):
+ """Wrap an element of hybrid container to be mappable
+
+ The key is passed to the makemap function of the given container, which
+ should be an item generated by iter(container).
+ """
+ makemap = getattr(container, '_makemap', None)
+ if makemap is None:
+ return value
+ if util.safehasattr(value, '_makemap'):
+ # a nested hybrid list/dict, which has its own way of map operation
+ return value
+ return mappable(None, key, value, makemap)
+
+def compatdict(context, mapping, name, data, key='key', value='value',
+ fmt=None, plural=None, separator=' '):
+ """Wrap data like hybriddict(), but also supports old-style list template
+
+ This exists for backward compatibility with the old-style template. Use
+ hybriddict() for new template keywords.
+ """
+ c = [{key: k, value: v} for k, v in data.iteritems()]
+ t = context.resource(mapping, 'templ')
+ f = _showlist(name, c, t, mapping, plural, separator)
+ return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
+
+def compatlist(context, mapping, name, data, element=None, fmt=None,
+ plural=None, separator=' '):
+ """Wrap data like hybridlist(), but also supports old-style list template
+
+ This exists for backward compatibility with the old-style template. Use
+ hybridlist() for new template keywords.
+ """
+ t = context.resource(mapping, 'templ')
+ f = _showlist(name, data, t, mapping, plural, separator)
+ return hybridlist(data, name=element or name, fmt=fmt, gen=f)
+
+def _showlist(name, values, templ, mapping, plural=None, separator=' '):
+ '''expand set of values.
+ name is name of key in template map.
+ values is list of strings or dicts.
+ plural is plural of name, if not simply name + 's'.
+ separator is used to join values as a string
+
+ expansion works like this, given name 'foo'.
+
+ if values is empty, expand 'no_foos'.
+
+ if 'foo' not in template map, return values as a string,
+ joined by 'separator'.
+
+ expand 'start_foos'.
+
+ for each value, expand 'foo'. if 'last_foo' in template
+ map, expand it instead of 'foo' for last key.
+
+ expand 'end_foos'.
+ '''
+ strmapping = pycompat.strkwargs(mapping)
+ if not plural:
+ plural = name + 's'
+ if not values:
+ noname = 'no_' + plural
+ if noname in templ:
+ yield templ(noname, **strmapping)
+ return
+ if name not in templ:
+ if isinstance(values[0], bytes):
+ yield separator.join(values)
+ else:
+ for v in values:
+ r = dict(v)
+ r.update(mapping)
+ yield r
+ return
+ startname = 'start_' + plural
+ if startname in templ:
+ yield templ(startname, **strmapping)
+ vmapping = mapping.copy()
+ def one(v, tag=name):
+ try:
+ vmapping.update(v)
+ # Python 2 raises ValueError if the type of v is wrong. Python
+ # 3 raises TypeError.
+ except (AttributeError, TypeError, ValueError):
+ try:
+ # Python 2 raises ValueError trying to destructure an e.g.
+ # bytes. Python 3 raises TypeError.
+ for a, b in v:
+ vmapping[a] = b
+ except (TypeError, ValueError):
+ vmapping[name] = v
+ return templ(tag, **pycompat.strkwargs(vmapping))
+ lastname = 'last_' + name
+ if lastname in templ:
+ last = values.pop()
+ else:
+ last = None
+ for v in values:
+ yield one(v)
+ if last is not None:
+ yield one(last, tag=lastname)
+ endname = 'end_' + plural
+ if endname in templ:
+ yield templ(endname, **strmapping)
+
def stringify(thing):
"""Turn values into bytes by converting into text and concatenating them"""
- thing = templatekw.unwraphybrid(thing)
+ thing = unwraphybrid(thing)
if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
if isinstance(thing, str):
# This is only reachable on Python 3 (otherwise
@@ -59,7 +268,7 @@
def evalfuncarg(context, mapping, arg):
"""Evaluate given argument as value type"""
thing = evalrawexp(context, mapping, arg)
- thing = templatekw.unwrapvalue(thing)
+ thing = unwrapvalue(thing)
# evalrawexp() may return string, generator of strings or arbitrary object
# such as date tuple, but filter does not want generator.
if isinstance(thing, types.GeneratorType):
@@ -76,7 +285,7 @@
thing = util.parsebool(data)
else:
thing = func(context, mapping, data)
- thing = templatekw.unwrapvalue(thing)
+ thing = unwrapvalue(thing)
if isinstance(thing, bool):
return thing
# other objects are evaluated as strings, which means 0 is True, but
@@ -236,4 +445,4 @@
val = dictarg.get(key)
if val is None:
return
- return templatekw.wraphybridvalue(dictarg, key, val)
+ return wraphybridvalue(dictarg, key, val)