Mercurial > public > mercurial-scm > hg-stable
diff mercurial/templateutil.py @ 36927:32f9b7e3f056
templater: move hybrid class and functions to templateutil module
And make _hybrid and _mappable classes public. _showlist() is still marked
as private since it's weird and third-party codes shouldn't depend on it.
author | Yuya Nishihara <yuya@tcha.org> |
---|---|
date | Thu, 08 Mar 2018 23:15:09 +0900 |
parents | 6ff6e1d6b5b8 |
children | 255f635c3204 |
line wrap: on
line diff
--- 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)