##// END OF EJS Templates
templatefilters: allow declaration of input data type...
Yuya Nishihara -
r37239:54355c24 default
parent child Browse files
Show More
@@ -1,437 +1,443 b''
1 # registrar.py - utilities to register function for specific purpose
1 # registrar.py - utilities to register function for specific purpose
2 #
2 #
3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from . import (
10 from . import (
11 configitems,
11 configitems,
12 error,
12 error,
13 pycompat,
13 pycompat,
14 util,
14 util,
15 )
15 )
16
16
17 # unlike the other registered items, config options are neither functions or
17 # unlike the other registered items, config options are neither functions or
18 # classes. Registering the option is just small function call.
18 # classes. Registering the option is just small function call.
19 #
19 #
20 # We still add the official API to the registrar module for consistency with
20 # We still add the official API to the registrar module for consistency with
21 # the other items extensions want might to register.
21 # the other items extensions want might to register.
22 configitem = configitems.getitemregister
22 configitem = configitems.getitemregister
23
23
24 class _funcregistrarbase(object):
24 class _funcregistrarbase(object):
25 """Base of decorator to register a function for specific purpose
25 """Base of decorator to register a function for specific purpose
26
26
27 This decorator stores decorated functions into own dict 'table'.
27 This decorator stores decorated functions into own dict 'table'.
28
28
29 The least derived class can be defined by overriding 'formatdoc',
29 The least derived class can be defined by overriding 'formatdoc',
30 for example::
30 for example::
31
31
32 class keyword(_funcregistrarbase):
32 class keyword(_funcregistrarbase):
33 _docformat = ":%s: %s"
33 _docformat = ":%s: %s"
34
34
35 This should be used as below:
35 This should be used as below:
36
36
37 keyword = registrar.keyword()
37 keyword = registrar.keyword()
38
38
39 @keyword('bar')
39 @keyword('bar')
40 def barfunc(*args, **kwargs):
40 def barfunc(*args, **kwargs):
41 '''Explanation of bar keyword ....
41 '''Explanation of bar keyword ....
42 '''
42 '''
43 pass
43 pass
44
44
45 In this case:
45 In this case:
46
46
47 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
47 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
48 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
48 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
49 """
49 """
50 def __init__(self, table=None):
50 def __init__(self, table=None):
51 if table is None:
51 if table is None:
52 self._table = {}
52 self._table = {}
53 else:
53 else:
54 self._table = table
54 self._table = table
55
55
56 def __call__(self, decl, *args, **kwargs):
56 def __call__(self, decl, *args, **kwargs):
57 return lambda func: self._doregister(func, decl, *args, **kwargs)
57 return lambda func: self._doregister(func, decl, *args, **kwargs)
58
58
59 def _doregister(self, func, decl, *args, **kwargs):
59 def _doregister(self, func, decl, *args, **kwargs):
60 name = self._getname(decl)
60 name = self._getname(decl)
61
61
62 if name in self._table:
62 if name in self._table:
63 msg = 'duplicate registration for name: "%s"' % name
63 msg = 'duplicate registration for name: "%s"' % name
64 raise error.ProgrammingError(msg)
64 raise error.ProgrammingError(msg)
65
65
66 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
66 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
67 doc = pycompat.sysbytes(func.__doc__).strip()
67 doc = pycompat.sysbytes(func.__doc__).strip()
68 func._origdoc = doc
68 func._origdoc = doc
69 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
69 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
70
70
71 self._table[name] = func
71 self._table[name] = func
72 self._extrasetup(name, func, *args, **kwargs)
72 self._extrasetup(name, func, *args, **kwargs)
73
73
74 return func
74 return func
75
75
76 def _parsefuncdecl(self, decl):
76 def _parsefuncdecl(self, decl):
77 """Parse function declaration and return the name of function in it
77 """Parse function declaration and return the name of function in it
78 """
78 """
79 i = decl.find('(')
79 i = decl.find('(')
80 if i >= 0:
80 if i >= 0:
81 return decl[:i]
81 return decl[:i]
82 else:
82 else:
83 return decl
83 return decl
84
84
85 def _getname(self, decl):
85 def _getname(self, decl):
86 """Return the name of the registered function from decl
86 """Return the name of the registered function from decl
87
87
88 Derived class should override this, if it allows more
88 Derived class should override this, if it allows more
89 descriptive 'decl' string than just a name.
89 descriptive 'decl' string than just a name.
90 """
90 """
91 return decl
91 return decl
92
92
93 _docformat = None
93 _docformat = None
94
94
95 def _formatdoc(self, decl, doc):
95 def _formatdoc(self, decl, doc):
96 """Return formatted document of the registered function for help
96 """Return formatted document of the registered function for help
97
97
98 'doc' is '__doc__.strip()' of the registered function.
98 'doc' is '__doc__.strip()' of the registered function.
99 """
99 """
100 return self._docformat % (decl, doc)
100 return self._docformat % (decl, doc)
101
101
102 def _extrasetup(self, name, func):
102 def _extrasetup(self, name, func):
103 """Execute exra setup for registered function, if needed
103 """Execute exra setup for registered function, if needed
104 """
104 """
105
105
106 class command(_funcregistrarbase):
106 class command(_funcregistrarbase):
107 """Decorator to register a command function to table
107 """Decorator to register a command function to table
108
108
109 This class receives a command table as its argument. The table should
109 This class receives a command table as its argument. The table should
110 be a dict.
110 be a dict.
111
111
112 The created object can be used as a decorator for adding commands to
112 The created object can be used as a decorator for adding commands to
113 that command table. This accepts multiple arguments to define a command.
113 that command table. This accepts multiple arguments to define a command.
114
114
115 The first argument is the command name (as bytes).
115 The first argument is the command name (as bytes).
116
116
117 The `options` keyword argument is an iterable of tuples defining command
117 The `options` keyword argument is an iterable of tuples defining command
118 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
118 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
119 tuple.
119 tuple.
120
120
121 The `synopsis` argument defines a short, one line summary of how to use the
121 The `synopsis` argument defines a short, one line summary of how to use the
122 command. This shows up in the help output.
122 command. This shows up in the help output.
123
123
124 There are three arguments that control what repository (if any) is found
124 There are three arguments that control what repository (if any) is found
125 and passed to the decorated function: `norepo`, `optionalrepo`, and
125 and passed to the decorated function: `norepo`, `optionalrepo`, and
126 `inferrepo`.
126 `inferrepo`.
127
127
128 The `norepo` argument defines whether the command does not require a
128 The `norepo` argument defines whether the command does not require a
129 local repository. Most commands operate against a repository, thus the
129 local repository. Most commands operate against a repository, thus the
130 default is False. When True, no repository will be passed.
130 default is False. When True, no repository will be passed.
131
131
132 The `optionalrepo` argument defines whether the command optionally requires
132 The `optionalrepo` argument defines whether the command optionally requires
133 a local repository. If no repository can be found, None will be passed
133 a local repository. If no repository can be found, None will be passed
134 to the decorated function.
134 to the decorated function.
135
135
136 The `inferrepo` argument defines whether to try to find a repository from
136 The `inferrepo` argument defines whether to try to find a repository from
137 the command line arguments. If True, arguments will be examined for
137 the command line arguments. If True, arguments will be examined for
138 potential repository locations. See ``findrepo()``. If a repository is
138 potential repository locations. See ``findrepo()``. If a repository is
139 found, it will be used and passed to the decorated function.
139 found, it will be used and passed to the decorated function.
140
140
141 There are three constants in the class which tells what type of the command
141 There are three constants in the class which tells what type of the command
142 that is. That information will be helpful at various places. It will be also
142 that is. That information will be helpful at various places. It will be also
143 be used to decide what level of access the command has on hidden commits.
143 be used to decide what level of access the command has on hidden commits.
144 The constants are:
144 The constants are:
145
145
146 `unrecoverablewrite` is for those write commands which can't be recovered
146 `unrecoverablewrite` is for those write commands which can't be recovered
147 like push.
147 like push.
148 `recoverablewrite` is for write commands which can be recovered like commit.
148 `recoverablewrite` is for write commands which can be recovered like commit.
149 `readonly` is for commands which are read only.
149 `readonly` is for commands which are read only.
150
150
151 The signature of the decorated function looks like this:
151 The signature of the decorated function looks like this:
152 def cmd(ui[, repo] [, <args>] [, <options>])
152 def cmd(ui[, repo] [, <args>] [, <options>])
153
153
154 `repo` is required if `norepo` is False.
154 `repo` is required if `norepo` is False.
155 `<args>` are positional args (or `*args`) arguments, of non-option
155 `<args>` are positional args (or `*args`) arguments, of non-option
156 arguments from the command line.
156 arguments from the command line.
157 `<options>` are keyword arguments (or `**options`) of option arguments
157 `<options>` are keyword arguments (or `**options`) of option arguments
158 from the command line.
158 from the command line.
159
159
160 See the WritingExtensions and MercurialApi documentation for more exhaustive
160 See the WritingExtensions and MercurialApi documentation for more exhaustive
161 descriptions and examples.
161 descriptions and examples.
162 """
162 """
163
163
164 unrecoverablewrite = "unrecoverable"
164 unrecoverablewrite = "unrecoverable"
165 recoverablewrite = "recoverable"
165 recoverablewrite = "recoverable"
166 readonly = "readonly"
166 readonly = "readonly"
167
167
168 possiblecmdtypes = {unrecoverablewrite, recoverablewrite, readonly}
168 possiblecmdtypes = {unrecoverablewrite, recoverablewrite, readonly}
169
169
170 def _doregister(self, func, name, options=(), synopsis=None,
170 def _doregister(self, func, name, options=(), synopsis=None,
171 norepo=False, optionalrepo=False, inferrepo=False,
171 norepo=False, optionalrepo=False, inferrepo=False,
172 cmdtype=unrecoverablewrite):
172 cmdtype=unrecoverablewrite):
173
173
174 if cmdtype not in self.possiblecmdtypes:
174 if cmdtype not in self.possiblecmdtypes:
175 raise error.ProgrammingError("unknown cmdtype value '%s' for "
175 raise error.ProgrammingError("unknown cmdtype value '%s' for "
176 "'%s' command" % (cmdtype, name))
176 "'%s' command" % (cmdtype, name))
177 func.norepo = norepo
177 func.norepo = norepo
178 func.optionalrepo = optionalrepo
178 func.optionalrepo = optionalrepo
179 func.inferrepo = inferrepo
179 func.inferrepo = inferrepo
180 func.cmdtype = cmdtype
180 func.cmdtype = cmdtype
181 if synopsis:
181 if synopsis:
182 self._table[name] = func, list(options), synopsis
182 self._table[name] = func, list(options), synopsis
183 else:
183 else:
184 self._table[name] = func, list(options)
184 self._table[name] = func, list(options)
185 return func
185 return func
186
186
187 class revsetpredicate(_funcregistrarbase):
187 class revsetpredicate(_funcregistrarbase):
188 """Decorator to register revset predicate
188 """Decorator to register revset predicate
189
189
190 Usage::
190 Usage::
191
191
192 revsetpredicate = registrar.revsetpredicate()
192 revsetpredicate = registrar.revsetpredicate()
193
193
194 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
194 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
195 def mypredicatefunc(repo, subset, x):
195 def mypredicatefunc(repo, subset, x):
196 '''Explanation of this revset predicate ....
196 '''Explanation of this revset predicate ....
197 '''
197 '''
198 pass
198 pass
199
199
200 The first string argument is used also in online help.
200 The first string argument is used also in online help.
201
201
202 Optional argument 'safe' indicates whether a predicate is safe for
202 Optional argument 'safe' indicates whether a predicate is safe for
203 DoS attack (False by default).
203 DoS attack (False by default).
204
204
205 Optional argument 'takeorder' indicates whether a predicate function
205 Optional argument 'takeorder' indicates whether a predicate function
206 takes ordering policy as the last argument.
206 takes ordering policy as the last argument.
207
207
208 Optional argument 'weight' indicates the estimated run-time cost, useful
208 Optional argument 'weight' indicates the estimated run-time cost, useful
209 for static optimization, default is 1. Higher weight means more expensive.
209 for static optimization, default is 1. Higher weight means more expensive.
210 Usually, revsets that are fast and return only one revision has a weight of
210 Usually, revsets that are fast and return only one revision has a weight of
211 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
211 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
212 changelog have weight 10 (ex. author); revsets reading manifest deltas have
212 changelog have weight 10 (ex. author); revsets reading manifest deltas have
213 weight 30 (ex. adds); revset reading manifest contents have weight 100
213 weight 30 (ex. adds); revset reading manifest contents have weight 100
214 (ex. contains). Note: those values are flexible. If the revset has a
214 (ex. contains). Note: those values are flexible. If the revset has a
215 same big-O time complexity as 'contains', but with a smaller constant, it
215 same big-O time complexity as 'contains', but with a smaller constant, it
216 might have a weight of 90.
216 might have a weight of 90.
217
217
218 'revsetpredicate' instance in example above can be used to
218 'revsetpredicate' instance in example above can be used to
219 decorate multiple functions.
219 decorate multiple functions.
220
220
221 Decorated functions are registered automatically at loading
221 Decorated functions are registered automatically at loading
222 extension, if an instance named as 'revsetpredicate' is used for
222 extension, if an instance named as 'revsetpredicate' is used for
223 decorating in extension.
223 decorating in extension.
224
224
225 Otherwise, explicit 'revset.loadpredicate()' is needed.
225 Otherwise, explicit 'revset.loadpredicate()' is needed.
226 """
226 """
227 _getname = _funcregistrarbase._parsefuncdecl
227 _getname = _funcregistrarbase._parsefuncdecl
228 _docformat = "``%s``\n %s"
228 _docformat = "``%s``\n %s"
229
229
230 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
230 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
231 func._safe = safe
231 func._safe = safe
232 func._takeorder = takeorder
232 func._takeorder = takeorder
233 func._weight = weight
233 func._weight = weight
234
234
235 class filesetpredicate(_funcregistrarbase):
235 class filesetpredicate(_funcregistrarbase):
236 """Decorator to register fileset predicate
236 """Decorator to register fileset predicate
237
237
238 Usage::
238 Usage::
239
239
240 filesetpredicate = registrar.filesetpredicate()
240 filesetpredicate = registrar.filesetpredicate()
241
241
242 @filesetpredicate('mypredicate()')
242 @filesetpredicate('mypredicate()')
243 def mypredicatefunc(mctx, x):
243 def mypredicatefunc(mctx, x):
244 '''Explanation of this fileset predicate ....
244 '''Explanation of this fileset predicate ....
245 '''
245 '''
246 pass
246 pass
247
247
248 The first string argument is used also in online help.
248 The first string argument is used also in online help.
249
249
250 Optional argument 'callstatus' indicates whether a predicate
250 Optional argument 'callstatus' indicates whether a predicate
251 implies 'matchctx.status()' at runtime or not (False, by
251 implies 'matchctx.status()' at runtime or not (False, by
252 default).
252 default).
253
253
254 Optional argument 'callexisting' indicates whether a predicate
254 Optional argument 'callexisting' indicates whether a predicate
255 implies 'matchctx.existing()' at runtime or not (False, by
255 implies 'matchctx.existing()' at runtime or not (False, by
256 default).
256 default).
257
257
258 'filesetpredicate' instance in example above can be used to
258 'filesetpredicate' instance in example above can be used to
259 decorate multiple functions.
259 decorate multiple functions.
260
260
261 Decorated functions are registered automatically at loading
261 Decorated functions are registered automatically at loading
262 extension, if an instance named as 'filesetpredicate' is used for
262 extension, if an instance named as 'filesetpredicate' is used for
263 decorating in extension.
263 decorating in extension.
264
264
265 Otherwise, explicit 'fileset.loadpredicate()' is needed.
265 Otherwise, explicit 'fileset.loadpredicate()' is needed.
266 """
266 """
267 _getname = _funcregistrarbase._parsefuncdecl
267 _getname = _funcregistrarbase._parsefuncdecl
268 _docformat = "``%s``\n %s"
268 _docformat = "``%s``\n %s"
269
269
270 def _extrasetup(self, name, func, callstatus=False, callexisting=False):
270 def _extrasetup(self, name, func, callstatus=False, callexisting=False):
271 func._callstatus = callstatus
271 func._callstatus = callstatus
272 func._callexisting = callexisting
272 func._callexisting = callexisting
273
273
274 class _templateregistrarbase(_funcregistrarbase):
274 class _templateregistrarbase(_funcregistrarbase):
275 """Base of decorator to register functions as template specific one
275 """Base of decorator to register functions as template specific one
276 """
276 """
277 _docformat = ":%s: %s"
277 _docformat = ":%s: %s"
278
278
279 class templatekeyword(_templateregistrarbase):
279 class templatekeyword(_templateregistrarbase):
280 """Decorator to register template keyword
280 """Decorator to register template keyword
281
281
282 Usage::
282 Usage::
283
283
284 templatekeyword = registrar.templatekeyword()
284 templatekeyword = registrar.templatekeyword()
285
285
286 # new API (since Mercurial 4.6)
286 # new API (since Mercurial 4.6)
287 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
287 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
288 def mykeywordfunc(context, mapping):
288 def mykeywordfunc(context, mapping):
289 '''Explanation of this template keyword ....
289 '''Explanation of this template keyword ....
290 '''
290 '''
291 pass
291 pass
292
292
293 # old API
293 # old API
294 @templatekeyword('mykeyword')
294 @templatekeyword('mykeyword')
295 def mykeywordfunc(repo, ctx, templ, cache, revcache, **args):
295 def mykeywordfunc(repo, ctx, templ, cache, revcache, **args):
296 '''Explanation of this template keyword ....
296 '''Explanation of this template keyword ....
297 '''
297 '''
298 pass
298 pass
299
299
300 The first string argument is used also in online help.
300 The first string argument is used also in online help.
301
301
302 Optional argument 'requires' should be a collection of resource names
302 Optional argument 'requires' should be a collection of resource names
303 which the template keyword depends on. This also serves as a flag to
303 which the template keyword depends on. This also serves as a flag to
304 switch to the new API. If 'requires' is unspecified, all template
304 switch to the new API. If 'requires' is unspecified, all template
305 keywords and resources are expanded to the function arguments.
305 keywords and resources are expanded to the function arguments.
306
306
307 'templatekeyword' instance in example above can be used to
307 'templatekeyword' instance in example above can be used to
308 decorate multiple functions.
308 decorate multiple functions.
309
309
310 Decorated functions are registered automatically at loading
310 Decorated functions are registered automatically at loading
311 extension, if an instance named as 'templatekeyword' is used for
311 extension, if an instance named as 'templatekeyword' is used for
312 decorating in extension.
312 decorating in extension.
313
313
314 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
314 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
315 """
315 """
316
316
317 def _extrasetup(self, name, func, requires=None):
317 def _extrasetup(self, name, func, requires=None):
318 func._requires = requires
318 func._requires = requires
319
319
320 class templatefilter(_templateregistrarbase):
320 class templatefilter(_templateregistrarbase):
321 """Decorator to register template filer
321 """Decorator to register template filer
322
322
323 Usage::
323 Usage::
324
324
325 templatefilter = registrar.templatefilter()
325 templatefilter = registrar.templatefilter()
326
326
327 @templatefilter('myfilter')
327 @templatefilter('myfilter', intype=bytes)
328 def myfilterfunc(text):
328 def myfilterfunc(text):
329 '''Explanation of this template filter ....
329 '''Explanation of this template filter ....
330 '''
330 '''
331 pass
331 pass
332
332
333 The first string argument is used also in online help.
333 The first string argument is used also in online help.
334
334
335 Optional argument 'intype' defines the type of the input argument,
336 which should be (bytes, int, or None for any.)
337
335 'templatefilter' instance in example above can be used to
338 'templatefilter' instance in example above can be used to
336 decorate multiple functions.
339 decorate multiple functions.
337
340
338 Decorated functions are registered automatically at loading
341 Decorated functions are registered automatically at loading
339 extension, if an instance named as 'templatefilter' is used for
342 extension, if an instance named as 'templatefilter' is used for
340 decorating in extension.
343 decorating in extension.
341
344
342 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
345 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
343 """
346 """
344
347
348 def _extrasetup(self, name, func, intype=None):
349 func._intype = intype
350
345 class templatefunc(_templateregistrarbase):
351 class templatefunc(_templateregistrarbase):
346 """Decorator to register template function
352 """Decorator to register template function
347
353
348 Usage::
354 Usage::
349
355
350 templatefunc = registrar.templatefunc()
356 templatefunc = registrar.templatefunc()
351
357
352 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3')
358 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3')
353 def myfuncfunc(context, mapping, args):
359 def myfuncfunc(context, mapping, args):
354 '''Explanation of this template function ....
360 '''Explanation of this template function ....
355 '''
361 '''
356 pass
362 pass
357
363
358 The first string argument is used also in online help.
364 The first string argument is used also in online help.
359
365
360 If optional 'argspec' is defined, the function will receive 'args' as
366 If optional 'argspec' is defined, the function will receive 'args' as
361 a dict of named arguments. Otherwise 'args' is a list of positional
367 a dict of named arguments. Otherwise 'args' is a list of positional
362 arguments.
368 arguments.
363
369
364 'templatefunc' instance in example above can be used to
370 'templatefunc' instance in example above can be used to
365 decorate multiple functions.
371 decorate multiple functions.
366
372
367 Decorated functions are registered automatically at loading
373 Decorated functions are registered automatically at loading
368 extension, if an instance named as 'templatefunc' is used for
374 extension, if an instance named as 'templatefunc' is used for
369 decorating in extension.
375 decorating in extension.
370
376
371 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
377 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
372 """
378 """
373 _getname = _funcregistrarbase._parsefuncdecl
379 _getname = _funcregistrarbase._parsefuncdecl
374
380
375 def _extrasetup(self, name, func, argspec=None):
381 def _extrasetup(self, name, func, argspec=None):
376 func._argspec = argspec
382 func._argspec = argspec
377
383
378 class internalmerge(_funcregistrarbase):
384 class internalmerge(_funcregistrarbase):
379 """Decorator to register in-process merge tool
385 """Decorator to register in-process merge tool
380
386
381 Usage::
387 Usage::
382
388
383 internalmerge = registrar.internalmerge()
389 internalmerge = registrar.internalmerge()
384
390
385 @internalmerge('mymerge', internalmerge.mergeonly,
391 @internalmerge('mymerge', internalmerge.mergeonly,
386 onfailure=None, precheck=None):
392 onfailure=None, precheck=None):
387 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
393 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
388 toolconf, files, labels=None):
394 toolconf, files, labels=None):
389 '''Explanation of this internal merge tool ....
395 '''Explanation of this internal merge tool ....
390 '''
396 '''
391 return 1, False # means "conflicted", "no deletion needed"
397 return 1, False # means "conflicted", "no deletion needed"
392
398
393 The first string argument is used to compose actual merge tool name,
399 The first string argument is used to compose actual merge tool name,
394 ":name" and "internal:name" (the latter is historical one).
400 ":name" and "internal:name" (the latter is historical one).
395
401
396 The second argument is one of merge types below:
402 The second argument is one of merge types below:
397
403
398 ========== ======== ======== =========
404 ========== ======== ======== =========
399 merge type precheck premerge fullmerge
405 merge type precheck premerge fullmerge
400 ========== ======== ======== =========
406 ========== ======== ======== =========
401 nomerge x x x
407 nomerge x x x
402 mergeonly o x o
408 mergeonly o x o
403 fullmerge o o o
409 fullmerge o o o
404 ========== ======== ======== =========
410 ========== ======== ======== =========
405
411
406 Optional argument 'onfailure' is the format of warning message
412 Optional argument 'onfailure' is the format of warning message
407 to be used at failure of merging (target filename is specified
413 to be used at failure of merging (target filename is specified
408 at formatting). Or, None or so, if warning message should be
414 at formatting). Or, None or so, if warning message should be
409 suppressed.
415 suppressed.
410
416
411 Optional argument 'precheck' is the function to be used
417 Optional argument 'precheck' is the function to be used
412 before actual invocation of internal merge tool itself.
418 before actual invocation of internal merge tool itself.
413 It takes as same arguments as internal merge tool does, other than
419 It takes as same arguments as internal merge tool does, other than
414 'files' and 'labels'. If it returns false value, merging is aborted
420 'files' and 'labels'. If it returns false value, merging is aborted
415 immediately (and file is marked as "unresolved").
421 immediately (and file is marked as "unresolved").
416
422
417 'internalmerge' instance in example above can be used to
423 'internalmerge' instance in example above can be used to
418 decorate multiple functions.
424 decorate multiple functions.
419
425
420 Decorated functions are registered automatically at loading
426 Decorated functions are registered automatically at loading
421 extension, if an instance named as 'internalmerge' is used for
427 extension, if an instance named as 'internalmerge' is used for
422 decorating in extension.
428 decorating in extension.
423
429
424 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
430 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
425 """
431 """
426 _docformat = "``:%s``\n %s"
432 _docformat = "``:%s``\n %s"
427
433
428 # merge type definitions:
434 # merge type definitions:
429 nomerge = None
435 nomerge = None
430 mergeonly = 'mergeonly' # just the full merge, no premerge
436 mergeonly = 'mergeonly' # just the full merge, no premerge
431 fullmerge = 'fullmerge' # both premerge and merge
437 fullmerge = 'fullmerge' # both premerge and merge
432
438
433 def _extrasetup(self, name, func, mergetype,
439 def _extrasetup(self, name, func, mergetype,
434 onfailure=None, precheck=None):
440 onfailure=None, precheck=None):
435 func.mergetype = mergetype
441 func.mergetype = mergetype
436 func.onfailure = onfailure
442 func.onfailure = onfailure
437 func.precheck = precheck
443 func.precheck = precheck
@@ -1,436 +1,436 b''
1 # templatefilters.py - common template expansion filters
1 # templatefilters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import time
12 import time
13
13
14 from . import (
14 from . import (
15 encoding,
15 encoding,
16 error,
16 error,
17 node,
17 node,
18 pycompat,
18 pycompat,
19 registrar,
19 registrar,
20 templateutil,
20 templateutil,
21 url,
21 url,
22 util,
22 util,
23 )
23 )
24 from .utils import (
24 from .utils import (
25 dateutil,
25 dateutil,
26 stringutil,
26 stringutil,
27 )
27 )
28
28
29 urlerr = util.urlerr
29 urlerr = util.urlerr
30 urlreq = util.urlreq
30 urlreq = util.urlreq
31
31
32 if pycompat.ispy3:
32 if pycompat.ispy3:
33 long = int
33 long = int
34
34
35 # filters are callables like:
35 # filters are callables like:
36 # fn(obj)
36 # fn(obj)
37 # with:
37 # with:
38 # obj - object to be filtered (text, date, list and so on)
38 # obj - object to be filtered (text, date, list and so on)
39 filters = {}
39 filters = {}
40
40
41 templatefilter = registrar.templatefilter(filters)
41 templatefilter = registrar.templatefilter(filters)
42
42
43 @templatefilter('addbreaks')
43 @templatefilter('addbreaks')
44 def addbreaks(text):
44 def addbreaks(text):
45 """Any text. Add an XHTML "<br />" tag before the end of
45 """Any text. Add an XHTML "<br />" tag before the end of
46 every line except the last.
46 every line except the last.
47 """
47 """
48 return text.replace('\n', '<br/>\n')
48 return text.replace('\n', '<br/>\n')
49
49
50 agescales = [("year", 3600 * 24 * 365, 'Y'),
50 agescales = [("year", 3600 * 24 * 365, 'Y'),
51 ("month", 3600 * 24 * 30, 'M'),
51 ("month", 3600 * 24 * 30, 'M'),
52 ("week", 3600 * 24 * 7, 'W'),
52 ("week", 3600 * 24 * 7, 'W'),
53 ("day", 3600 * 24, 'd'),
53 ("day", 3600 * 24, 'd'),
54 ("hour", 3600, 'h'),
54 ("hour", 3600, 'h'),
55 ("minute", 60, 'm'),
55 ("minute", 60, 'm'),
56 ("second", 1, 's')]
56 ("second", 1, 's')]
57
57
58 @templatefilter('age')
58 @templatefilter('age')
59 def age(date, abbrev=False):
59 def age(date, abbrev=False):
60 """Date. Returns a human-readable date/time difference between the
60 """Date. Returns a human-readable date/time difference between the
61 given date/time and the current date/time.
61 given date/time and the current date/time.
62 """
62 """
63
63
64 def plural(t, c):
64 def plural(t, c):
65 if c == 1:
65 if c == 1:
66 return t
66 return t
67 return t + "s"
67 return t + "s"
68 def fmt(t, c, a):
68 def fmt(t, c, a):
69 if abbrev:
69 if abbrev:
70 return "%d%s" % (c, a)
70 return "%d%s" % (c, a)
71 return "%d %s" % (c, plural(t, c))
71 return "%d %s" % (c, plural(t, c))
72
72
73 now = time.time()
73 now = time.time()
74 then = date[0]
74 then = date[0]
75 future = False
75 future = False
76 if then > now:
76 if then > now:
77 future = True
77 future = True
78 delta = max(1, int(then - now))
78 delta = max(1, int(then - now))
79 if delta > agescales[0][1] * 30:
79 if delta > agescales[0][1] * 30:
80 return 'in the distant future'
80 return 'in the distant future'
81 else:
81 else:
82 delta = max(1, int(now - then))
82 delta = max(1, int(now - then))
83 if delta > agescales[0][1] * 2:
83 if delta > agescales[0][1] * 2:
84 return dateutil.shortdate(date)
84 return dateutil.shortdate(date)
85
85
86 for t, s, a in agescales:
86 for t, s, a in agescales:
87 n = delta // s
87 n = delta // s
88 if n >= 2 or s == 1:
88 if n >= 2 or s == 1:
89 if future:
89 if future:
90 return '%s from now' % fmt(t, n, a)
90 return '%s from now' % fmt(t, n, a)
91 return '%s ago' % fmt(t, n, a)
91 return '%s ago' % fmt(t, n, a)
92
92
93 @templatefilter('basename')
93 @templatefilter('basename')
94 def basename(path):
94 def basename(path):
95 """Any text. Treats the text as a path, and returns the last
95 """Any text. Treats the text as a path, and returns the last
96 component of the path after splitting by the path separator.
96 component of the path after splitting by the path separator.
97 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
97 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
98 """
98 """
99 return os.path.basename(path)
99 return os.path.basename(path)
100
100
101 @templatefilter('count')
101 @templatefilter('count')
102 def count(i):
102 def count(i):
103 """List or text. Returns the length as an integer."""
103 """List or text. Returns the length as an integer."""
104 return len(i)
104 return len(i)
105
105
106 @templatefilter('dirname')
106 @templatefilter('dirname')
107 def dirname(path):
107 def dirname(path):
108 """Any text. Treats the text as a path, and strips the last
108 """Any text. Treats the text as a path, and strips the last
109 component of the path after splitting by the path separator.
109 component of the path after splitting by the path separator.
110 """
110 """
111 return os.path.dirname(path)
111 return os.path.dirname(path)
112
112
113 @templatefilter('domain')
113 @templatefilter('domain')
114 def domain(author):
114 def domain(author):
115 """Any text. Finds the first string that looks like an email
115 """Any text. Finds the first string that looks like an email
116 address, and extracts just the domain component. Example: ``User
116 address, and extracts just the domain component. Example: ``User
117 <user@example.com>`` becomes ``example.com``.
117 <user@example.com>`` becomes ``example.com``.
118 """
118 """
119 f = author.find('@')
119 f = author.find('@')
120 if f == -1:
120 if f == -1:
121 return ''
121 return ''
122 author = author[f + 1:]
122 author = author[f + 1:]
123 f = author.find('>')
123 f = author.find('>')
124 if f >= 0:
124 if f >= 0:
125 author = author[:f]
125 author = author[:f]
126 return author
126 return author
127
127
128 @templatefilter('email')
128 @templatefilter('email')
129 def email(text):
129 def email(text):
130 """Any text. Extracts the first string that looks like an email
130 """Any text. Extracts the first string that looks like an email
131 address. Example: ``User <user@example.com>`` becomes
131 address. Example: ``User <user@example.com>`` becomes
132 ``user@example.com``.
132 ``user@example.com``.
133 """
133 """
134 return stringutil.email(text)
134 return stringutil.email(text)
135
135
136 @templatefilter('escape')
136 @templatefilter('escape')
137 def escape(text):
137 def escape(text):
138 """Any text. Replaces the special XML/XHTML characters "&", "<"
138 """Any text. Replaces the special XML/XHTML characters "&", "<"
139 and ">" with XML entities, and filters out NUL characters.
139 and ">" with XML entities, and filters out NUL characters.
140 """
140 """
141 return url.escape(text.replace('\0', ''), True)
141 return url.escape(text.replace('\0', ''), True)
142
142
143 para_re = None
143 para_re = None
144 space_re = None
144 space_re = None
145
145
146 def fill(text, width, initindent='', hangindent=''):
146 def fill(text, width, initindent='', hangindent=''):
147 '''fill many paragraphs with optional indentation.'''
147 '''fill many paragraphs with optional indentation.'''
148 global para_re, space_re
148 global para_re, space_re
149 if para_re is None:
149 if para_re is None:
150 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
150 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
151 space_re = re.compile(br' +')
151 space_re = re.compile(br' +')
152
152
153 def findparas():
153 def findparas():
154 start = 0
154 start = 0
155 while True:
155 while True:
156 m = para_re.search(text, start)
156 m = para_re.search(text, start)
157 if not m:
157 if not m:
158 uctext = encoding.unifromlocal(text[start:])
158 uctext = encoding.unifromlocal(text[start:])
159 w = len(uctext)
159 w = len(uctext)
160 while 0 < w and uctext[w - 1].isspace():
160 while 0 < w and uctext[w - 1].isspace():
161 w -= 1
161 w -= 1
162 yield (encoding.unitolocal(uctext[:w]),
162 yield (encoding.unitolocal(uctext[:w]),
163 encoding.unitolocal(uctext[w:]))
163 encoding.unitolocal(uctext[w:]))
164 break
164 break
165 yield text[start:m.start(0)], m.group(1)
165 yield text[start:m.start(0)], m.group(1)
166 start = m.end(1)
166 start = m.end(1)
167
167
168 return "".join([stringutil.wrap(space_re.sub(' ',
168 return "".join([stringutil.wrap(space_re.sub(' ',
169 stringutil.wrap(para, width)),
169 stringutil.wrap(para, width)),
170 width, initindent, hangindent) + rest
170 width, initindent, hangindent) + rest
171 for para, rest in findparas()])
171 for para, rest in findparas()])
172
172
173 @templatefilter('fill68')
173 @templatefilter('fill68')
174 def fill68(text):
174 def fill68(text):
175 """Any text. Wraps the text to fit in 68 columns."""
175 """Any text. Wraps the text to fit in 68 columns."""
176 return fill(text, 68)
176 return fill(text, 68)
177
177
178 @templatefilter('fill76')
178 @templatefilter('fill76')
179 def fill76(text):
179 def fill76(text):
180 """Any text. Wraps the text to fit in 76 columns."""
180 """Any text. Wraps the text to fit in 76 columns."""
181 return fill(text, 76)
181 return fill(text, 76)
182
182
183 @templatefilter('firstline')
183 @templatefilter('firstline')
184 def firstline(text):
184 def firstline(text):
185 """Any text. Returns the first line of text."""
185 """Any text. Returns the first line of text."""
186 try:
186 try:
187 return text.splitlines(True)[0].rstrip('\r\n')
187 return text.splitlines(True)[0].rstrip('\r\n')
188 except IndexError:
188 except IndexError:
189 return ''
189 return ''
190
190
191 @templatefilter('hex')
191 @templatefilter('hex')
192 def hexfilter(text):
192 def hexfilter(text):
193 """Any text. Convert a binary Mercurial node identifier into
193 """Any text. Convert a binary Mercurial node identifier into
194 its long hexadecimal representation.
194 its long hexadecimal representation.
195 """
195 """
196 return node.hex(text)
196 return node.hex(text)
197
197
198 @templatefilter('hgdate')
198 @templatefilter('hgdate')
199 def hgdate(text):
199 def hgdate(text):
200 """Date. Returns the date as a pair of numbers: "1157407993
200 """Date. Returns the date as a pair of numbers: "1157407993
201 25200" (Unix timestamp, timezone offset).
201 25200" (Unix timestamp, timezone offset).
202 """
202 """
203 return "%d %d" % text
203 return "%d %d" % text
204
204
205 @templatefilter('isodate')
205 @templatefilter('isodate')
206 def isodate(text):
206 def isodate(text):
207 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
207 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
208 +0200".
208 +0200".
209 """
209 """
210 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
210 return dateutil.datestr(text, '%Y-%m-%d %H:%M %1%2')
211
211
212 @templatefilter('isodatesec')
212 @templatefilter('isodatesec')
213 def isodatesec(text):
213 def isodatesec(text):
214 """Date. Returns the date in ISO 8601 format, including
214 """Date. Returns the date in ISO 8601 format, including
215 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
215 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
216 filter.
216 filter.
217 """
217 """
218 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
218 return dateutil.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
219
219
220 def indent(text, prefix):
220 def indent(text, prefix):
221 '''indent each non-empty line of text after first with prefix.'''
221 '''indent each non-empty line of text after first with prefix.'''
222 lines = text.splitlines()
222 lines = text.splitlines()
223 num_lines = len(lines)
223 num_lines = len(lines)
224 endswithnewline = text[-1:] == '\n'
224 endswithnewline = text[-1:] == '\n'
225 def indenter():
225 def indenter():
226 for i in xrange(num_lines):
226 for i in xrange(num_lines):
227 l = lines[i]
227 l = lines[i]
228 if i and l.strip():
228 if i and l.strip():
229 yield prefix
229 yield prefix
230 yield l
230 yield l
231 if i < num_lines - 1 or endswithnewline:
231 if i < num_lines - 1 or endswithnewline:
232 yield '\n'
232 yield '\n'
233 return "".join(indenter())
233 return "".join(indenter())
234
234
235 @templatefilter('json')
235 @templatefilter('json')
236 def json(obj, paranoid=True):
236 def json(obj, paranoid=True):
237 if obj is None:
237 if obj is None:
238 return 'null'
238 return 'null'
239 elif obj is False:
239 elif obj is False:
240 return 'false'
240 return 'false'
241 elif obj is True:
241 elif obj is True:
242 return 'true'
242 return 'true'
243 elif isinstance(obj, (int, long, float)):
243 elif isinstance(obj, (int, long, float)):
244 return pycompat.bytestr(obj)
244 return pycompat.bytestr(obj)
245 elif isinstance(obj, bytes):
245 elif isinstance(obj, bytes):
246 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
246 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
247 elif isinstance(obj, str):
247 elif isinstance(obj, str):
248 # This branch is unreachable on Python 2, because bytes == str
248 # This branch is unreachable on Python 2, because bytes == str
249 # and we'll return in the next-earlier block in the elif
249 # and we'll return in the next-earlier block in the elif
250 # ladder. On Python 3, this helps us catch bugs before they
250 # ladder. On Python 3, this helps us catch bugs before they
251 # hurt someone.
251 # hurt someone.
252 raise error.ProgrammingError(
252 raise error.ProgrammingError(
253 'Mercurial only does output with bytes on Python 3: %r' % obj)
253 'Mercurial only does output with bytes on Python 3: %r' % obj)
254 elif util.safehasattr(obj, 'keys'):
254 elif util.safehasattr(obj, 'keys'):
255 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
255 out = ['"%s": %s' % (encoding.jsonescape(k, paranoid=paranoid),
256 json(v, paranoid))
256 json(v, paranoid))
257 for k, v in sorted(obj.iteritems())]
257 for k, v in sorted(obj.iteritems())]
258 return '{' + ', '.join(out) + '}'
258 return '{' + ', '.join(out) + '}'
259 elif util.safehasattr(obj, '__iter__'):
259 elif util.safehasattr(obj, '__iter__'):
260 out = [json(i, paranoid) for i in obj]
260 out = [json(i, paranoid) for i in obj]
261 return '[' + ', '.join(out) + ']'
261 return '[' + ', '.join(out) + ']'
262 else:
262 else:
263 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
263 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
264
264
265 @templatefilter('lower')
265 @templatefilter('lower')
266 def lower(text):
266 def lower(text):
267 """Any text. Converts the text to lowercase."""
267 """Any text. Converts the text to lowercase."""
268 return encoding.lower(text)
268 return encoding.lower(text)
269
269
270 @templatefilter('nonempty')
270 @templatefilter('nonempty')
271 def nonempty(text):
271 def nonempty(text):
272 """Any text. Returns '(none)' if the string is empty."""
272 """Any text. Returns '(none)' if the string is empty."""
273 return text or "(none)"
273 return text or "(none)"
274
274
275 @templatefilter('obfuscate')
275 @templatefilter('obfuscate')
276 def obfuscate(text):
276 def obfuscate(text):
277 """Any text. Returns the input text rendered as a sequence of
277 """Any text. Returns the input text rendered as a sequence of
278 XML entities.
278 XML entities.
279 """
279 """
280 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
280 text = unicode(text, pycompat.sysstr(encoding.encoding), r'replace')
281 return ''.join(['&#%d;' % ord(c) for c in text])
281 return ''.join(['&#%d;' % ord(c) for c in text])
282
282
283 @templatefilter('permissions')
283 @templatefilter('permissions')
284 def permissions(flags):
284 def permissions(flags):
285 if "l" in flags:
285 if "l" in flags:
286 return "lrwxrwxrwx"
286 return "lrwxrwxrwx"
287 if "x" in flags:
287 if "x" in flags:
288 return "-rwxr-xr-x"
288 return "-rwxr-xr-x"
289 return "-rw-r--r--"
289 return "-rw-r--r--"
290
290
291 @templatefilter('person')
291 @templatefilter('person')
292 def person(author):
292 def person(author):
293 """Any text. Returns the name before an email address,
293 """Any text. Returns the name before an email address,
294 interpreting it as per RFC 5322.
294 interpreting it as per RFC 5322.
295 """
295 """
296 return stringutil.person(author)
296 return stringutil.person(author)
297
297
298 @templatefilter('revescape')
298 @templatefilter('revescape')
299 def revescape(text):
299 def revescape(text):
300 """Any text. Escapes all "special" characters, except @.
300 """Any text. Escapes all "special" characters, except @.
301 Forward slashes are escaped twice to prevent web servers from prematurely
301 Forward slashes are escaped twice to prevent web servers from prematurely
302 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
302 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
303 """
303 """
304 return urlreq.quote(text, safe='/@').replace('/', '%252F')
304 return urlreq.quote(text, safe='/@').replace('/', '%252F')
305
305
306 @templatefilter('rfc3339date')
306 @templatefilter('rfc3339date')
307 def rfc3339date(text):
307 def rfc3339date(text):
308 """Date. Returns a date using the Internet date format
308 """Date. Returns a date using the Internet date format
309 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
309 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
310 """
310 """
311 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
311 return dateutil.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
312
312
313 @templatefilter('rfc822date')
313 @templatefilter('rfc822date')
314 def rfc822date(text):
314 def rfc822date(text):
315 """Date. Returns a date using the same format used in email
315 """Date. Returns a date using the same format used in email
316 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
316 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
317 """
317 """
318 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
318 return dateutil.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
319
319
320 @templatefilter('short')
320 @templatefilter('short')
321 def short(text):
321 def short(text):
322 """Changeset hash. Returns the short form of a changeset hash,
322 """Changeset hash. Returns the short form of a changeset hash,
323 i.e. a 12 hexadecimal digit string.
323 i.e. a 12 hexadecimal digit string.
324 """
324 """
325 return text[:12]
325 return text[:12]
326
326
327 @templatefilter('shortbisect')
327 @templatefilter('shortbisect')
328 def shortbisect(label):
328 def shortbisect(label):
329 """Any text. Treats `label` as a bisection status, and
329 """Any text. Treats `label` as a bisection status, and
330 returns a single-character representing the status (G: good, B: bad,
330 returns a single-character representing the status (G: good, B: bad,
331 S: skipped, U: untested, I: ignored). Returns single space if `text`
331 S: skipped, U: untested, I: ignored). Returns single space if `text`
332 is not a valid bisection status.
332 is not a valid bisection status.
333 """
333 """
334 if label:
334 if label:
335 return label[0:1].upper()
335 return label[0:1].upper()
336 return ' '
336 return ' '
337
337
338 @templatefilter('shortdate')
338 @templatefilter('shortdate')
339 def shortdate(text):
339 def shortdate(text):
340 """Date. Returns a date like "2006-09-18"."""
340 """Date. Returns a date like "2006-09-18"."""
341 return dateutil.shortdate(text)
341 return dateutil.shortdate(text)
342
342
343 @templatefilter('slashpath')
343 @templatefilter('slashpath')
344 def slashpath(path):
344 def slashpath(path):
345 """Any text. Replaces the native path separator with slash."""
345 """Any text. Replaces the native path separator with slash."""
346 return util.pconvert(path)
346 return util.pconvert(path)
347
347
348 @templatefilter('splitlines')
348 @templatefilter('splitlines')
349 def splitlines(text):
349 def splitlines(text):
350 """Any text. Split text into a list of lines."""
350 """Any text. Split text into a list of lines."""
351 return templateutil.hybridlist(text.splitlines(), name='line')
351 return templateutil.hybridlist(text.splitlines(), name='line')
352
352
353 @templatefilter('stringescape')
353 @templatefilter('stringescape')
354 def stringescape(text):
354 def stringescape(text):
355 return stringutil.escapestr(text)
355 return stringutil.escapestr(text)
356
356
357 @templatefilter('stringify')
357 @templatefilter('stringify', intype=bytes)
358 def stringify(thing):
358 def stringify(thing):
359 """Any type. Turns the value into text by converting values into
359 """Any type. Turns the value into text by converting values into
360 text and concatenating them.
360 text and concatenating them.
361 """
361 """
362 return templateutil.stringify(thing)
362 return thing # coerced by the intype
363
363
364 @templatefilter('stripdir')
364 @templatefilter('stripdir')
365 def stripdir(text):
365 def stripdir(text):
366 """Treat the text as path and strip a directory level, if
366 """Treat the text as path and strip a directory level, if
367 possible. For example, "foo" and "foo/bar" becomes "foo".
367 possible. For example, "foo" and "foo/bar" becomes "foo".
368 """
368 """
369 dir = os.path.dirname(text)
369 dir = os.path.dirname(text)
370 if dir == "":
370 if dir == "":
371 return os.path.basename(text)
371 return os.path.basename(text)
372 else:
372 else:
373 return dir
373 return dir
374
374
375 @templatefilter('tabindent')
375 @templatefilter('tabindent')
376 def tabindent(text):
376 def tabindent(text):
377 """Any text. Returns the text, with every non-empty line
377 """Any text. Returns the text, with every non-empty line
378 except the first starting with a tab character.
378 except the first starting with a tab character.
379 """
379 """
380 return indent(text, '\t')
380 return indent(text, '\t')
381
381
382 @templatefilter('upper')
382 @templatefilter('upper')
383 def upper(text):
383 def upper(text):
384 """Any text. Converts the text to uppercase."""
384 """Any text. Converts the text to uppercase."""
385 return encoding.upper(text)
385 return encoding.upper(text)
386
386
387 @templatefilter('urlescape')
387 @templatefilter('urlescape')
388 def urlescape(text):
388 def urlescape(text):
389 """Any text. Escapes all "special" characters. For example,
389 """Any text. Escapes all "special" characters. For example,
390 "foo bar" becomes "foo%20bar".
390 "foo bar" becomes "foo%20bar".
391 """
391 """
392 return urlreq.quote(text)
392 return urlreq.quote(text)
393
393
394 @templatefilter('user')
394 @templatefilter('user')
395 def userfilter(text):
395 def userfilter(text):
396 """Any text. Returns a short representation of a user name or email
396 """Any text. Returns a short representation of a user name or email
397 address."""
397 address."""
398 return stringutil.shortuser(text)
398 return stringutil.shortuser(text)
399
399
400 @templatefilter('emailuser')
400 @templatefilter('emailuser')
401 def emailuser(text):
401 def emailuser(text):
402 """Any text. Returns the user portion of an email address."""
402 """Any text. Returns the user portion of an email address."""
403 return stringutil.emailuser(text)
403 return stringutil.emailuser(text)
404
404
405 @templatefilter('utf8')
405 @templatefilter('utf8')
406 def utf8(text):
406 def utf8(text):
407 """Any text. Converts from the local character encoding to UTF-8."""
407 """Any text. Converts from the local character encoding to UTF-8."""
408 return encoding.fromlocal(text)
408 return encoding.fromlocal(text)
409
409
410 @templatefilter('xmlescape')
410 @templatefilter('xmlescape')
411 def xmlescape(text):
411 def xmlescape(text):
412 text = (text
412 text = (text
413 .replace('&', '&amp;')
413 .replace('&', '&amp;')
414 .replace('<', '&lt;')
414 .replace('<', '&lt;')
415 .replace('>', '&gt;')
415 .replace('>', '&gt;')
416 .replace('"', '&quot;')
416 .replace('"', '&quot;')
417 .replace("'", '&#39;')) # &apos; invalid in HTML
417 .replace("'", '&#39;')) # &apos; invalid in HTML
418 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
418 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
419
419
420 def websub(text, websubtable):
420 def websub(text, websubtable):
421 """:websub: Any text. Only applies to hgweb. Applies the regular
421 """:websub: Any text. Only applies to hgweb. Applies the regular
422 expression replacements defined in the websub section.
422 expression replacements defined in the websub section.
423 """
423 """
424 if websubtable:
424 if websubtable:
425 for regexp, format in websubtable:
425 for regexp, format in websubtable:
426 text = regexp.sub(format, text)
426 text = regexp.sub(format, text)
427 return text
427 return text
428
428
429 def loadfilter(ui, extname, registrarobj):
429 def loadfilter(ui, extname, registrarobj):
430 """Load template filter from specified registrarobj
430 """Load template filter from specified registrarobj
431 """
431 """
432 for name, func in registrarobj._table.iteritems():
432 for name, func in registrarobj._table.iteritems():
433 filters[name] = func
433 filters[name] = func
434
434
435 # tell hggettext to extract docstrings from these functions:
435 # tell hggettext to extract docstrings from these functions:
436 i18nfunctions = filters.values()
436 i18nfunctions = filters.values()
@@ -1,477 +1,479 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import types
10 import types
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 util,
16 util,
17 )
17 )
18 from .utils import (
18 from .utils import (
19 stringutil,
19 stringutil,
20 )
20 )
21
21
22 class ResourceUnavailable(error.Abort):
22 class ResourceUnavailable(error.Abort):
23 pass
23 pass
24
24
25 class TemplateNotFound(error.Abort):
25 class TemplateNotFound(error.Abort):
26 pass
26 pass
27
27
28 class hybrid(object):
28 class hybrid(object):
29 """Wrapper for list or dict to support legacy template
29 """Wrapper for list or dict to support legacy template
30
30
31 This class allows us to handle both:
31 This class allows us to handle both:
32 - "{files}" (legacy command-line-specific list hack) and
32 - "{files}" (legacy command-line-specific list hack) and
33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
33 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 and to access raw values:
34 and to access raw values:
35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
35 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 - "{get(extras, key)}"
36 - "{get(extras, key)}"
37 - "{files|json}"
37 - "{files|json}"
38 """
38 """
39
39
40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
40 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 if gen is not None:
41 if gen is not None:
42 self.gen = gen # generator or function returning generator
42 self.gen = gen # generator or function returning generator
43 self._values = values
43 self._values = values
44 self._makemap = makemap
44 self._makemap = makemap
45 self.joinfmt = joinfmt
45 self.joinfmt = joinfmt
46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
46 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 def gen(self):
47 def gen(self):
48 """Default generator to stringify this as {join(self, ' ')}"""
48 """Default generator to stringify this as {join(self, ' ')}"""
49 for i, x in enumerate(self._values):
49 for i, x in enumerate(self._values):
50 if i > 0:
50 if i > 0:
51 yield ' '
51 yield ' '
52 yield self.joinfmt(x)
52 yield self.joinfmt(x)
53 def itermaps(self):
53 def itermaps(self):
54 makemap = self._makemap
54 makemap = self._makemap
55 for x in self._values:
55 for x in self._values:
56 yield makemap(x)
56 yield makemap(x)
57 def __contains__(self, x):
57 def __contains__(self, x):
58 return x in self._values
58 return x in self._values
59 def __getitem__(self, key):
59 def __getitem__(self, key):
60 return self._values[key]
60 return self._values[key]
61 def __len__(self):
61 def __len__(self):
62 return len(self._values)
62 return len(self._values)
63 def __iter__(self):
63 def __iter__(self):
64 return iter(self._values)
64 return iter(self._values)
65 def __getattr__(self, name):
65 def __getattr__(self, name):
66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
66 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 r'itervalues', r'keys', r'values'):
67 r'itervalues', r'keys', r'values'):
68 raise AttributeError(name)
68 raise AttributeError(name)
69 return getattr(self._values, name)
69 return getattr(self._values, name)
70
70
71 class mappable(object):
71 class mappable(object):
72 """Wrapper for non-list/dict object to support map operation
72 """Wrapper for non-list/dict object to support map operation
73
73
74 This class allows us to handle both:
74 This class allows us to handle both:
75 - "{manifest}"
75 - "{manifest}"
76 - "{manifest % '{rev}:{node}'}"
76 - "{manifest % '{rev}:{node}'}"
77 - "{manifest.rev}"
77 - "{manifest.rev}"
78
78
79 Unlike a hybrid, this does not simulate the behavior of the underling
79 Unlike a hybrid, this does not simulate the behavior of the underling
80 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
80 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
81 the inner object.
81 the inner object.
82 """
82 """
83
83
84 def __init__(self, gen, key, value, makemap):
84 def __init__(self, gen, key, value, makemap):
85 if gen is not None:
85 if gen is not None:
86 self.gen = gen # generator or function returning generator
86 self.gen = gen # generator or function returning generator
87 self._key = key
87 self._key = key
88 self._value = value # may be generator of strings
88 self._value = value # may be generator of strings
89 self._makemap = makemap
89 self._makemap = makemap
90
90
91 def gen(self):
91 def gen(self):
92 yield pycompat.bytestr(self._value)
92 yield pycompat.bytestr(self._value)
93
93
94 def tomap(self):
94 def tomap(self):
95 return self._makemap(self._key)
95 return self._makemap(self._key)
96
96
97 def itermaps(self):
97 def itermaps(self):
98 yield self.tomap()
98 yield self.tomap()
99
99
100 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
100 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
101 """Wrap data to support both dict-like and string-like operations"""
101 """Wrap data to support both dict-like and string-like operations"""
102 prefmt = pycompat.identity
102 prefmt = pycompat.identity
103 if fmt is None:
103 if fmt is None:
104 fmt = '%s=%s'
104 fmt = '%s=%s'
105 prefmt = pycompat.bytestr
105 prefmt = pycompat.bytestr
106 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
106 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
107 lambda k: fmt % (prefmt(k), prefmt(data[k])))
107 lambda k: fmt % (prefmt(k), prefmt(data[k])))
108
108
109 def hybridlist(data, name, fmt=None, gen=None):
109 def hybridlist(data, name, fmt=None, gen=None):
110 """Wrap data to support both list-like and string-like operations"""
110 """Wrap data to support both list-like and string-like operations"""
111 prefmt = pycompat.identity
111 prefmt = pycompat.identity
112 if fmt is None:
112 if fmt is None:
113 fmt = '%s'
113 fmt = '%s'
114 prefmt = pycompat.bytestr
114 prefmt = pycompat.bytestr
115 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
115 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
116
116
117 def unwraphybrid(thing):
117 def unwraphybrid(thing):
118 """Return an object which can be stringified possibly by using a legacy
118 """Return an object which can be stringified possibly by using a legacy
119 template"""
119 template"""
120 gen = getattr(thing, 'gen', None)
120 gen = getattr(thing, 'gen', None)
121 if gen is None:
121 if gen is None:
122 return thing
122 return thing
123 if callable(gen):
123 if callable(gen):
124 return gen()
124 return gen()
125 return gen
125 return gen
126
126
127 def unwrapvalue(thing):
127 def unwrapvalue(thing):
128 """Move the inner value object out of the wrapper"""
128 """Move the inner value object out of the wrapper"""
129 if not util.safehasattr(thing, '_value'):
129 if not util.safehasattr(thing, '_value'):
130 return thing
130 return thing
131 return thing._value
131 return thing._value
132
132
133 def wraphybridvalue(container, key, value):
133 def wraphybridvalue(container, key, value):
134 """Wrap an element of hybrid container to be mappable
134 """Wrap an element of hybrid container to be mappable
135
135
136 The key is passed to the makemap function of the given container, which
136 The key is passed to the makemap function of the given container, which
137 should be an item generated by iter(container).
137 should be an item generated by iter(container).
138 """
138 """
139 makemap = getattr(container, '_makemap', None)
139 makemap = getattr(container, '_makemap', None)
140 if makemap is None:
140 if makemap is None:
141 return value
141 return value
142 if util.safehasattr(value, '_makemap'):
142 if util.safehasattr(value, '_makemap'):
143 # a nested hybrid list/dict, which has its own way of map operation
143 # a nested hybrid list/dict, which has its own way of map operation
144 return value
144 return value
145 return mappable(None, key, value, makemap)
145 return mappable(None, key, value, makemap)
146
146
147 def compatdict(context, mapping, name, data, key='key', value='value',
147 def compatdict(context, mapping, name, data, key='key', value='value',
148 fmt=None, plural=None, separator=' '):
148 fmt=None, plural=None, separator=' '):
149 """Wrap data like hybriddict(), but also supports old-style list template
149 """Wrap data like hybriddict(), but also supports old-style list template
150
150
151 This exists for backward compatibility with the old-style template. Use
151 This exists for backward compatibility with the old-style template. Use
152 hybriddict() for new template keywords.
152 hybriddict() for new template keywords.
153 """
153 """
154 c = [{key: k, value: v} for k, v in data.iteritems()]
154 c = [{key: k, value: v} for k, v in data.iteritems()]
155 f = _showcompatlist(context, mapping, name, c, plural, separator)
155 f = _showcompatlist(context, mapping, name, c, plural, separator)
156 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
156 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
157
157
158 def compatlist(context, mapping, name, data, element=None, fmt=None,
158 def compatlist(context, mapping, name, data, element=None, fmt=None,
159 plural=None, separator=' '):
159 plural=None, separator=' '):
160 """Wrap data like hybridlist(), but also supports old-style list template
160 """Wrap data like hybridlist(), but also supports old-style list template
161
161
162 This exists for backward compatibility with the old-style template. Use
162 This exists for backward compatibility with the old-style template. Use
163 hybridlist() for new template keywords.
163 hybridlist() for new template keywords.
164 """
164 """
165 f = _showcompatlist(context, mapping, name, data, plural, separator)
165 f = _showcompatlist(context, mapping, name, data, plural, separator)
166 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
166 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
167
167
168 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
168 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
169 """Return a generator that renders old-style list template
169 """Return a generator that renders old-style list template
170
170
171 name is name of key in template map.
171 name is name of key in template map.
172 values is list of strings or dicts.
172 values is list of strings or dicts.
173 plural is plural of name, if not simply name + 's'.
173 plural is plural of name, if not simply name + 's'.
174 separator is used to join values as a string
174 separator is used to join values as a string
175
175
176 expansion works like this, given name 'foo'.
176 expansion works like this, given name 'foo'.
177
177
178 if values is empty, expand 'no_foos'.
178 if values is empty, expand 'no_foos'.
179
179
180 if 'foo' not in template map, return values as a string,
180 if 'foo' not in template map, return values as a string,
181 joined by 'separator'.
181 joined by 'separator'.
182
182
183 expand 'start_foos'.
183 expand 'start_foos'.
184
184
185 for each value, expand 'foo'. if 'last_foo' in template
185 for each value, expand 'foo'. if 'last_foo' in template
186 map, expand it instead of 'foo' for last key.
186 map, expand it instead of 'foo' for last key.
187
187
188 expand 'end_foos'.
188 expand 'end_foos'.
189 """
189 """
190 if not plural:
190 if not plural:
191 plural = name + 's'
191 plural = name + 's'
192 if not values:
192 if not values:
193 noname = 'no_' + plural
193 noname = 'no_' + plural
194 if context.preload(noname):
194 if context.preload(noname):
195 yield context.process(noname, mapping)
195 yield context.process(noname, mapping)
196 return
196 return
197 if not context.preload(name):
197 if not context.preload(name):
198 if isinstance(values[0], bytes):
198 if isinstance(values[0], bytes):
199 yield separator.join(values)
199 yield separator.join(values)
200 else:
200 else:
201 for v in values:
201 for v in values:
202 r = dict(v)
202 r = dict(v)
203 r.update(mapping)
203 r.update(mapping)
204 yield r
204 yield r
205 return
205 return
206 startname = 'start_' + plural
206 startname = 'start_' + plural
207 if context.preload(startname):
207 if context.preload(startname):
208 yield context.process(startname, mapping)
208 yield context.process(startname, mapping)
209 def one(v, tag=name):
209 def one(v, tag=name):
210 vmapping = {}
210 vmapping = {}
211 try:
211 try:
212 vmapping.update(v)
212 vmapping.update(v)
213 # Python 2 raises ValueError if the type of v is wrong. Python
213 # Python 2 raises ValueError if the type of v is wrong. Python
214 # 3 raises TypeError.
214 # 3 raises TypeError.
215 except (AttributeError, TypeError, ValueError):
215 except (AttributeError, TypeError, ValueError):
216 try:
216 try:
217 # Python 2 raises ValueError trying to destructure an e.g.
217 # Python 2 raises ValueError trying to destructure an e.g.
218 # bytes. Python 3 raises TypeError.
218 # bytes. Python 3 raises TypeError.
219 for a, b in v:
219 for a, b in v:
220 vmapping[a] = b
220 vmapping[a] = b
221 except (TypeError, ValueError):
221 except (TypeError, ValueError):
222 vmapping[name] = v
222 vmapping[name] = v
223 vmapping = context.overlaymap(mapping, vmapping)
223 vmapping = context.overlaymap(mapping, vmapping)
224 return context.process(tag, vmapping)
224 return context.process(tag, vmapping)
225 lastname = 'last_' + name
225 lastname = 'last_' + name
226 if context.preload(lastname):
226 if context.preload(lastname):
227 last = values.pop()
227 last = values.pop()
228 else:
228 else:
229 last = None
229 last = None
230 for v in values:
230 for v in values:
231 yield one(v)
231 yield one(v)
232 if last is not None:
232 if last is not None:
233 yield one(last, tag=lastname)
233 yield one(last, tag=lastname)
234 endname = 'end_' + plural
234 endname = 'end_' + plural
235 if context.preload(endname):
235 if context.preload(endname):
236 yield context.process(endname, mapping)
236 yield context.process(endname, mapping)
237
237
238 def flatten(thing):
238 def flatten(thing):
239 """Yield a single stream from a possibly nested set of iterators"""
239 """Yield a single stream from a possibly nested set of iterators"""
240 thing = unwraphybrid(thing)
240 thing = unwraphybrid(thing)
241 if isinstance(thing, bytes):
241 if isinstance(thing, bytes):
242 yield thing
242 yield thing
243 elif isinstance(thing, str):
243 elif isinstance(thing, str):
244 # We can only hit this on Python 3, and it's here to guard
244 # We can only hit this on Python 3, and it's here to guard
245 # against infinite recursion.
245 # against infinite recursion.
246 raise error.ProgrammingError('Mercurial IO including templates is done'
246 raise error.ProgrammingError('Mercurial IO including templates is done'
247 ' with bytes, not strings, got %r' % thing)
247 ' with bytes, not strings, got %r' % thing)
248 elif thing is None:
248 elif thing is None:
249 pass
249 pass
250 elif not util.safehasattr(thing, '__iter__'):
250 elif not util.safehasattr(thing, '__iter__'):
251 yield pycompat.bytestr(thing)
251 yield pycompat.bytestr(thing)
252 else:
252 else:
253 for i in thing:
253 for i in thing:
254 i = unwraphybrid(i)
254 i = unwraphybrid(i)
255 if isinstance(i, bytes):
255 if isinstance(i, bytes):
256 yield i
256 yield i
257 elif i is None:
257 elif i is None:
258 pass
258 pass
259 elif not util.safehasattr(i, '__iter__'):
259 elif not util.safehasattr(i, '__iter__'):
260 yield pycompat.bytestr(i)
260 yield pycompat.bytestr(i)
261 else:
261 else:
262 for j in flatten(i):
262 for j in flatten(i):
263 yield j
263 yield j
264
264
265 def stringify(thing):
265 def stringify(thing):
266 """Turn values into bytes by converting into text and concatenating them"""
266 """Turn values into bytes by converting into text and concatenating them"""
267 if isinstance(thing, bytes):
267 if isinstance(thing, bytes):
268 return thing # retain localstr to be round-tripped
268 return thing # retain localstr to be round-tripped
269 return b''.join(flatten(thing))
269 return b''.join(flatten(thing))
270
270
271 def findsymbolicname(arg):
271 def findsymbolicname(arg):
272 """Find symbolic name for the given compiled expression; returns None
272 """Find symbolic name for the given compiled expression; returns None
273 if nothing found reliably"""
273 if nothing found reliably"""
274 while True:
274 while True:
275 func, data = arg
275 func, data = arg
276 if func is runsymbol:
276 if func is runsymbol:
277 return data
277 return data
278 elif func is runfilter:
278 elif func is runfilter:
279 arg = data[0]
279 arg = data[0]
280 else:
280 else:
281 return None
281 return None
282
282
283 def evalrawexp(context, mapping, arg):
283 def evalrawexp(context, mapping, arg):
284 """Evaluate given argument as a bare template object which may require
284 """Evaluate given argument as a bare template object which may require
285 further processing (such as folding generator of strings)"""
285 further processing (such as folding generator of strings)"""
286 func, data = arg
286 func, data = arg
287 return func(context, mapping, data)
287 return func(context, mapping, data)
288
288
289 def evalfuncarg(context, mapping, arg):
289 def evalfuncarg(context, mapping, arg):
290 """Evaluate given argument as value type"""
290 """Evaluate given argument as value type"""
291 return _unwrapvalue(evalrawexp(context, mapping, arg))
291 return _unwrapvalue(evalrawexp(context, mapping, arg))
292
292
293 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
293 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
294 # is fixed. we can't do that right now because join() has to take a generator
294 # is fixed. we can't do that right now because join() has to take a generator
295 # of byte strings as it is, not a lazy byte string.
295 # of byte strings as it is, not a lazy byte string.
296 def _unwrapvalue(thing):
296 def _unwrapvalue(thing):
297 thing = unwrapvalue(thing)
297 thing = unwrapvalue(thing)
298 # evalrawexp() may return string, generator of strings or arbitrary object
298 # evalrawexp() may return string, generator of strings or arbitrary object
299 # such as date tuple, but filter does not want generator.
299 # such as date tuple, but filter does not want generator.
300 if isinstance(thing, types.GeneratorType):
300 if isinstance(thing, types.GeneratorType):
301 thing = stringify(thing)
301 thing = stringify(thing)
302 return thing
302 return thing
303
303
304 def evalboolean(context, mapping, arg):
304 def evalboolean(context, mapping, arg):
305 """Evaluate given argument as boolean, but also takes boolean literals"""
305 """Evaluate given argument as boolean, but also takes boolean literals"""
306 func, data = arg
306 func, data = arg
307 if func is runsymbol:
307 if func is runsymbol:
308 thing = func(context, mapping, data, default=None)
308 thing = func(context, mapping, data, default=None)
309 if thing is None:
309 if thing is None:
310 # not a template keyword, takes as a boolean literal
310 # not a template keyword, takes as a boolean literal
311 thing = stringutil.parsebool(data)
311 thing = stringutil.parsebool(data)
312 else:
312 else:
313 thing = func(context, mapping, data)
313 thing = func(context, mapping, data)
314 thing = unwrapvalue(thing)
314 thing = unwrapvalue(thing)
315 if isinstance(thing, bool):
315 if isinstance(thing, bool):
316 return thing
316 return thing
317 # other objects are evaluated as strings, which means 0 is True, but
317 # other objects are evaluated as strings, which means 0 is True, but
318 # empty dict/list should be False as they are expected to be ''
318 # empty dict/list should be False as they are expected to be ''
319 return bool(stringify(thing))
319 return bool(stringify(thing))
320
320
321 def evalinteger(context, mapping, arg, err=None):
321 def evalinteger(context, mapping, arg, err=None):
322 return unwrapinteger(evalrawexp(context, mapping, arg), err)
322 return unwrapinteger(evalrawexp(context, mapping, arg), err)
323
323
324 def unwrapinteger(thing, err=None):
324 def unwrapinteger(thing, err=None):
325 thing = _unwrapvalue(thing)
325 thing = _unwrapvalue(thing)
326 try:
326 try:
327 return int(thing)
327 return int(thing)
328 except (TypeError, ValueError):
328 except (TypeError, ValueError):
329 raise error.ParseError(err or _('not an integer'))
329 raise error.ParseError(err or _('not an integer'))
330
330
331 def evalstring(context, mapping, arg):
331 def evalstring(context, mapping, arg):
332 return stringify(evalrawexp(context, mapping, arg))
332 return stringify(evalrawexp(context, mapping, arg))
333
333
334 def evalstringliteral(context, mapping, arg):
334 def evalstringliteral(context, mapping, arg):
335 """Evaluate given argument as string template, but returns symbol name
335 """Evaluate given argument as string template, but returns symbol name
336 if it is unknown"""
336 if it is unknown"""
337 func, data = arg
337 func, data = arg
338 if func is runsymbol:
338 if func is runsymbol:
339 thing = func(context, mapping, data, default=data)
339 thing = func(context, mapping, data, default=data)
340 else:
340 else:
341 thing = func(context, mapping, data)
341 thing = func(context, mapping, data)
342 return stringify(thing)
342 return stringify(thing)
343
343
344 _unwrapfuncbytype = {
344 _unwrapfuncbytype = {
345 None: _unwrapvalue,
345 bytes: stringify,
346 bytes: stringify,
346 int: unwrapinteger,
347 int: unwrapinteger,
347 }
348 }
348
349
349 def unwrapastype(thing, typ):
350 def unwrapastype(thing, typ):
350 """Move the inner value object out of the wrapper and coerce its type"""
351 """Move the inner value object out of the wrapper and coerce its type"""
351 try:
352 try:
352 f = _unwrapfuncbytype[typ]
353 f = _unwrapfuncbytype[typ]
353 except KeyError:
354 except KeyError:
354 raise error.ProgrammingError('invalid type specified: %r' % typ)
355 raise error.ProgrammingError('invalid type specified: %r' % typ)
355 return f(thing)
356 return f(thing)
356
357
357 def runinteger(context, mapping, data):
358 def runinteger(context, mapping, data):
358 return int(data)
359 return int(data)
359
360
360 def runstring(context, mapping, data):
361 def runstring(context, mapping, data):
361 return data
362 return data
362
363
363 def _recursivesymbolblocker(key):
364 def _recursivesymbolblocker(key):
364 def showrecursion(**args):
365 def showrecursion(**args):
365 raise error.Abort(_("recursive reference '%s' in template") % key)
366 raise error.Abort(_("recursive reference '%s' in template") % key)
366 return showrecursion
367 return showrecursion
367
368
368 def runsymbol(context, mapping, key, default=''):
369 def runsymbol(context, mapping, key, default=''):
369 v = context.symbol(mapping, key)
370 v = context.symbol(mapping, key)
370 if v is None:
371 if v is None:
371 # put poison to cut recursion. we can't move this to parsing phase
372 # put poison to cut recursion. we can't move this to parsing phase
372 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
373 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
373 safemapping = mapping.copy()
374 safemapping = mapping.copy()
374 safemapping[key] = _recursivesymbolblocker(key)
375 safemapping[key] = _recursivesymbolblocker(key)
375 try:
376 try:
376 v = context.process(key, safemapping)
377 v = context.process(key, safemapping)
377 except TemplateNotFound:
378 except TemplateNotFound:
378 v = default
379 v = default
379 if callable(v) and getattr(v, '_requires', None) is None:
380 if callable(v) and getattr(v, '_requires', None) is None:
380 # old templatekw: expand all keywords and resources
381 # old templatekw: expand all keywords and resources
381 # (TODO: deprecate this after porting web template keywords to new API)
382 # (TODO: deprecate this after porting web template keywords to new API)
382 props = {k: context._resources.lookup(context, mapping, k)
383 props = {k: context._resources.lookup(context, mapping, k)
383 for k in context._resources.knownkeys()}
384 for k in context._resources.knownkeys()}
384 # pass context to _showcompatlist() through templatekw._showlist()
385 # pass context to _showcompatlist() through templatekw._showlist()
385 props['templ'] = context
386 props['templ'] = context
386 props.update(mapping)
387 props.update(mapping)
387 return v(**pycompat.strkwargs(props))
388 return v(**pycompat.strkwargs(props))
388 if callable(v):
389 if callable(v):
389 # new templatekw
390 # new templatekw
390 try:
391 try:
391 return v(context, mapping)
392 return v(context, mapping)
392 except ResourceUnavailable:
393 except ResourceUnavailable:
393 # unsupported keyword is mapped to empty just like unknown keyword
394 # unsupported keyword is mapped to empty just like unknown keyword
394 return None
395 return None
395 return v
396 return v
396
397
397 def runtemplate(context, mapping, template):
398 def runtemplate(context, mapping, template):
398 for arg in template:
399 for arg in template:
399 yield evalrawexp(context, mapping, arg)
400 yield evalrawexp(context, mapping, arg)
400
401
401 def runfilter(context, mapping, data):
402 def runfilter(context, mapping, data):
402 arg, filt = data
403 arg, filt = data
403 thing = evalfuncarg(context, mapping, arg)
404 thing = evalrawexp(context, mapping, arg)
404 try:
405 try:
406 thing = unwrapastype(thing, getattr(filt, '_intype', None))
405 return filt(thing)
407 return filt(thing)
406 except (ValueError, AttributeError, TypeError):
408 except (ValueError, AttributeError, TypeError):
407 sym = findsymbolicname(arg)
409 sym = findsymbolicname(arg)
408 if sym:
410 if sym:
409 msg = (_("template filter '%s' is not compatible with keyword '%s'")
411 msg = (_("template filter '%s' is not compatible with keyword '%s'")
410 % (pycompat.sysbytes(filt.__name__), sym))
412 % (pycompat.sysbytes(filt.__name__), sym))
411 else:
413 else:
412 msg = (_("incompatible use of template filter '%s'")
414 msg = (_("incompatible use of template filter '%s'")
413 % pycompat.sysbytes(filt.__name__))
415 % pycompat.sysbytes(filt.__name__))
414 raise error.Abort(msg)
416 raise error.Abort(msg)
415
417
416 def runmap(context, mapping, data):
418 def runmap(context, mapping, data):
417 darg, targ = data
419 darg, targ = data
418 d = evalrawexp(context, mapping, darg)
420 d = evalrawexp(context, mapping, darg)
419 if util.safehasattr(d, 'itermaps'):
421 if util.safehasattr(d, 'itermaps'):
420 diter = d.itermaps()
422 diter = d.itermaps()
421 else:
423 else:
422 try:
424 try:
423 diter = iter(d)
425 diter = iter(d)
424 except TypeError:
426 except TypeError:
425 sym = findsymbolicname(darg)
427 sym = findsymbolicname(darg)
426 if sym:
428 if sym:
427 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
429 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
428 else:
430 else:
429 raise error.ParseError(_("%r is not iterable") % d)
431 raise error.ParseError(_("%r is not iterable") % d)
430
432
431 for i, v in enumerate(diter):
433 for i, v in enumerate(diter):
432 if isinstance(v, dict):
434 if isinstance(v, dict):
433 lm = context.overlaymap(mapping, v)
435 lm = context.overlaymap(mapping, v)
434 lm['index'] = i
436 lm['index'] = i
435 yield evalrawexp(context, lm, targ)
437 yield evalrawexp(context, lm, targ)
436 else:
438 else:
437 # v is not an iterable of dicts, this happen when 'key'
439 # v is not an iterable of dicts, this happen when 'key'
438 # has been fully expanded already and format is useless.
440 # has been fully expanded already and format is useless.
439 # If so, return the expanded value.
441 # If so, return the expanded value.
440 yield v
442 yield v
441
443
442 def runmember(context, mapping, data):
444 def runmember(context, mapping, data):
443 darg, memb = data
445 darg, memb = data
444 d = evalrawexp(context, mapping, darg)
446 d = evalrawexp(context, mapping, darg)
445 if util.safehasattr(d, 'tomap'):
447 if util.safehasattr(d, 'tomap'):
446 lm = context.overlaymap(mapping, d.tomap())
448 lm = context.overlaymap(mapping, d.tomap())
447 return runsymbol(context, lm, memb)
449 return runsymbol(context, lm, memb)
448 if util.safehasattr(d, 'get'):
450 if util.safehasattr(d, 'get'):
449 return getdictitem(d, memb)
451 return getdictitem(d, memb)
450
452
451 sym = findsymbolicname(darg)
453 sym = findsymbolicname(darg)
452 if sym:
454 if sym:
453 raise error.ParseError(_("keyword '%s' has no member") % sym)
455 raise error.ParseError(_("keyword '%s' has no member") % sym)
454 else:
456 else:
455 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
457 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
456
458
457 def runnegate(context, mapping, data):
459 def runnegate(context, mapping, data):
458 data = evalinteger(context, mapping, data,
460 data = evalinteger(context, mapping, data,
459 _('negation needs an integer argument'))
461 _('negation needs an integer argument'))
460 return -data
462 return -data
461
463
462 def runarithmetic(context, mapping, data):
464 def runarithmetic(context, mapping, data):
463 func, left, right = data
465 func, left, right = data
464 left = evalinteger(context, mapping, left,
466 left = evalinteger(context, mapping, left,
465 _('arithmetic only defined on integers'))
467 _('arithmetic only defined on integers'))
466 right = evalinteger(context, mapping, right,
468 right = evalinteger(context, mapping, right,
467 _('arithmetic only defined on integers'))
469 _('arithmetic only defined on integers'))
468 try:
470 try:
469 return func(left, right)
471 return func(left, right)
470 except ZeroDivisionError:
472 except ZeroDivisionError:
471 raise error.Abort(_('division by zero is not defined'))
473 raise error.Abort(_('division by zero is not defined'))
472
474
473 def getdictitem(dictarg, key):
475 def getdictitem(dictarg, key):
474 val = dictarg.get(key)
476 val = dictarg.get(key)
475 if val is None:
477 if val is None:
476 return
478 return
477 return wraphybridvalue(dictarg, key, val)
479 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now