templateutil.py
986 lines
| 33.2 KiB
| text/x-python
|
PythonLexer
/ mercurial / templateutil.py
Yuya Nishihara
|
r36931 | # templateutil.py - utility for template evaluation | ||
# | ||||
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
Yuya Nishihara
|
r37291 | import abc | ||
Yuya Nishihara
|
r36931 | import types | ||
from .i18n import _ | ||||
from . import ( | ||||
error, | ||||
pycompat, | ||||
util, | ||||
) | ||||
Yuya Nishihara
|
r37102 | from .utils import ( | ||
Yuya Nishihara
|
r37241 | dateutil, | ||
Yuya Nishihara
|
r37102 | stringutil, | ||
) | ||||
Yuya Nishihara
|
r36931 | |||
class ResourceUnavailable(error.Abort): | ||||
pass | ||||
class TemplateNotFound(error.Abort): | ||||
pass | ||||
Yuya Nishihara
|
r37291 | class wrapped(object): | ||
"""Object requiring extra conversion prior to displaying or processing | ||||
Yuya Nishihara
|
r37297 | as value | ||
Yuya Nishihara
|
r38289 | Use unwrapvalue() or unwrapastype() to obtain the inner object. | ||
Yuya Nishihara
|
r37297 | """ | ||
Yuya Nishihara
|
r37291 | |||
__metaclass__ = abc.ABCMeta | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
"""Test if the specified item is in self | ||||
The item argument may be a wrapped object. | ||||
""" | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r38261 | def getmember(self, context, mapping, key): | ||
"""Return a member item for the specified key | ||||
Yuya Nishihara
|
r38262 | The key argument may be a wrapped object. | ||
Yuya Nishihara
|
r38261 | A returned object may be either a wrapped object or a pure value | ||
depending on the self type. | ||||
""" | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
"""Return the smallest item, which may be either a wrapped or a pure | ||||
value depending on the self type""" | ||||
@abc.abstractmethod | ||||
def getmax(self, context, mapping): | ||||
"""Return the largest item, which may be either a wrapped or a pure | ||||
value depending on the self type""" | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
"""Return new container of the same type which includes only the | ||||
selected elements | ||||
select() takes each item as a wrapped object and returns True/False. | ||||
""" | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r37340 | def itermaps(self, context): | ||
Yuya Nishihara
|
r37339 | """Yield each template mapping""" | ||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r37343 | def join(self, context, mapping, sep): | ||
"""Join items with the separator; Returns a bytes or (possibly nested) | ||||
generator of bytes | ||||
A pre-configured template may be rendered per item if this container | ||||
holds unprintable items. | ||||
""" | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r37291 | def show(self, context, mapping): | ||
"""Return a bytes or (possibly nested) generator of bytes representing | ||||
the underlying object | ||||
A pre-configured template may be rendered if the underlying object is | ||||
not printable. | ||||
""" | ||||
Yuya Nishihara
|
r37297 | @abc.abstractmethod | ||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
"""Return a boolean representation of the inner value""" | ||||
@abc.abstractmethod | ||||
Yuya Nishihara
|
r37297 | def tovalue(self, context, mapping): | ||
"""Move the inner value object out or create a value representation | ||||
A returned value must be serializable by templaterfilters.json(). | ||||
""" | ||||
Yuya Nishihara
|
r38303 | class mappable(object): | ||
"""Object which can be converted to a single template mapping""" | ||||
def itermaps(self, context): | ||||
yield self.tomap(context) | ||||
@abc.abstractmethod | ||||
def tomap(self, context): | ||||
"""Create a single template mapping representing this""" | ||||
Yuya Nishihara
|
r38228 | class wrappedbytes(wrapped): | ||
"""Wrapper for byte string""" | ||||
def __init__(self, value): | ||||
self._value = value | ||||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
item = stringify(context, mapping, item) | ||||
return item in self._value | ||||
Yuya Nishihara
|
r38261 | def getmember(self, context, mapping, key): | ||
raise error.ParseError(_('%r is not a dictionary') | ||||
% pycompat.bytestr(self._value)) | ||||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
return self._getby(context, mapping, min) | ||||
def getmax(self, context, mapping): | ||||
return self._getby(context, mapping, max) | ||||
def _getby(self, context, mapping, func): | ||||
if not self._value: | ||||
raise error.ParseError(_('empty string')) | ||||
return func(pycompat.iterbytestr(self._value)) | ||||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
raise error.ParseError(_('%r is not filterable') | ||||
% pycompat.bytestr(self._value)) | ||||
Yuya Nishihara
|
r38228 | def itermaps(self, context): | ||
raise error.ParseError(_('%r is not iterable of mappings') | ||||
% pycompat.bytestr(self._value)) | ||||
def join(self, context, mapping, sep): | ||||
return joinitems(pycompat.iterbytestr(self._value), sep) | ||||
def show(self, context, mapping): | ||||
return self._value | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
return bool(self._value) | ||||
Yuya Nishihara
|
r38228 | def tovalue(self, context, mapping): | ||
return self._value | ||||
class wrappedvalue(wrapped): | ||||
"""Generic wrapper for pure non-list/dict/bytes value""" | ||||
def __init__(self, value): | ||||
self._value = value | ||||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
raise error.ParseError(_("%r is not iterable") % self._value) | ||||
Yuya Nishihara
|
r38261 | def getmember(self, context, mapping, key): | ||
raise error.ParseError(_('%r is not a dictionary') % self._value) | ||||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
raise error.ParseError(_("%r is not iterable") % self._value) | ||||
def getmax(self, context, mapping): | ||||
raise error.ParseError(_("%r is not iterable") % self._value) | ||||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
raise error.ParseError(_("%r is not iterable") % self._value) | ||||
Yuya Nishihara
|
r38228 | def itermaps(self, context): | ||
raise error.ParseError(_('%r is not iterable of mappings') | ||||
% self._value) | ||||
def join(self, context, mapping, sep): | ||||
raise error.ParseError(_('%r is not iterable') % self._value) | ||||
def show(self, context, mapping): | ||||
Yuya Nishihara
|
r38291 | if self._value is None: | ||
return b'' | ||||
Yuya Nishihara
|
r38228 | return pycompat.bytestr(self._value) | ||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
if self._value is None: | ||||
return False | ||||
if isinstance(self._value, bool): | ||||
return self._value | ||||
# otherwise evaluate as string, which means 0 is True | ||||
return bool(pycompat.bytestr(self._value)) | ||||
Yuya Nishihara
|
r38228 | def tovalue(self, context, mapping): | ||
return self._value | ||||
Yuya Nishihara
|
r38305 | class date(mappable, wrapped): | ||
Yuya Nishihara
|
r38304 | """Wrapper for date tuple""" | ||
Yuya Nishihara
|
r38318 | def __init__(self, value, showfmt='%d %d'): | ||
Yuya Nishihara
|
r38304 | # value may be (float, int), but public interface shouldn't support | ||
# floating-point timestamp | ||||
self._unixtime, self._tzoffset = map(int, value) | ||||
Yuya Nishihara
|
r38318 | self._showfmt = showfmt | ||
Yuya Nishihara
|
r38304 | |||
def contains(self, context, mapping, item): | ||||
raise error.ParseError(_('date is not iterable')) | ||||
def getmember(self, context, mapping, key): | ||||
raise error.ParseError(_('date is not a dictionary')) | ||||
def getmin(self, context, mapping): | ||||
raise error.ParseError(_('date is not iterable')) | ||||
def getmax(self, context, mapping): | ||||
raise error.ParseError(_('date is not iterable')) | ||||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
raise error.ParseError(_('date is not iterable')) | ||||
Yuya Nishihara
|
r38304 | def join(self, context, mapping, sep): | ||
raise error.ParseError(_("date is not iterable")) | ||||
def show(self, context, mapping): | ||||
Yuya Nishihara
|
r38318 | return self._showfmt % (self._unixtime, self._tzoffset) | ||
Yuya Nishihara
|
r38304 | |||
Yuya Nishihara
|
r38305 | def tomap(self, context): | ||
return {'unixtime': self._unixtime, 'tzoffset': self._tzoffset} | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
return True | ||||
Yuya Nishihara
|
r38304 | def tovalue(self, context, mapping): | ||
return (self._unixtime, self._tzoffset) | ||||
Yuya Nishihara
|
r37244 | |||
Yuya Nishihara
|
r37291 | class hybrid(wrapped): | ||
Yuya Nishihara
|
r36939 | """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): | ||||
Yuya Nishihara
|
r37341 | self._gen = gen # generator or function returning generator | ||
Yuya Nishihara
|
r36939 | self._values = values | ||
self._makemap = makemap | ||||
Yuya Nishihara
|
r37345 | self._joinfmt = joinfmt | ||
Yuya Nishihara
|
r38287 | self._keytype = keytype # hint for 'x in y' where type(x) is unresolved | ||
Yuya Nishihara
|
r37293 | |||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
Yuya Nishihara
|
r38287 | item = unwrapastype(context, mapping, item, self._keytype) | ||
Yuya Nishihara
|
r38286 | return item in self._values | ||
Yuya Nishihara
|
r38260 | def getmember(self, context, mapping, key): | ||
# TODO: maybe split hybrid list/dict types? | ||||
if not util.safehasattr(self._values, 'get'): | ||||
raise error.ParseError(_('not a dictionary')) | ||||
Yuya Nishihara
|
r38287 | key = unwrapastype(context, mapping, key, self._keytype) | ||
Yuya Nishihara
|
r38260 | return self._wrapvalue(key, self._values.get(key)) | ||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
return self._getby(context, mapping, min) | ||||
def getmax(self, context, mapping): | ||||
return self._getby(context, mapping, max) | ||||
def _getby(self, context, mapping, func): | ||||
if not self._values: | ||||
raise error.ParseError(_('empty sequence')) | ||||
val = func(self._values) | ||||
return self._wrapvalue(val, val) | ||||
Yuya Nishihara
|
r38260 | def _wrapvalue(self, key, val): | ||
if val is None: | ||||
return | ||||
Yuya Nishihara
|
r38285 | if util.safehasattr(val, '_makemap'): | ||
# a nested hybrid list/dict, which has its own way of map operation | ||||
return val | ||||
Yuya Nishihara
|
r38302 | return hybriditem(None, key, val, self._makemap) | ||
Yuya Nishihara
|
r38260 | |||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
if util.safehasattr(self._values, 'get'): | ||||
values = {k: v for k, v in self._values.iteritems() | ||||
if select(self._wrapvalue(k, v))} | ||||
else: | ||||
values = [v for v in self._values if select(self._wrapvalue(v, v))] | ||||
return hybrid(None, values, self._makemap, self._joinfmt, self._keytype) | ||||
Yuya Nishihara
|
r37340 | def itermaps(self, context): | ||
Yuya Nishihara
|
r36939 | makemap = self._makemap | ||
for x in self._values: | ||||
yield makemap(x) | ||||
Yuya Nishihara
|
r37291 | |||
Yuya Nishihara
|
r37343 | def join(self, context, mapping, sep): | ||
# TODO: switch gen to (context, mapping) API? | ||||
Yuya Nishihara
|
r37345 | return joinitems((self._joinfmt(x) for x in self._values), sep) | ||
Yuya Nishihara
|
r37343 | |||
Yuya Nishihara
|
r37291 | def show(self, context, mapping): | ||
# TODO: switch gen to (context, mapping) API? | ||||
Yuya Nishihara
|
r37293 | gen = self._gen | ||
Yuya Nishihara
|
r37341 | if gen is None: | ||
Yuya Nishihara
|
r37343 | return self.join(context, mapping, ' ') | ||
Yuya Nishihara
|
r37291 | if callable(gen): | ||
return gen() | ||||
return gen | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
return bool(self._values) | ||||
Yuya Nishihara
|
r37297 | def tovalue(self, context, mapping): | ||
Yuya Nishihara
|
r38288 | # TODO: make it non-recursive for trivial lists/dicts | ||
xs = self._values | ||||
if util.safehasattr(xs, 'get'): | ||||
return {k: unwrapvalue(context, mapping, v) | ||||
for k, v in xs.iteritems()} | ||||
return [unwrapvalue(context, mapping, x) for x in xs] | ||||
Yuya Nishihara
|
r36939 | |||
Yuya Nishihara
|
r38303 | class hybriditem(mappable, wrapped): | ||
Yuya Nishihara
|
r36939 | """Wrapper for non-list/dict object to support map operation | ||
This class allows us to handle both: | ||||
- "{manifest}" | ||||
- "{manifest % '{rev}:{node}'}" | ||||
- "{manifest.rev}" | ||||
""" | ||||
def __init__(self, gen, key, value, makemap): | ||||
Yuya Nishihara
|
r37294 | self._gen = gen # generator or function returning generator | ||
Yuya Nishihara
|
r36939 | self._key = key | ||
self._value = value # may be generator of strings | ||||
self._makemap = makemap | ||||
Yuya Nishihara
|
r38303 | def tomap(self, context): | ||
Yuya Nishihara
|
r36939 | return self._makemap(self._key) | ||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
w = makewrapped(context, mapping, self._value) | ||||
return w.contains(context, mapping, item) | ||||
Yuya Nishihara
|
r38261 | def getmember(self, context, mapping, key): | ||
w = makewrapped(context, mapping, self._value) | ||||
return w.getmember(context, mapping, key) | ||||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
w = makewrapped(context, mapping, self._value) | ||||
return w.getmin(context, mapping) | ||||
def getmax(self, context, mapping): | ||||
w = makewrapped(context, mapping, self._value) | ||||
return w.getmax(context, mapping) | ||||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
w = makewrapped(context, mapping, self._value) | ||||
return w.filter(context, mapping, select) | ||||
Yuya Nishihara
|
r37343 | def join(self, context, mapping, sep): | ||
Yuya Nishihara
|
r38230 | w = makewrapped(context, mapping, self._value) | ||
return w.join(context, mapping, sep) | ||||
Yuya Nishihara
|
r37343 | |||
Yuya Nishihara
|
r37291 | def show(self, context, mapping): | ||
# TODO: switch gen to (context, mapping) API? | ||||
Yuya Nishihara
|
r37293 | gen = self._gen | ||
Yuya Nishihara
|
r37294 | if gen is None: | ||
return pycompat.bytestr(self._value) | ||||
Yuya Nishihara
|
r37291 | if callable(gen): | ||
return gen() | ||||
return gen | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
Yuya Nishihara
|
r38466 | w = makewrapped(context, mapping, self._value) | ||
return w.tobool(context, mapping) | ||||
Yuya Nishihara
|
r38308 | |||
Yuya Nishihara
|
r37297 | def tovalue(self, context, mapping): | ||
return _unthunk(context, mapping, self._value) | ||||
Yuya Nishihara
|
r37417 | class _mappingsequence(wrapped): | ||
"""Wrapper for sequence of template mappings | ||||
This represents an inner template structure (i.e. a list of dicts), | ||||
which can also be rendered by the specified named/literal template. | ||||
Template mappings may be nested. | ||||
""" | ||||
def __init__(self, name=None, tmpl=None, sep=''): | ||||
if name is not None and tmpl is not None: | ||||
raise error.ProgrammingError('name and tmpl are mutually exclusive') | ||||
self._name = name | ||||
self._tmpl = tmpl | ||||
self._defaultsep = sep | ||||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
raise error.ParseError(_('not comparable')) | ||||
Yuya Nishihara
|
r38261 | def getmember(self, context, mapping, key): | ||
raise error.ParseError(_('not a dictionary')) | ||||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
raise error.ParseError(_('not comparable')) | ||||
def getmax(self, context, mapping): | ||||
raise error.ParseError(_('not comparable')) | ||||
Yuya Nishihara
|
r38467 | def filter(self, context, mapping, select): | ||
Yuya Nishihara
|
r38468 | # implement if necessary; we'll need a wrapped type for a mapping dict | ||
Yuya Nishihara
|
r38467 | raise error.ParseError(_('not filterable without template')) | ||
Yuya Nishihara
|
r37417 | def join(self, context, mapping, sep): | ||
mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context)) | ||||
if self._name: | ||||
itemiter = (context.process(self._name, m) for m in mapsiter) | ||||
elif self._tmpl: | ||||
itemiter = (context.expand(self._tmpl, m) for m in mapsiter) | ||||
else: | ||||
raise error.ParseError(_('not displayable without template')) | ||||
return joinitems(itemiter, sep) | ||||
def show(self, context, mapping): | ||||
return self.join(context, mapping, self._defaultsep) | ||||
def tovalue(self, context, mapping): | ||||
Yuya Nishihara
|
r37520 | knownres = context.knownresourcekeys() | ||
items = [] | ||||
for nm in self.itermaps(context): | ||||
# drop internal resources (recursively) which shouldn't be displayed | ||||
lm = context.overlaymap(mapping, nm) | ||||
items.append({k: unwrapvalue(context, lm, v) | ||||
for k, v in nm.iteritems() if k not in knownres}) | ||||
return items | ||||
Yuya Nishihara
|
r37417 | |||
class mappinggenerator(_mappingsequence): | ||||
"""Wrapper for generator of template mappings | ||||
The function ``make(context, *args)`` should return a generator of | ||||
mapping dicts. | ||||
""" | ||||
def __init__(self, make, args=(), name=None, tmpl=None, sep=''): | ||||
super(mappinggenerator, self).__init__(name, tmpl, sep) | ||||
self._make = make | ||||
self._args = args | ||||
def itermaps(self, context): | ||||
return self._make(context, *self._args) | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
return _nonempty(self.itermaps(context)) | ||||
Yuya Nishihara
|
r37417 | class mappinglist(_mappingsequence): | ||
"""Wrapper for list of template mappings""" | ||||
def __init__(self, mappings, name=None, tmpl=None, sep=''): | ||||
super(mappinglist, self).__init__(name, tmpl, sep) | ||||
self._mappings = mappings | ||||
def itermaps(self, context): | ||||
return iter(self._mappings) | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
return bool(self._mappings) | ||||
Yuya Nishihara
|
r40509 | class mappingdict(mappable, _mappingsequence): | ||
"""Wrapper for a single template mapping | ||||
This isn't a sequence in a way that the underlying dict won't be iterated | ||||
as a dict, but shares most of the _mappingsequence functions. | ||||
""" | ||||
def __init__(self, mapping, name=None, tmpl=None): | ||||
super(mappingdict, self).__init__(name, tmpl) | ||||
self._mapping = mapping | ||||
def tomap(self, context): | ||||
return self._mapping | ||||
def tobool(self, context, mapping): | ||||
# no idea when a template mapping should be considered an empty, but | ||||
# a mapping dict should have at least one item in practice, so always | ||||
# mark this as non-empty. | ||||
return True | ||||
def tovalue(self, context, mapping): | ||||
return super(mappingdict, self).tovalue(context, mapping)[0] | ||||
Yuya Nishihara
|
r37517 | class mappedgenerator(wrapped): | ||
"""Wrapper for generator of strings which acts as a list | ||||
The function ``make(context, *args)`` should return a generator of | ||||
byte strings, or a generator of (possibly nested) generators of byte | ||||
strings (i.e. a generator for a list of byte strings.) | ||||
""" | ||||
def __init__(self, make, args=()): | ||||
self._make = make | ||||
self._args = args | ||||
Yuya Nishihara
|
r38286 | def contains(self, context, mapping, item): | ||
item = stringify(context, mapping, item) | ||||
return item in self.tovalue(context, mapping) | ||||
Yuya Nishihara
|
r37517 | def _gen(self, context): | ||
return self._make(context, *self._args) | ||||
Yuya Nishihara
|
r38261 | def getmember(self, context, mapping, key): | ||
raise error.ParseError(_('not a dictionary')) | ||||
Yuya Nishihara
|
r38284 | def getmin(self, context, mapping): | ||
return self._getby(context, mapping, min) | ||||
def getmax(self, context, mapping): | ||||
return self._getby(context, mapping, max) | ||||
def _getby(self, context, mapping, func): | ||||
xs = self.tovalue(context, mapping) | ||||
if not xs: | ||||
raise error.ParseError(_('empty sequence')) | ||||
return func(xs) | ||||
Yuya Nishihara
|
r38467 | @staticmethod | ||
def _filteredgen(context, mapping, make, args, select): | ||||
for x in make(context, *args): | ||||
s = stringify(context, mapping, x) | ||||
if select(wrappedbytes(s)): | ||||
yield s | ||||
def filter(self, context, mapping, select): | ||||
args = (mapping, self._make, self._args, select) | ||||
return mappedgenerator(self._filteredgen, args) | ||||
Yuya Nishihara
|
r37517 | def itermaps(self, context): | ||
raise error.ParseError(_('list of strings is not mappable')) | ||||
def join(self, context, mapping, sep): | ||||
return joinitems(self._gen(context), sep) | ||||
def show(self, context, mapping): | ||||
return self.join(context, mapping, '') | ||||
Yuya Nishihara
|
r38308 | def tobool(self, context, mapping): | ||
return _nonempty(self._gen(context)) | ||||
Yuya Nishihara
|
r37517 | def tovalue(self, context, mapping): | ||
return [stringify(context, mapping, x) for x in self._gen(context)] | ||||
Yuya Nishihara
|
r36939 | 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 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()] | ||||
Yuya Nishihara
|
r37086 | f = _showcompatlist(context, mapping, name, c, plural, separator) | ||
Yuya Nishihara
|
r36939 | 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. | ||||
""" | ||||
Yuya Nishihara
|
r37086 | f = _showcompatlist(context, mapping, name, data, plural, separator) | ||
Yuya Nishihara
|
r36939 | return hybridlist(data, name=element or name, fmt=fmt, gen=f) | ||
Yuya Nishihara
|
r39404 | def compatfilecopiesdict(context, mapping, name, copies): | ||
"""Wrap list of (dest, source) file names to support old-style list | ||||
template and field names | ||||
This exists for backward compatibility. Use hybriddict for new template | ||||
keywords. | ||||
""" | ||||
# no need to provide {path} to old-style list template | ||||
c = [{'name': k, 'source': v} for k, v in copies] | ||||
f = _showcompatlist(context, mapping, name, c, plural='file_copies') | ||||
copies = util.sortdict(copies) | ||||
return hybrid(f, copies, | ||||
lambda k: {'name': k, 'path': k, 'source': copies[k]}, | ||||
lambda k: '%s (%s)' % (k, copies[k])) | ||||
Yuya Nishihara
|
r39403 | def compatfileslist(context, mapping, name, files): | ||
"""Wrap list of file names to support old-style list template and field | ||||
names | ||||
This exists for backward compatibility. Use hybridlist for new template | ||||
keywords. | ||||
""" | ||||
f = _showcompatlist(context, mapping, name, files) | ||||
return hybrid(f, files, lambda x: {'file': x, 'path': x}, | ||||
pycompat.identity) | ||||
Yuya Nishihara
|
r37086 | def _showcompatlist(context, mapping, name, values, plural=None, separator=' '): | ||
"""Return a generator that renders old-style list template | ||||
Yuya Nishihara
|
r36939 | 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'. | ||||
Yuya Nishihara
|
r37086 | """ | ||
Yuya Nishihara
|
r36939 | if not plural: | ||
plural = name + 's' | ||||
if not values: | ||||
noname = 'no_' + plural | ||||
Yuya Nishihara
|
r37086 | if context.preload(noname): | ||
yield context.process(noname, mapping) | ||||
Yuya Nishihara
|
r36939 | return | ||
Yuya Nishihara
|
r37086 | if not context.preload(name): | ||
Yuya Nishihara
|
r36939 | 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 | ||||
Yuya Nishihara
|
r37086 | if context.preload(startname): | ||
yield context.process(startname, mapping) | ||||
Yuya Nishihara
|
r36939 | def one(v, tag=name): | ||
Yuya Nishihara
|
r37092 | vmapping = {} | ||
Yuya Nishihara
|
r36939 | 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 | ||||
Yuya Nishihara
|
r37092 | vmapping = context.overlaymap(mapping, vmapping) | ||
Yuya Nishihara
|
r37086 | return context.process(tag, vmapping) | ||
Yuya Nishihara
|
r36939 | lastname = 'last_' + name | ||
Yuya Nishihara
|
r37086 | if context.preload(lastname): | ||
Yuya Nishihara
|
r36939 | 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 | ||||
Yuya Nishihara
|
r37086 | if context.preload(endname): | ||
yield context.process(endname, mapping) | ||||
Yuya Nishihara
|
r36939 | |||
Yuya Nishihara
|
r37290 | def flatten(context, mapping, thing): | ||
Yuya Nishihara
|
r37174 | """Yield a single stream from a possibly nested set of iterators""" | ||
Yuya Nishihara
|
r38289 | if isinstance(thing, wrapped): | ||
thing = thing.show(context, mapping) | ||||
Yuya Nishihara
|
r37174 | if isinstance(thing, bytes): | ||
yield thing | ||||
elif isinstance(thing, str): | ||||
# We can only hit this on Python 3, and it's here to guard | ||||
# against infinite recursion. | ||||
raise error.ProgrammingError('Mercurial IO including templates is done' | ||||
' with bytes, not strings, got %r' % thing) | ||||
elif thing is None: | ||||
pass | ||||
elif not util.safehasattr(thing, '__iter__'): | ||||
yield pycompat.bytestr(thing) | ||||
else: | ||||
for i in thing: | ||||
Yuya Nishihara
|
r38289 | if isinstance(i, wrapped): | ||
i = i.show(context, mapping) | ||||
Yuya Nishihara
|
r37174 | if isinstance(i, bytes): | ||
yield i | ||||
elif i is None: | ||||
pass | ||||
elif not util.safehasattr(i, '__iter__'): | ||||
yield pycompat.bytestr(i) | ||||
else: | ||||
Yuya Nishihara
|
r37290 | for j in flatten(context, mapping, i): | ||
Yuya Nishihara
|
r37174 | yield j | ||
Yuya Nishihara
|
r37290 | def stringify(context, mapping, thing): | ||
Yuya Nishihara
|
r36938 | """Turn values into bytes by converting into text and concatenating them""" | ||
Yuya Nishihara
|
r37175 | if isinstance(thing, bytes): | ||
return thing # retain localstr to be round-tripped | ||||
Yuya Nishihara
|
r37290 | return b''.join(flatten(context, mapping, thing)) | ||
Yuya Nishihara
|
r36938 | |||
Yuya Nishihara
|
r36931 | def findsymbolicname(arg): | ||
"""Find symbolic name for the given compiled expression; returns None | ||||
if nothing found reliably""" | ||||
while True: | ||||
func, data = arg | ||||
if func is runsymbol: | ||||
return data | ||||
elif func is runfilter: | ||||
arg = data[0] | ||||
else: | ||||
return None | ||||
Yuya Nishihara
|
r38308 | def _nonempty(xiter): | ||
try: | ||||
next(xiter) | ||||
return True | ||||
except StopIteration: | ||||
return False | ||||
Yuya Nishihara
|
r37296 | def _unthunk(context, mapping, thing): | ||
"""Evaluate a lazy byte string into value""" | ||||
if not isinstance(thing, types.GeneratorType): | ||||
return thing | ||||
return stringify(context, mapping, thing) | ||||
Yuya Nishihara
|
r36931 | def evalrawexp(context, mapping, arg): | ||
"""Evaluate given argument as a bare template object which may require | ||||
further processing (such as folding generator of strings)""" | ||||
func, data = arg | ||||
return func(context, mapping, data) | ||||
Yuya Nishihara
|
r38228 | def evalwrapped(context, mapping, arg): | ||
"""Evaluate given argument to wrapped object""" | ||||
thing = evalrawexp(context, mapping, arg) | ||||
return makewrapped(context, mapping, thing) | ||||
def makewrapped(context, mapping, thing): | ||||
"""Lift object to a wrapped type""" | ||||
if isinstance(thing, wrapped): | ||||
return thing | ||||
thing = _unthunk(context, mapping, thing) | ||||
if isinstance(thing, bytes): | ||||
return wrappedbytes(thing) | ||||
return wrappedvalue(thing) | ||||
Yuya Nishihara
|
r36931 | def evalfuncarg(context, mapping, arg): | ||
"""Evaluate given argument as value type""" | ||||
Yuya Nishihara
|
r38227 | return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg)) | ||
Yuya Nishihara
|
r37178 | |||
Yuya Nishihara
|
r38227 | def unwrapvalue(context, mapping, thing): | ||
"""Move the inner value object out of the wrapper""" | ||||
Yuya Nishihara
|
r38226 | if isinstance(thing, wrapped): | ||
return thing.tovalue(context, mapping) | ||||
Yuya Nishihara
|
r36931 | # evalrawexp() may return string, generator of strings or arbitrary object | ||
# such as date tuple, but filter does not want generator. | ||||
Yuya Nishihara
|
r37296 | return _unthunk(context, mapping, thing) | ||
Yuya Nishihara
|
r36931 | |||
def evalboolean(context, mapping, arg): | ||||
"""Evaluate given argument as boolean, but also takes boolean literals""" | ||||
func, data = arg | ||||
if func is runsymbol: | ||||
thing = func(context, mapping, data, default=None) | ||||
if thing is None: | ||||
# not a template keyword, takes as a boolean literal | ||||
Yuya Nishihara
|
r37102 | thing = stringutil.parsebool(data) | ||
Yuya Nishihara
|
r36931 | else: | ||
thing = func(context, mapping, data) | ||||
Yuya Nishihara
|
r38308 | return makewrapped(context, mapping, thing).tobool(context, mapping) | ||
Yuya Nishihara
|
r36931 | |||
Yuya Nishihara
|
r37241 | def evaldate(context, mapping, arg, err=None): | ||
"""Evaluate given argument as a date tuple or a date string; returns | ||||
a (unixtime, offset) tuple""" | ||||
Yuya Nishihara
|
r37290 | thing = evalrawexp(context, mapping, arg) | ||
return unwrapdate(context, mapping, thing, err) | ||||
Yuya Nishihara
|
r37241 | |||
Yuya Nishihara
|
r37290 | def unwrapdate(context, mapping, thing, err=None): | ||
Yuya Nishihara
|
r38304 | if isinstance(thing, date): | ||
return thing.tovalue(context, mapping) | ||||
# TODO: update hgweb to not return bare tuple; then just stringify 'thing' | ||||
Yuya Nishihara
|
r38227 | thing = unwrapvalue(context, mapping, thing) | ||
Yuya Nishihara
|
r37241 | try: | ||
return dateutil.parsedate(thing) | ||||
except AttributeError: | ||||
raise error.ParseError(err or _('not a date tuple nor a string')) | ||||
Yuya Nishihara
|
r37242 | except error.ParseError: | ||
if not err: | ||||
raise | ||||
raise error.ParseError(err) | ||||
Yuya Nishihara
|
r37241 | |||
Yuya Nishihara
|
r36931 | def evalinteger(context, mapping, arg, err=None): | ||
Yuya Nishihara
|
r37290 | thing = evalrawexp(context, mapping, arg) | ||
return unwrapinteger(context, mapping, thing, err) | ||||
Yuya Nishihara
|
r37179 | |||
Yuya Nishihara
|
r37290 | def unwrapinteger(context, mapping, thing, err=None): | ||
Yuya Nishihara
|
r38227 | thing = unwrapvalue(context, mapping, thing) | ||
Yuya Nishihara
|
r36931 | try: | ||
Yuya Nishihara
|
r37179 | return int(thing) | ||
Yuya Nishihara
|
r36931 | except (TypeError, ValueError): | ||
raise error.ParseError(err or _('not an integer')) | ||||
def evalstring(context, mapping, arg): | ||||
Yuya Nishihara
|
r37290 | return stringify(context, mapping, evalrawexp(context, mapping, arg)) | ||
Yuya Nishihara
|
r36931 | |||
def evalstringliteral(context, mapping, arg): | ||||
"""Evaluate given argument as string template, but returns symbol name | ||||
if it is unknown""" | ||||
func, data = arg | ||||
if func is runsymbol: | ||||
thing = func(context, mapping, data, default=data) | ||||
else: | ||||
thing = func(context, mapping, data) | ||||
Yuya Nishihara
|
r37290 | return stringify(context, mapping, thing) | ||
Yuya Nishihara
|
r36931 | |||
Yuya Nishihara
|
r37180 | _unwrapfuncbytype = { | ||
Yuya Nishihara
|
r38227 | None: unwrapvalue, | ||
Yuya Nishihara
|
r37180 | bytes: stringify, | ||
Yuya Nishihara
|
r37244 | date: unwrapdate, | ||
Yuya Nishihara
|
r37180 | int: unwrapinteger, | ||
Yuya Nishihara
|
r36931 | } | ||
Yuya Nishihara
|
r37290 | def unwrapastype(context, mapping, thing, typ): | ||
Yuya Nishihara
|
r37180 | """Move the inner value object out of the wrapper and coerce its type""" | ||
Yuya Nishihara
|
r36931 | try: | ||
Yuya Nishihara
|
r37180 | f = _unwrapfuncbytype[typ] | ||
Yuya Nishihara
|
r36931 | except KeyError: | ||
raise error.ProgrammingError('invalid type specified: %r' % typ) | ||||
Yuya Nishihara
|
r37290 | return f(context, mapping, thing) | ||
Yuya Nishihara
|
r36931 | |||
def runinteger(context, mapping, data): | ||||
return int(data) | ||||
def runstring(context, mapping, data): | ||||
return data | ||||
def _recursivesymbolblocker(key): | ||||
Yuya Nishihara
|
r38966 | def showrecursion(context, mapping): | ||
Yuya Nishihara
|
r36931 | raise error.Abort(_("recursive reference '%s' in template") % key) | ||
Yuya Nishihara
|
r38966 | showrecursion._requires = () # mark as new-style templatekw | ||
Yuya Nishihara
|
r36931 | return showrecursion | ||
def runsymbol(context, mapping, key, default=''): | ||||
v = context.symbol(mapping, key) | ||||
if v is None: | ||||
# put poison to cut recursion. we can't move this to parsing phase | ||||
# because "x = {x}" is allowed if "x" is a keyword. (issue4758) | ||||
safemapping = mapping.copy() | ||||
safemapping[key] = _recursivesymbolblocker(key) | ||||
try: | ||||
v = context.process(key, safemapping) | ||||
except TemplateNotFound: | ||||
v = default | ||||
if callable(v) and getattr(v, '_requires', None) is None: | ||||
# old templatekw: expand all keywords and resources | ||||
Yuya Nishihara
|
r38966 | # (TODO: drop support for old-style functions. 'f._requires = ()' | ||
# can be removed.) | ||||
Yuya Nishihara
|
r39618 | props = {k: context._resources.lookup(mapping, k) | ||
Yuya Nishihara
|
r37091 | for k in context._resources.knownkeys()} | ||
Yuya Nishihara
|
r37088 | # pass context to _showcompatlist() through templatekw._showlist() | ||
props['templ'] = context | ||||
Yuya Nishihara
|
r36931 | props.update(mapping) | ||
Yuya Nishihara
|
r38966 | ui = props.get('ui') | ||
if ui: | ||||
ui.deprecwarn("old-style template keyword '%s'" % key, '4.8') | ||||
Yuya Nishihara
|
r36931 | return v(**pycompat.strkwargs(props)) | ||
if callable(v): | ||||
# new templatekw | ||||
try: | ||||
return v(context, mapping) | ||||
except ResourceUnavailable: | ||||
# unsupported keyword is mapped to empty just like unknown keyword | ||||
return None | ||||
return v | ||||
def runtemplate(context, mapping, template): | ||||
for arg in template: | ||||
yield evalrawexp(context, mapping, arg) | ||||
def runfilter(context, mapping, data): | ||||
arg, filt = data | ||||
Yuya Nishihara
|
r37239 | thing = evalrawexp(context, mapping, arg) | ||
Yuya Nishihara
|
r37290 | intype = getattr(filt, '_intype', None) | ||
Yuya Nishihara
|
r36931 | try: | ||
Yuya Nishihara
|
r37290 | thing = unwrapastype(context, mapping, thing, intype) | ||
Yuya Nishihara
|
r36931 | return filt(thing) | ||
Yuya Nishihara
|
r37243 | except error.ParseError as e: | ||
raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt)) | ||||
def _formatfiltererror(arg, filt): | ||||
fn = pycompat.sysbytes(filt.__name__) | ||||
sym = findsymbolicname(arg) | ||||
if not sym: | ||||
return _("incompatible use of template filter '%s'") % fn | ||||
return (_("template filter '%s' is not compatible with keyword '%s'") | ||||
% (fn, sym)) | ||||
Yuya Nishihara
|
r36931 | |||
Yuya Nishihara
|
r37417 | def _iteroverlaymaps(context, origmapping, newmappings): | ||
"""Generate combined mappings from the original mapping and an iterable | ||||
of partial mappings to override the original""" | ||||
for i, nm in enumerate(newmappings): | ||||
lm = context.overlaymap(origmapping, nm) | ||||
lm['index'] = i | ||||
yield lm | ||||
Yuya Nishihara
|
r38290 | def _applymap(context, mapping, d, darg, targ): | ||
try: | ||||
diter = d.itermaps(context) | ||||
except error.ParseError as err: | ||||
sym = findsymbolicname(darg) | ||||
if not sym: | ||||
raise | ||||
hint = _("keyword '%s' does not support map operation") % sym | ||||
raise error.ParseError(bytes(err), hint=hint) | ||||
for lm in _iteroverlaymaps(context, mapping, diter): | ||||
Yuya Nishihara
|
r37517 | yield evalrawexp(context, lm, targ) | ||
Yuya Nishihara
|
r36931 | def runmap(context, mapping, data): | ||
darg, targ = data | ||||
Yuya Nishihara
|
r38231 | d = evalwrapped(context, mapping, darg) | ||
Yuya Nishihara
|
r38290 | return mappedgenerator(_applymap, args=(mapping, d, darg, targ)) | ||
Yuya Nishihara
|
r36931 | |||
def runmember(context, mapping, data): | ||||
darg, memb = data | ||||
Yuya Nishihara
|
r38258 | d = evalwrapped(context, mapping, darg) | ||
Yuya Nishihara
|
r38303 | if isinstance(d, mappable): | ||
lm = context.overlaymap(mapping, d.tomap(context)) | ||||
Yuya Nishihara
|
r36931 | return runsymbol(context, lm, memb) | ||
Yuya Nishihara
|
r38259 | try: | ||
Yuya Nishihara
|
r38261 | return d.getmember(context, mapping, memb) | ||
except error.ParseError as err: | ||||
Yuya Nishihara
|
r38259 | sym = findsymbolicname(darg) | ||
Yuya Nishihara
|
r38261 | if not sym: | ||
raise | ||||
hint = _("keyword '%s' does not support member operation") % sym | ||||
raise error.ParseError(bytes(err), hint=hint) | ||||
Yuya Nishihara
|
r36931 | |||
def runnegate(context, mapping, data): | ||||
data = evalinteger(context, mapping, data, | ||||
_('negation needs an integer argument')) | ||||
return -data | ||||
def runarithmetic(context, mapping, data): | ||||
func, left, right = data | ||||
left = evalinteger(context, mapping, left, | ||||
_('arithmetic only defined on integers')) | ||||
right = evalinteger(context, mapping, right, | ||||
_('arithmetic only defined on integers')) | ||||
try: | ||||
return func(left, right) | ||||
except ZeroDivisionError: | ||||
raise error.Abort(_('division by zero is not defined')) | ||||
Yuya Nishihara
|
r37341 | def joinitems(itemiter, sep): | ||
"""Join items with the separator; Returns generator of bytes""" | ||||
first = True | ||||
for x in itemiter: | ||||
if first: | ||||
first = False | ||||
Yuya Nishihara
|
r37342 | elif sep: | ||
Yuya Nishihara
|
r37341 | yield sep | ||
yield x | ||||