Show More
@@ -143,7 +143,7 b' from mercurial import (' | |||||
143 | registrar, |
|
143 | registrar, | |
144 | revlog, |
|
144 | revlog, | |
145 | scmutil, |
|
145 | scmutil, | |
146 |
template |
|
146 | templateutil, | |
147 | upgrade, |
|
147 | upgrade, | |
148 | util, |
|
148 | util, | |
149 | vfs as vfsmod, |
|
149 | vfs as vfsmod, | |
@@ -375,12 +375,12 b' def lfsfiles(context, mapping):' | |||||
375 | makemap = lambda v: { |
|
375 | makemap = lambda v: { | |
376 | 'file': v, |
|
376 | 'file': v, | |
377 | 'lfsoid': pointers[v].oid() if pointers[v] else None, |
|
377 | 'lfsoid': pointers[v].oid() if pointers[v] else None, | |
378 |
'lfspointer': template |
|
378 | 'lfspointer': templateutil.hybriddict(pointer(v)), | |
379 | } |
|
379 | } | |
380 |
|
380 | |||
381 | # TODO: make the separator ', '? |
|
381 | # TODO: make the separator ', '? | |
382 |
f = template |
|
382 | f = templateutil._showlist('lfs_file', files, templ, mapping) | |
383 |
return template |
|
383 | return templateutil.hybrid(f, files, makemap, pycompat.identity) | |
384 |
|
384 | |||
385 | @command('debuglfsupload', |
|
385 | @command('debuglfsupload', | |
386 | [('r', 'rev', [], _('upload large files introduced by REV'))]) |
|
386 | [('r', 'rev', [], _('upload large files introduced by REV'))]) |
@@ -35,7 +35,7 b' from mercurial import (' | |||||
35 | registrar, |
|
35 | registrar, | |
36 | revsetlang, |
|
36 | revsetlang, | |
37 | smartset, |
|
37 | smartset, | |
38 |
template |
|
38 | templateutil, | |
39 | ) |
|
39 | ) | |
40 |
|
40 | |||
41 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
41 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | |
@@ -237,8 +237,8 b' def remotenameskw(context, mapping):' | |||||
237 | if 'remotebranches' in repo.names: |
|
237 | if 'remotebranches' in repo.names: | |
238 | remotenames += repo.names['remotebranches'].names(repo, ctx.node()) |
|
238 | remotenames += repo.names['remotebranches'].names(repo, ctx.node()) | |
239 |
|
239 | |||
240 |
return template |
|
240 | return templateutil.compatlist(context, mapping, 'remotename', remotenames, | |
241 | plural='remotenames') |
|
241 | plural='remotenames') | |
242 |
|
242 | |||
243 | @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'}) |
|
243 | @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'}) | |
244 | def remotebookmarkskw(context, mapping): |
|
244 | def remotebookmarkskw(context, mapping): | |
@@ -250,8 +250,8 b' def remotebookmarkskw(context, mapping):' | |||||
250 | if 'remotebookmarks' in repo.names: |
|
250 | if 'remotebookmarks' in repo.names: | |
251 | remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node()) |
|
251 | remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node()) | |
252 |
|
252 | |||
253 |
return template |
|
253 | return templateutil.compatlist(context, mapping, 'remotebookmark', | |
254 | remotebmarks, plural='remotebookmarks') |
|
254 | remotebmarks, plural='remotebookmarks') | |
255 |
|
255 | |||
256 | @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'}) |
|
256 | @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'}) | |
257 | def remotebrancheskw(context, mapping): |
|
257 | def remotebrancheskw(context, mapping): | |
@@ -263,8 +263,8 b' def remotebrancheskw(context, mapping):' | |||||
263 | if 'remotebranches' in repo.names: |
|
263 | if 'remotebranches' in repo.names: | |
264 | remotebranches = repo.names['remotebranches'].names(repo, ctx.node()) |
|
264 | remotebranches = repo.names['remotebranches'].names(repo, ctx.node()) | |
265 |
|
265 | |||
266 |
return template |
|
266 | return templateutil.compatlist(context, mapping, 'remotebranch', | |
267 | remotebranches, plural='remotebranches') |
|
267 | remotebranches, plural='remotebranches') | |
268 |
|
268 | |||
269 | def _revsetutil(repo, subset, x, rtypes): |
|
269 | def _revsetutil(repo, subset, x, rtypes): | |
270 | """utility function to return a set of revs based on the rtypes""" |
|
270 | """utility function to return a set of revs based on the rtypes""" |
@@ -359,14 +359,15 b' class _templateconverter(object):' | |||||
359 | data = util.sortdict(_iteritems(data)) |
|
359 | data = util.sortdict(_iteritems(data)) | |
360 | def f(): |
|
360 | def f(): | |
361 | yield _plainconverter.formatdict(data, key, value, fmt, sep) |
|
361 | yield _plainconverter.formatdict(data, key, value, fmt, sep) | |
362 |
return template |
|
362 | return templateutil.hybriddict(data, key=key, value=value, fmt=fmt, | |
|
363 | gen=f) | |||
363 | @staticmethod |
|
364 | @staticmethod | |
364 | def formatlist(data, name, fmt, sep): |
|
365 | def formatlist(data, name, fmt, sep): | |
365 | '''build object that can be evaluated as either plain string or list''' |
|
366 | '''build object that can be evaluated as either plain string or list''' | |
366 | data = list(data) |
|
367 | data = list(data) | |
367 | def f(): |
|
368 | def f(): | |
368 | yield _plainconverter.formatlist(data, name, fmt, sep) |
|
369 | yield _plainconverter.formatlist(data, name, fmt, sep) | |
369 |
return template |
|
370 | return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f) | |
370 |
|
371 | |||
371 | class templateformatter(baseformatter): |
|
372 | class templateformatter(baseformatter): | |
372 | def __init__(self, ui, out, topic, opts): |
|
373 | def __init__(self, ui, out, topic, opts): |
@@ -17,7 +17,6 b' from . import (' | |||||
17 | node, |
|
17 | node, | |
18 | pycompat, |
|
18 | pycompat, | |
19 | registrar, |
|
19 | registrar, | |
20 | templatekw, |
|
|||
21 | templateutil, |
|
20 | templateutil, | |
22 | url, |
|
21 | url, | |
23 | util, |
|
22 | util, | |
@@ -366,7 +365,7 b' def slashpath(path):' | |||||
366 | @templatefilter('splitlines') |
|
365 | @templatefilter('splitlines') | |
367 | def splitlines(text): |
|
366 | def splitlines(text): | |
368 | """Any text. Split text into a list of lines.""" |
|
367 | """Any text. Split text into a list of lines.""" | |
369 |
return template |
|
368 | return templateutil.hybridlist(text.splitlines(), name='line') | |
370 |
|
369 | |||
371 | @templatefilter('stringescape') |
|
370 | @templatefilter('stringescape') | |
372 | def stringescape(text): |
|
371 | def stringescape(text): |
@@ -23,156 +23,24 b' from . import (' | |||||
23 | pycompat, |
|
23 | pycompat, | |
24 | registrar, |
|
24 | registrar, | |
25 | scmutil, |
|
25 | scmutil, | |
|
26 | templateutil, | |||
26 | util, |
|
27 | util, | |
27 | ) |
|
28 | ) | |
28 |
|
29 | |||
29 | class _hybrid(object): |
|
30 | _hybrid = templateutil.hybrid | |
30 | """Wrapper for list or dict to support legacy template |
|
31 | _mappable = templateutil.mappable | |
31 |
|
32 | _showlist = templateutil._showlist | ||
32 | This class allows us to handle both: |
|
33 | hybriddict = templateutil.hybriddict | |
33 | - "{files}" (legacy command-line-specific list hack) and |
|
34 | hybridlist = templateutil.hybridlist | |
34 | - "{files % '{file}\n'}" (hgweb-style with inlining and function support) |
|
35 | compatdict = templateutil.compatdict | |
35 | and to access raw values: |
|
36 | compatlist = templateutil.compatlist | |
36 | - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}" |
|
|||
37 | - "{get(extras, key)}" |
|
|||
38 | - "{files|json}" |
|
|||
39 | """ |
|
|||
40 |
|
||||
41 | def __init__(self, gen, values, makemap, joinfmt, keytype=None): |
|
|||
42 | if gen is not None: |
|
|||
43 | self.gen = gen # generator or function returning generator |
|
|||
44 | self._values = values |
|
|||
45 | self._makemap = makemap |
|
|||
46 | self.joinfmt = joinfmt |
|
|||
47 | self.keytype = keytype # hint for 'x in y' where type(x) is unresolved |
|
|||
48 | def gen(self): |
|
|||
49 | """Default generator to stringify this as {join(self, ' ')}""" |
|
|||
50 | for i, x in enumerate(self._values): |
|
|||
51 | if i > 0: |
|
|||
52 | yield ' ' |
|
|||
53 | yield self.joinfmt(x) |
|
|||
54 | def itermaps(self): |
|
|||
55 | makemap = self._makemap |
|
|||
56 | for x in self._values: |
|
|||
57 | yield makemap(x) |
|
|||
58 | def __contains__(self, x): |
|
|||
59 | return x in self._values |
|
|||
60 | def __getitem__(self, key): |
|
|||
61 | return self._values[key] |
|
|||
62 | def __len__(self): |
|
|||
63 | return len(self._values) |
|
|||
64 | def __iter__(self): |
|
|||
65 | return iter(self._values) |
|
|||
66 | def __getattr__(self, name): |
|
|||
67 | if name not in (r'get', r'items', r'iteritems', r'iterkeys', |
|
|||
68 | r'itervalues', r'keys', r'values'): |
|
|||
69 | raise AttributeError(name) |
|
|||
70 | return getattr(self._values, name) |
|
|||
71 |
|
||||
72 | class _mappable(object): |
|
|||
73 | """Wrapper for non-list/dict object to support map operation |
|
|||
74 |
|
||||
75 | This class allows us to handle both: |
|
|||
76 | - "{manifest}" |
|
|||
77 | - "{manifest % '{rev}:{node}'}" |
|
|||
78 | - "{manifest.rev}" |
|
|||
79 |
|
||||
80 | Unlike a _hybrid, this does not simulate the behavior of the underling |
|
|||
81 | value. Use unwrapvalue() or unwraphybrid() to obtain the inner object. |
|
|||
82 | """ |
|
|||
83 |
|
||||
84 | def __init__(self, gen, key, value, makemap): |
|
|||
85 | if gen is not None: |
|
|||
86 | self.gen = gen # generator or function returning generator |
|
|||
87 | self._key = key |
|
|||
88 | self._value = value # may be generator of strings |
|
|||
89 | self._makemap = makemap |
|
|||
90 |
|
||||
91 | def gen(self): |
|
|||
92 | yield pycompat.bytestr(self._value) |
|
|||
93 |
|
||||
94 | def tomap(self): |
|
|||
95 | return self._makemap(self._key) |
|
|||
96 |
|
||||
97 | def itermaps(self): |
|
|||
98 | yield self.tomap() |
|
|||
99 |
|
||||
100 | def hybriddict(data, key='key', value='value', fmt=None, gen=None): |
|
|||
101 | """Wrap data to support both dict-like and string-like operations""" |
|
|||
102 | prefmt = pycompat.identity |
|
|||
103 | if fmt is None: |
|
|||
104 | fmt = '%s=%s' |
|
|||
105 | prefmt = pycompat.bytestr |
|
|||
106 | return _hybrid(gen, data, lambda k: {key: k, value: data[k]}, |
|
|||
107 | lambda k: fmt % (prefmt(k), prefmt(data[k]))) |
|
|||
108 |
|
||||
109 | def hybridlist(data, name, fmt=None, gen=None): |
|
|||
110 | """Wrap data to support both list-like and string-like operations""" |
|
|||
111 | prefmt = pycompat.identity |
|
|||
112 | if fmt is None: |
|
|||
113 | fmt = '%s' |
|
|||
114 | prefmt = pycompat.bytestr |
|
|||
115 | return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x)) |
|
|||
116 |
|
||||
117 | def unwraphybrid(thing): |
|
|||
118 | """Return an object which can be stringified possibly by using a legacy |
|
|||
119 | template""" |
|
|||
120 | gen = getattr(thing, 'gen', None) |
|
|||
121 | if gen is None: |
|
|||
122 | return thing |
|
|||
123 | if callable(gen): |
|
|||
124 | return gen() |
|
|||
125 | return gen |
|
|||
126 |
|
||||
127 | def unwrapvalue(thing): |
|
|||
128 | """Move the inner value object out of the wrapper""" |
|
|||
129 | if not util.safehasattr(thing, '_value'): |
|
|||
130 | return thing |
|
|||
131 | return thing._value |
|
|||
132 |
|
||||
133 | def wraphybridvalue(container, key, value): |
|
|||
134 | """Wrap an element of hybrid container to be mappable |
|
|||
135 |
|
||||
136 | The key is passed to the makemap function of the given container, which |
|
|||
137 | should be an item generated by iter(container). |
|
|||
138 | """ |
|
|||
139 | makemap = getattr(container, '_makemap', None) |
|
|||
140 | if makemap is None: |
|
|||
141 | return value |
|
|||
142 | if util.safehasattr(value, '_makemap'): |
|
|||
143 | # a nested hybrid list/dict, which has its own way of map operation |
|
|||
144 | return value |
|
|||
145 | return _mappable(None, key, value, makemap) |
|
|||
146 |
|
||||
147 | def compatdict(context, mapping, name, data, key='key', value='value', |
|
|||
148 | fmt=None, plural=None, separator=' '): |
|
|||
149 | """Wrap data like hybriddict(), but also supports old-style list template |
|
|||
150 |
|
||||
151 | This exists for backward compatibility with the old-style template. Use |
|
|||
152 | hybriddict() for new template keywords. |
|
|||
153 | """ |
|
|||
154 | c = [{key: k, value: v} for k, v in data.iteritems()] |
|
|||
155 | t = context.resource(mapping, 'templ') |
|
|||
156 | f = _showlist(name, c, t, mapping, plural, separator) |
|
|||
157 | return hybriddict(data, key=key, value=value, fmt=fmt, gen=f) |
|
|||
158 |
|
||||
159 | def compatlist(context, mapping, name, data, element=None, fmt=None, |
|
|||
160 | plural=None, separator=' '): |
|
|||
161 | """Wrap data like hybridlist(), but also supports old-style list template |
|
|||
162 |
|
||||
163 | This exists for backward compatibility with the old-style template. Use |
|
|||
164 | hybridlist() for new template keywords. |
|
|||
165 | """ |
|
|||
166 | t = context.resource(mapping, 'templ') |
|
|||
167 | f = _showlist(name, data, t, mapping, plural, separator) |
|
|||
168 | return hybridlist(data, name=element or name, fmt=fmt, gen=f) |
|
|||
169 |
|
37 | |||
170 | def showdict(name, data, mapping, plural=None, key='key', value='value', |
|
38 | def showdict(name, data, mapping, plural=None, key='key', value='value', | |
171 | fmt=None, separator=' '): |
|
39 | fmt=None, separator=' '): | |
172 | ui = mapping.get('ui') |
|
40 | ui = mapping.get('ui') | |
173 | if ui: |
|
41 | if ui: | |
174 |
ui.deprecwarn("templatekw.showdict() is deprecated, use |
|
42 | ui.deprecwarn("templatekw.showdict() is deprecated, use " | |
175 | '4.6') |
|
43 | "templateutil.compatdict()", '4.6') | |
176 | c = [{key: k, value: v} for k, v in data.iteritems()] |
|
44 | c = [{key: k, value: v} for k, v in data.iteritems()] | |
177 | f = _showlist(name, c, mapping['templ'], mapping, plural, separator) |
|
45 | f = _showlist(name, c, mapping['templ'], mapping, plural, separator) | |
178 | return hybriddict(data, key=key, value=value, fmt=fmt, gen=f) |
|
46 | return hybriddict(data, key=key, value=value, fmt=fmt, gen=f) | |
@@ -180,82 +48,13 b' def showdict(name, data, mapping, plural' | |||||
180 | def showlist(name, values, mapping, plural=None, element=None, separator=' '): |
|
48 | def showlist(name, values, mapping, plural=None, element=None, separator=' '): | |
181 | ui = mapping.get('ui') |
|
49 | ui = mapping.get('ui') | |
182 | if ui: |
|
50 | if ui: | |
183 |
ui.deprecwarn("templatekw.showlist() is deprecated, use |
|
51 | ui.deprecwarn("templatekw.showlist() is deprecated, use " | |
184 | '4.6') |
|
52 | "templateutil.compatlist()", '4.6') | |
185 | if not element: |
|
53 | if not element: | |
186 | element = name |
|
54 | element = name | |
187 | f = _showlist(name, values, mapping['templ'], mapping, plural, separator) |
|
55 | f = _showlist(name, values, mapping['templ'], mapping, plural, separator) | |
188 | return hybridlist(values, name=element, gen=f) |
|
56 | return hybridlist(values, name=element, gen=f) | |
189 |
|
57 | |||
190 | def _showlist(name, values, templ, mapping, plural=None, separator=' '): |
|
|||
191 | '''expand set of values. |
|
|||
192 | name is name of key in template map. |
|
|||
193 | values is list of strings or dicts. |
|
|||
194 | plural is plural of name, if not simply name + 's'. |
|
|||
195 | separator is used to join values as a string |
|
|||
196 |
|
||||
197 | expansion works like this, given name 'foo'. |
|
|||
198 |
|
||||
199 | if values is empty, expand 'no_foos'. |
|
|||
200 |
|
||||
201 | if 'foo' not in template map, return values as a string, |
|
|||
202 | joined by 'separator'. |
|
|||
203 |
|
||||
204 | expand 'start_foos'. |
|
|||
205 |
|
||||
206 | for each value, expand 'foo'. if 'last_foo' in template |
|
|||
207 | map, expand it instead of 'foo' for last key. |
|
|||
208 |
|
||||
209 | expand 'end_foos'. |
|
|||
210 | ''' |
|
|||
211 | strmapping = pycompat.strkwargs(mapping) |
|
|||
212 | if not plural: |
|
|||
213 | plural = name + 's' |
|
|||
214 | if not values: |
|
|||
215 | noname = 'no_' + plural |
|
|||
216 | if noname in templ: |
|
|||
217 | yield templ(noname, **strmapping) |
|
|||
218 | return |
|
|||
219 | if name not in templ: |
|
|||
220 | if isinstance(values[0], bytes): |
|
|||
221 | yield separator.join(values) |
|
|||
222 | else: |
|
|||
223 | for v in values: |
|
|||
224 | r = dict(v) |
|
|||
225 | r.update(mapping) |
|
|||
226 | yield r |
|
|||
227 | return |
|
|||
228 | startname = 'start_' + plural |
|
|||
229 | if startname in templ: |
|
|||
230 | yield templ(startname, **strmapping) |
|
|||
231 | vmapping = mapping.copy() |
|
|||
232 | def one(v, tag=name): |
|
|||
233 | try: |
|
|||
234 | vmapping.update(v) |
|
|||
235 | # Python 2 raises ValueError if the type of v is wrong. Python |
|
|||
236 | # 3 raises TypeError. |
|
|||
237 | except (AttributeError, TypeError, ValueError): |
|
|||
238 | try: |
|
|||
239 | # Python 2 raises ValueError trying to destructure an e.g. |
|
|||
240 | # bytes. Python 3 raises TypeError. |
|
|||
241 | for a, b in v: |
|
|||
242 | vmapping[a] = b |
|
|||
243 | except (TypeError, ValueError): |
|
|||
244 | vmapping[name] = v |
|
|||
245 | return templ(tag, **pycompat.strkwargs(vmapping)) |
|
|||
246 | lastname = 'last_' + name |
|
|||
247 | if lastname in templ: |
|
|||
248 | last = values.pop() |
|
|||
249 | else: |
|
|||
250 | last = None |
|
|||
251 | for v in values: |
|
|||
252 | yield one(v) |
|
|||
253 | if last is not None: |
|
|||
254 | yield one(last, tag=lastname) |
|
|||
255 | endname = 'end_' + plural |
|
|||
256 | if endname in templ: |
|
|||
257 | yield templ(endname, **strmapping) |
|
|||
258 |
|
||||
259 | def getlatesttags(context, mapping, pattern=None): |
|
58 | def getlatesttags(context, mapping, pattern=None): | |
260 | '''return date, distance and name for the latest tag of rev''' |
|
59 | '''return date, distance and name for the latest tag of rev''' | |
261 | repo = context.resource(mapping, 'repo') |
|
60 | repo = context.resource(mapping, 'repo') |
@@ -498,7 +498,7 b' def dict_(context, mapping, args):' | |||||
498 |
|
498 | |||
499 | data.update((k, evalfuncarg(context, mapping, v)) |
|
499 | data.update((k, evalfuncarg(context, mapping, v)) | |
500 | for k, v in args['kwargs'].iteritems()) |
|
500 | for k, v in args['kwargs'].iteritems()) | |
501 |
return template |
|
501 | return templateutil.hybriddict(data) | |
502 |
|
502 | |||
503 | @templatefunc('diff([includepattern [, excludepattern]])') |
|
503 | @templatefunc('diff([includepattern [, excludepattern]])') | |
504 | def diff(context, mapping, args): |
|
504 | def diff(context, mapping, args): | |
@@ -548,7 +548,7 b' def files(context, mapping, args):' | |||||
548 | ctx = context.resource(mapping, 'ctx') |
|
548 | ctx = context.resource(mapping, 'ctx') | |
549 | m = ctx.match([raw]) |
|
549 | m = ctx.match([raw]) | |
550 | files = list(ctx.matches(m)) |
|
550 | files = list(ctx.matches(m)) | |
551 |
return template |
|
551 | return templateutil.compatlist(context, mapping, "file", files) | |
552 |
|
552 | |||
553 | @templatefunc('fill(text[, width[, initialident[, hangindent]]])') |
|
553 | @templatefunc('fill(text[, width[, initialident[, hangindent]]])') | |
554 | def fill(context, mapping, args): |
|
554 | def fill(context, mapping, args): | |
@@ -718,7 +718,7 b' def join(context, mapping, args):' | |||||
718 | # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb |
|
718 | # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb | |
719 | # abuses generator as a keyword that returns a list of dicts. |
|
719 | # abuses generator as a keyword that returns a list of dicts. | |
720 | joinset = evalrawexp(context, mapping, args[0]) |
|
720 | joinset = evalrawexp(context, mapping, args[0]) | |
721 |
joinset = template |
|
721 | joinset = templateutil.unwrapvalue(joinset) | |
722 | joinfmt = getattr(joinset, 'joinfmt', pycompat.identity) |
|
722 | joinfmt = getattr(joinset, 'joinfmt', pycompat.identity) | |
723 | joiner = " " |
|
723 | joiner = " " | |
724 | if len(args) > 1: |
|
724 | if len(args) > 1: | |
@@ -808,7 +808,7 b' def max_(context, mapping, args, **kwarg' | |||||
808 | except (TypeError, ValueError): |
|
808 | except (TypeError, ValueError): | |
809 | # i18n: "max" is a keyword |
|
809 | # i18n: "max" is a keyword | |
810 | raise error.ParseError(_("max first argument should be an iterable")) |
|
810 | raise error.ParseError(_("max first argument should be an iterable")) | |
811 |
return template |
|
811 | return templateutil.wraphybridvalue(iterable, x, x) | |
812 |
|
812 | |||
813 | @templatefunc('min(iterable)') |
|
813 | @templatefunc('min(iterable)') | |
814 | def min_(context, mapping, args, **kwargs): |
|
814 | def min_(context, mapping, args, **kwargs): | |
@@ -823,7 +823,7 b' def min_(context, mapping, args, **kwarg' | |||||
823 | except (TypeError, ValueError): |
|
823 | except (TypeError, ValueError): | |
824 | # i18n: "min" is a keyword |
|
824 | # i18n: "min" is a keyword | |
825 | raise error.ParseError(_("min first argument should be an iterable")) |
|
825 | raise error.ParseError(_("min first argument should be an iterable")) | |
826 |
return template |
|
826 | return templateutil.wraphybridvalue(iterable, x, x) | |
827 |
|
827 | |||
828 | @templatefunc('mod(a, b)') |
|
828 | @templatefunc('mod(a, b)') | |
829 | def mod(context, mapping, args): |
|
829 | def mod(context, mapping, args): | |
@@ -847,7 +847,7 b' def obsfateoperations(context, mapping, ' | |||||
847 |
|
847 | |||
848 | try: |
|
848 | try: | |
849 | data = obsutil.markersoperations(markers) |
|
849 | data = obsutil.markersoperations(markers) | |
850 |
return template |
|
850 | return templateutil.hybridlist(data, name='operation') | |
851 | except (TypeError, KeyError): |
|
851 | except (TypeError, KeyError): | |
852 | # i18n: "obsfateoperations" is a keyword |
|
852 | # i18n: "obsfateoperations" is a keyword | |
853 | errmsg = _("obsfateoperations first argument should be an iterable") |
|
853 | errmsg = _("obsfateoperations first argument should be an iterable") | |
@@ -864,7 +864,7 b' def obsfatedate(context, mapping, args):' | |||||
864 |
|
864 | |||
865 | try: |
|
865 | try: | |
866 | data = obsutil.markersdates(markers) |
|
866 | data = obsutil.markersdates(markers) | |
867 |
return template |
|
867 | return templateutil.hybridlist(data, name='date', fmt='%d %d') | |
868 | except (TypeError, KeyError): |
|
868 | except (TypeError, KeyError): | |
869 | # i18n: "obsfatedate" is a keyword |
|
869 | # i18n: "obsfatedate" is a keyword | |
870 | errmsg = _("obsfatedate first argument should be an iterable") |
|
870 | errmsg = _("obsfatedate first argument should be an iterable") | |
@@ -881,7 +881,7 b' def obsfateusers(context, mapping, args)' | |||||
881 |
|
881 | |||
882 | try: |
|
882 | try: | |
883 | data = obsutil.markersusers(markers) |
|
883 | data = obsutil.markersusers(markers) | |
884 |
return template |
|
884 | return templateutil.hybridlist(data, name='user') | |
885 | except (TypeError, KeyError, ValueError): |
|
885 | except (TypeError, KeyError, ValueError): | |
886 | # i18n: "obsfateusers" is a keyword |
|
886 | # i18n: "obsfateusers" is a keyword | |
887 | msg = _("obsfateusers first argument should be an iterable of " |
|
887 | msg = _("obsfateusers first argument should be an iterable of " | |
@@ -1120,7 +1120,7 b' def expandaliases(tree, aliases):' | |||||
1120 |
|
1120 | |||
1121 | def _flatten(thing): |
|
1121 | def _flatten(thing): | |
1122 | '''yield a single stream from a possibly nested set of iterators''' |
|
1122 | '''yield a single stream from a possibly nested set of iterators''' | |
1123 |
thing = template |
|
1123 | thing = templateutil.unwraphybrid(thing) | |
1124 | if isinstance(thing, bytes): |
|
1124 | if isinstance(thing, bytes): | |
1125 | yield thing |
|
1125 | yield thing | |
1126 | elif isinstance(thing, str): |
|
1126 | elif isinstance(thing, str): | |
@@ -1134,7 +1134,7 b' def _flatten(thing):' | |||||
1134 | yield pycompat.bytestr(thing) |
|
1134 | yield pycompat.bytestr(thing) | |
1135 | else: |
|
1135 | else: | |
1136 | for i in thing: |
|
1136 | for i in thing: | |
1137 |
i = template |
|
1137 | i = templateutil.unwraphybrid(i) | |
1138 | if isinstance(i, bytes): |
|
1138 | if isinstance(i, bytes): | |
1139 | yield i |
|
1139 | yield i | |
1140 | elif i is None: |
|
1140 | elif i is None: |
@@ -13,7 +13,6 b' 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 | |||
@@ -23,9 +22,219 b' class ResourceUnavailable(error.Abort):' | |||||
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 = |
|
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 | |
@@ -59,7 +268,7 b' def evalrawexp(context, mapping, arg):' | |||||
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 = |
|
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): | |
@@ -76,7 +285,7 b' def evalboolean(context, mapping, arg):' | |||||
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 = |
|
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 | |
@@ -236,4 +445,4 b' 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 |
|
448 | return wraphybridvalue(dictarg, key, val) |
General Comments 0
You need to be logged in to leave comments.
Login now