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