# HG changeset patch # User Yuya Nishihara # Date 1461490883 -32400 # Node ID 89aec1834a86003def3b6656863e48c5d41a858f # Parent 6367318327f03877834561c6f18d0c0bebe8d7b1 templatekw: add new-style template expansion to {manifest} The goal is to allow us to easily access to nested data. The dot operator will be introduced later so we can write '{p1.files}' instead of '{revset("p1()") % "{files}"}' for example. In the example above, 'p1' needs to carry a mapping dict along with its string representation. If it were a list or a dict, it could be wrapped semi-transparently with the _hybrid class, but for non-list/dict types, it would be difficult to proxy all necessary functions to underlying value type because several core operations may conflict with the ones of the underlying value: - hash(value) should be different from hash(wrapped(value)), which means dict[wrapped(value)] would be invalid - 'value == wrapped(value)' would be false, breaks 'ifcontains' - len(wrapped(value)) may be either len(value) or len(iter(wrapped(value))) So the wrapper has no proxy functions and its scope designed to be minimal. It's unwrapped at eval*() functions so we don't have to care for a wrapped object unless it's really needed: # most template functions just call evalfuncarg() unwrapped_value = evalfuncarg(context, mapping, args[n]) # if wrapped value is needed, use evalrawexp() maybe_wrapped_value = evalrawexp(context, mapping, args[n]) Another idea was to wrap every template variable with a tagging class, but which seemed uneasy without a static type checker. This patch updates {manifest} to a mappable as an example. diff -r 6367318327f0 -r 89aec1834a86 mercurial/templatekw.py --- a/mercurial/templatekw.py Mon Apr 24 21:37:11 2017 +0900 +++ b/mercurial/templatekw.py Sun Apr 24 18:41:23 2016 +0900 @@ -70,6 +70,28 @@ 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}'}" + + 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, value, makemap): + self.gen = gen + self._value = value # may be generator of strings + self._makemap = makemap + + def tomap(self): + return self._makemap() + + def itermaps(self): + yield self.tomap() + def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None): """Wrap data to support both dict-like and string-like operations""" return _hybrid(gen, data, lambda k: {key: k, value: data[k]}, @@ -86,6 +108,12 @@ return thing return thing.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 showdict(name, data, mapping, plural=None, key='key', value='value', fmt='%s=%s', separator=' '): c = [{key: k, value: v} for k, v in data.iteritems()] @@ -543,10 +571,14 @@ if mnode is None: # just avoid crash, we might want to use the 'ff...' hash in future return + mrev = repo.manifestlog._revlog.rev(mnode) + mhex = hex(mnode) args = args.copy() - args.update({r'rev': repo.manifestlog._revlog.rev(mnode), - r'node': hex(mnode)}) - return templ('manifest', **args) + args.update({r'rev': mrev, r'node': mhex}) + f = templ('manifest', **args) + # TODO: perhaps 'ctx' should be dropped from mapping because manifest + # rev and node are completely different from changeset's. + return _mappable(f, f, lambda: {'rev': mrev, 'node': mhex}) def shownames(namespace, **args): """helper method to generate a template keyword for a namespace""" diff -r 6367318327f0 -r 89aec1834a86 mercurial/templater.py --- a/mercurial/templater.py Mon Apr 24 21:37:11 2017 +0900 +++ b/mercurial/templater.py Sun Apr 24 18:41:23 2016 +0900 @@ -307,6 +307,7 @@ def evalfuncarg(context, mapping, arg): """Evaluate given argument as value type""" thing = evalrawexp(context, mapping, arg) + thing = templatekw.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): @@ -323,6 +324,7 @@ thing = util.parsebool(data) else: thing = func(context, mapping, data) + thing = templatekw.unwrapvalue(thing) if isinstance(thing, bool): return thing # other objects are evaluated as strings, which means 0 is True, but @@ -768,6 +770,7 @@ # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb # abuses generator as a keyword that returns a list of dicts. joinset = evalrawexp(context, mapping, args[0]) + joinset = templatekw.unwrapvalue(joinset) joinfmt = getattr(joinset, 'joinfmt', pycompat.identity) joiner = " " if len(args) > 1: diff -r 6367318327f0 -r 89aec1834a86 tests/test-command-template.t --- a/tests/test-command-template.t Mon Apr 24 21:37:11 2017 +0900 +++ b/tests/test-command-template.t Sun Apr 24 18:41:23 2016 +0900 @@ -3119,6 +3119,20 @@ hg: parse error: None is not iterable [255] +Test new-style inline templating of non-list/dict type: + + $ hg log -R latesttag -r tip -T '{manifest}\n' + 11:2bc6e9006ce2 + $ hg log -R latesttag -r tip -T 'string length: {manifest|count}\n' + string length: 15 + $ hg log -R latesttag -r tip -T '{manifest % "{rev}:{node}"}\n' + 11:2bc6e9006ce29882383a22d39fd1f4e66dd3e2fc + +Test manifest can be join()-ed as before, though it's silly: + + $ hg log -R latesttag -r tip -T '{join(manifest, "")}\n' + 11:2bc6e9006ce2 + Test the sub function of templating for expansion: $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'