mercurial/templateutil.py
changeset 36921 32f9b7e3f056
parent 36920 6ff6e1d6b5b8
child 36982 255f635c3204
equal deleted inserted replaced
36920:6ff6e1d6b5b8 36921:32f9b7e3f056
    11 
    11 
    12 from .i18n import _
    12 from .i18n import _
    13 from . import (
    13 from . import (
    14     error,
    14     error,
    15     pycompat,
    15     pycompat,
    16     templatekw,
       
    17     util,
    16     util,
    18 )
    17 )
    19 
    18 
    20 class ResourceUnavailable(error.Abort):
    19 class ResourceUnavailable(error.Abort):
    21     pass
    20     pass
    22 
    21 
    23 class TemplateNotFound(error.Abort):
    22 class TemplateNotFound(error.Abort):
    24     pass
    23     pass
    25 
    24 
       
    25 class hybrid(object):
       
    26     """Wrapper for list or dict to support legacy template
       
    27 
       
    28     This class allows us to handle both:
       
    29     - "{files}" (legacy command-line-specific list hack) and
       
    30     - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
       
    31     and to access raw values:
       
    32     - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
       
    33     - "{get(extras, key)}"
       
    34     - "{files|json}"
       
    35     """
       
    36 
       
    37     def __init__(self, gen, values, makemap, joinfmt, keytype=None):
       
    38         if gen is not None:
       
    39             self.gen = gen  # generator or function returning generator
       
    40         self._values = values
       
    41         self._makemap = makemap
       
    42         self.joinfmt = joinfmt
       
    43         self.keytype = keytype  # hint for 'x in y' where type(x) is unresolved
       
    44     def gen(self):
       
    45         """Default generator to stringify this as {join(self, ' ')}"""
       
    46         for i, x in enumerate(self._values):
       
    47             if i > 0:
       
    48                 yield ' '
       
    49             yield self.joinfmt(x)
       
    50     def itermaps(self):
       
    51         makemap = self._makemap
       
    52         for x in self._values:
       
    53             yield makemap(x)
       
    54     def __contains__(self, x):
       
    55         return x in self._values
       
    56     def __getitem__(self, key):
       
    57         return self._values[key]
       
    58     def __len__(self):
       
    59         return len(self._values)
       
    60     def __iter__(self):
       
    61         return iter(self._values)
       
    62     def __getattr__(self, name):
       
    63         if name not in (r'get', r'items', r'iteritems', r'iterkeys',
       
    64                         r'itervalues', r'keys', r'values'):
       
    65             raise AttributeError(name)
       
    66         return getattr(self._values, name)
       
    67 
       
    68 class mappable(object):
       
    69     """Wrapper for non-list/dict object to support map operation
       
    70 
       
    71     This class allows us to handle both:
       
    72     - "{manifest}"
       
    73     - "{manifest % '{rev}:{node}'}"
       
    74     - "{manifest.rev}"
       
    75 
       
    76     Unlike a hybrid, this does not simulate the behavior of the underling
       
    77     value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
       
    78     """
       
    79 
       
    80     def __init__(self, gen, key, value, makemap):
       
    81         if gen is not None:
       
    82             self.gen = gen  # generator or function returning generator
       
    83         self._key = key
       
    84         self._value = value  # may be generator of strings
       
    85         self._makemap = makemap
       
    86 
       
    87     def gen(self):
       
    88         yield pycompat.bytestr(self._value)
       
    89 
       
    90     def tomap(self):
       
    91         return self._makemap(self._key)
       
    92 
       
    93     def itermaps(self):
       
    94         yield self.tomap()
       
    95 
       
    96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
       
    97     """Wrap data to support both dict-like and string-like operations"""
       
    98     prefmt = pycompat.identity
       
    99     if fmt is None:
       
   100         fmt = '%s=%s'
       
   101         prefmt = pycompat.bytestr
       
   102     return hybrid(gen, data, lambda k: {key: k, value: data[k]},
       
   103                   lambda k: fmt % (prefmt(k), prefmt(data[k])))
       
   104 
       
   105 def hybridlist(data, name, fmt=None, gen=None):
       
   106     """Wrap data to support both list-like and string-like operations"""
       
   107     prefmt = pycompat.identity
       
   108     if fmt is None:
       
   109         fmt = '%s'
       
   110         prefmt = pycompat.bytestr
       
   111     return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
       
   112 
       
   113 def unwraphybrid(thing):
       
   114     """Return an object which can be stringified possibly by using a legacy
       
   115     template"""
       
   116     gen = getattr(thing, 'gen', None)
       
   117     if gen is None:
       
   118         return thing
       
   119     if callable(gen):
       
   120         return gen()
       
   121     return gen
       
   122 
       
   123 def unwrapvalue(thing):
       
   124     """Move the inner value object out of the wrapper"""
       
   125     if not util.safehasattr(thing, '_value'):
       
   126         return thing
       
   127     return thing._value
       
   128 
       
   129 def wraphybridvalue(container, key, value):
       
   130     """Wrap an element of hybrid container to be mappable
       
   131 
       
   132     The key is passed to the makemap function of the given container, which
       
   133     should be an item generated by iter(container).
       
   134     """
       
   135     makemap = getattr(container, '_makemap', None)
       
   136     if makemap is None:
       
   137         return value
       
   138     if util.safehasattr(value, '_makemap'):
       
   139         # a nested hybrid list/dict, which has its own way of map operation
       
   140         return value
       
   141     return mappable(None, key, value, makemap)
       
   142 
       
   143 def compatdict(context, mapping, name, data, key='key', value='value',
       
   144                fmt=None, plural=None, separator=' '):
       
   145     """Wrap data like hybriddict(), but also supports old-style list template
       
   146 
       
   147     This exists for backward compatibility with the old-style template. Use
       
   148     hybriddict() for new template keywords.
       
   149     """
       
   150     c = [{key: k, value: v} for k, v in data.iteritems()]
       
   151     t = context.resource(mapping, 'templ')
       
   152     f = _showlist(name, c, t, mapping, plural, separator)
       
   153     return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
       
   154 
       
   155 def compatlist(context, mapping, name, data, element=None, fmt=None,
       
   156                plural=None, separator=' '):
       
   157     """Wrap data like hybridlist(), but also supports old-style list template
       
   158 
       
   159     This exists for backward compatibility with the old-style template. Use
       
   160     hybridlist() for new template keywords.
       
   161     """
       
   162     t = context.resource(mapping, 'templ')
       
   163     f = _showlist(name, data, t, mapping, plural, separator)
       
   164     return hybridlist(data, name=element or name, fmt=fmt, gen=f)
       
   165 
       
   166 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
       
   167     '''expand set of values.
       
   168     name is name of key in template map.
       
   169     values is list of strings or dicts.
       
   170     plural is plural of name, if not simply name + 's'.
       
   171     separator is used to join values as a string
       
   172 
       
   173     expansion works like this, given name 'foo'.
       
   174 
       
   175     if values is empty, expand 'no_foos'.
       
   176 
       
   177     if 'foo' not in template map, return values as a string,
       
   178     joined by 'separator'.
       
   179 
       
   180     expand 'start_foos'.
       
   181 
       
   182     for each value, expand 'foo'. if 'last_foo' in template
       
   183     map, expand it instead of 'foo' for last key.
       
   184 
       
   185     expand 'end_foos'.
       
   186     '''
       
   187     strmapping = pycompat.strkwargs(mapping)
       
   188     if not plural:
       
   189         plural = name + 's'
       
   190     if not values:
       
   191         noname = 'no_' + plural
       
   192         if noname in templ:
       
   193             yield templ(noname, **strmapping)
       
   194         return
       
   195     if name not in templ:
       
   196         if isinstance(values[0], bytes):
       
   197             yield separator.join(values)
       
   198         else:
       
   199             for v in values:
       
   200                 r = dict(v)
       
   201                 r.update(mapping)
       
   202                 yield r
       
   203         return
       
   204     startname = 'start_' + plural
       
   205     if startname in templ:
       
   206         yield templ(startname, **strmapping)
       
   207     vmapping = mapping.copy()
       
   208     def one(v, tag=name):
       
   209         try:
       
   210             vmapping.update(v)
       
   211         # Python 2 raises ValueError if the type of v is wrong. Python
       
   212         # 3 raises TypeError.
       
   213         except (AttributeError, TypeError, ValueError):
       
   214             try:
       
   215                 # Python 2 raises ValueError trying to destructure an e.g.
       
   216                 # bytes. Python 3 raises TypeError.
       
   217                 for a, b in v:
       
   218                     vmapping[a] = b
       
   219             except (TypeError, ValueError):
       
   220                 vmapping[name] = v
       
   221         return templ(tag, **pycompat.strkwargs(vmapping))
       
   222     lastname = 'last_' + name
       
   223     if lastname in templ:
       
   224         last = values.pop()
       
   225     else:
       
   226         last = None
       
   227     for v in values:
       
   228         yield one(v)
       
   229     if last is not None:
       
   230         yield one(last, tag=lastname)
       
   231     endname = 'end_' + plural
       
   232     if endname in templ:
       
   233         yield templ(endname, **strmapping)
       
   234 
    26 def stringify(thing):
   235 def stringify(thing):
    27     """Turn values into bytes by converting into text and concatenating them"""
   236     """Turn values into bytes by converting into text and concatenating them"""
    28     thing = templatekw.unwraphybrid(thing)
   237     thing = unwraphybrid(thing)
    29     if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
   238     if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
    30         if isinstance(thing, str):
   239         if isinstance(thing, str):
    31             # This is only reachable on Python 3 (otherwise
   240             # This is only reachable on Python 3 (otherwise
    32             # isinstance(thing, bytes) would have been true), and is
   241             # isinstance(thing, bytes) would have been true), and is
    33             # here to prevent infinite recursion bugs on Python 3.
   242             # here to prevent infinite recursion bugs on Python 3.
    57     return func(context, mapping, data)
   266     return func(context, mapping, data)
    58 
   267 
    59 def evalfuncarg(context, mapping, arg):
   268 def evalfuncarg(context, mapping, arg):
    60     """Evaluate given argument as value type"""
   269     """Evaluate given argument as value type"""
    61     thing = evalrawexp(context, mapping, arg)
   270     thing = evalrawexp(context, mapping, arg)
    62     thing = templatekw.unwrapvalue(thing)
   271     thing = unwrapvalue(thing)
    63     # evalrawexp() may return string, generator of strings or arbitrary object
   272     # evalrawexp() may return string, generator of strings or arbitrary object
    64     # such as date tuple, but filter does not want generator.
   273     # such as date tuple, but filter does not want generator.
    65     if isinstance(thing, types.GeneratorType):
   274     if isinstance(thing, types.GeneratorType):
    66         thing = stringify(thing)
   275         thing = stringify(thing)
    67     return thing
   276     return thing
    74         if thing is None:
   283         if thing is None:
    75             # not a template keyword, takes as a boolean literal
   284             # not a template keyword, takes as a boolean literal
    76             thing = util.parsebool(data)
   285             thing = util.parsebool(data)
    77     else:
   286     else:
    78         thing = func(context, mapping, data)
   287         thing = func(context, mapping, data)
    79     thing = templatekw.unwrapvalue(thing)
   288     thing = unwrapvalue(thing)
    80     if isinstance(thing, bool):
   289     if isinstance(thing, bool):
    81         return thing
   290         return thing
    82     # other objects are evaluated as strings, which means 0 is True, but
   291     # other objects are evaluated as strings, which means 0 is True, but
    83     # empty dict/list should be False as they are expected to be ''
   292     # empty dict/list should be False as they are expected to be ''
    84     return bool(stringify(thing))
   293     return bool(stringify(thing))
   234 
   443 
   235 def getdictitem(dictarg, key):
   444 def getdictitem(dictarg, key):
   236     val = dictarg.get(key)
   445     val = dictarg.get(key)
   237     if val is None:
   446     if val is None:
   238         return
   447         return
   239     return templatekw.wraphybridvalue(dictarg, key, val)
   448     return wraphybridvalue(dictarg, key, val)