##// END OF EJS Templates
fileset: drop false function signatures from revs() and status() docs...
Yuya Nishihara -
r31254:2140e12d default
parent child Browse files
Show More
@@ -1,634 +1,630
1 1 # fileset.py - file set queries for mercurial
2 2 #
3 3 # Copyright 2010 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 re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 merge,
16 16 parser,
17 17 registrar,
18 18 scmutil,
19 19 util,
20 20 )
21 21
22 22 elements = {
23 23 # token-type: binding-strength, primary, prefix, infix, suffix
24 24 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
25 25 "-": (5, None, ("negate", 19), ("minus", 5), None),
26 26 "not": (10, None, ("not", 10), None, None),
27 27 "!": (10, None, ("not", 10), None, None),
28 28 "and": (5, None, None, ("and", 5), None),
29 29 "&": (5, None, None, ("and", 5), None),
30 30 "or": (4, None, None, ("or", 4), None),
31 31 "|": (4, None, None, ("or", 4), None),
32 32 "+": (4, None, None, ("or", 4), None),
33 33 ",": (2, None, None, ("list", 2), None),
34 34 ")": (0, None, None, None, None),
35 35 "symbol": (0, "symbol", None, None, None),
36 36 "string": (0, "string", None, None, None),
37 37 "end": (0, None, None, None, None),
38 38 }
39 39
40 40 keywords = set(['and', 'or', 'not'])
41 41
42 42 globchars = ".*{}[]?/\\_"
43 43
44 44 def tokenize(program):
45 45 pos, l = 0, len(program)
46 46 while pos < l:
47 47 c = program[pos]
48 48 if c.isspace(): # skip inter-token whitespace
49 49 pass
50 50 elif c in "(),-|&+!": # handle simple operators
51 51 yield (c, None, pos)
52 52 elif (c in '"\'' or c == 'r' and
53 53 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
54 54 if c == 'r':
55 55 pos += 1
56 56 c = program[pos]
57 57 decode = lambda x: x
58 58 else:
59 59 decode = parser.unescapestr
60 60 pos += 1
61 61 s = pos
62 62 while pos < l: # find closing quote
63 63 d = program[pos]
64 64 if d == '\\': # skip over escaped characters
65 65 pos += 2
66 66 continue
67 67 if d == c:
68 68 yield ('string', decode(program[s:pos]), s)
69 69 break
70 70 pos += 1
71 71 else:
72 72 raise error.ParseError(_("unterminated string"), s)
73 73 elif c.isalnum() or c in globchars or ord(c) > 127:
74 74 # gather up a symbol/keyword
75 75 s = pos
76 76 pos += 1
77 77 while pos < l: # find end of symbol
78 78 d = program[pos]
79 79 if not (d.isalnum() or d in globchars or ord(d) > 127):
80 80 break
81 81 pos += 1
82 82 sym = program[s:pos]
83 83 if sym in keywords: # operator keywords
84 84 yield (sym, None, s)
85 85 else:
86 86 yield ('symbol', sym, s)
87 87 pos -= 1
88 88 else:
89 89 raise error.ParseError(_("syntax error"), pos)
90 90 pos += 1
91 91 yield ('end', None, pos)
92 92
93 93 def parse(expr):
94 94 p = parser.parser(elements)
95 95 tree, pos = p.parse(tokenize(expr))
96 96 if pos != len(expr):
97 97 raise error.ParseError(_("invalid token"), pos)
98 98 return tree
99 99
100 100 def getstring(x, err):
101 101 if x and (x[0] == 'string' or x[0] == 'symbol'):
102 102 return x[1]
103 103 raise error.ParseError(err)
104 104
105 105 def getset(mctx, x):
106 106 if not x:
107 107 raise error.ParseError(_("missing argument"))
108 108 return methods[x[0]](mctx, *x[1:])
109 109
110 110 def stringset(mctx, x):
111 111 m = mctx.matcher([x])
112 112 return [f for f in mctx.subset if m(f)]
113 113
114 114 def andset(mctx, x, y):
115 115 return getset(mctx.narrow(getset(mctx, x)), y)
116 116
117 117 def orset(mctx, x, y):
118 118 # needs optimizing
119 119 xl = getset(mctx, x)
120 120 yl = getset(mctx, y)
121 121 return xl + [f for f in yl if f not in xl]
122 122
123 123 def notset(mctx, x):
124 124 s = set(getset(mctx, x))
125 125 return [r for r in mctx.subset if r not in s]
126 126
127 127 def minusset(mctx, x, y):
128 128 xl = getset(mctx, x)
129 129 yl = set(getset(mctx, y))
130 130 return [f for f in xl if f not in yl]
131 131
132 132 def listset(mctx, a, b):
133 133 raise error.ParseError(_("can't use a list in this context"),
134 134 hint=_('see hg help "filesets.x or y"'))
135 135
136 136 # symbols are callable like:
137 137 # fun(mctx, x)
138 138 # with:
139 139 # mctx - current matchctx instance
140 140 # x - argument in tree form
141 141 symbols = {}
142 142
143 143 # filesets using matchctx.status()
144 144 _statuscallers = set()
145 145
146 146 # filesets using matchctx.existing()
147 147 _existingcallers = set()
148 148
149 149 predicate = registrar.filesetpredicate()
150 150
151 151 @predicate('modified()', callstatus=True)
152 152 def modified(mctx, x):
153 153 """File that is modified according to :hg:`status`.
154 154 """
155 155 # i18n: "modified" is a keyword
156 156 getargs(x, 0, 0, _("modified takes no arguments"))
157 157 s = mctx.status().modified
158 158 return [f for f in mctx.subset if f in s]
159 159
160 160 @predicate('added()', callstatus=True)
161 161 def added(mctx, x):
162 162 """File that is added according to :hg:`status`.
163 163 """
164 164 # i18n: "added" is a keyword
165 165 getargs(x, 0, 0, _("added takes no arguments"))
166 166 s = mctx.status().added
167 167 return [f for f in mctx.subset if f in s]
168 168
169 169 @predicate('removed()', callstatus=True)
170 170 def removed(mctx, x):
171 171 """File that is removed according to :hg:`status`.
172 172 """
173 173 # i18n: "removed" is a keyword
174 174 getargs(x, 0, 0, _("removed takes no arguments"))
175 175 s = mctx.status().removed
176 176 return [f for f in mctx.subset if f in s]
177 177
178 178 @predicate('deleted()', callstatus=True)
179 179 def deleted(mctx, x):
180 180 """Alias for ``missing()``.
181 181 """
182 182 # i18n: "deleted" is a keyword
183 183 getargs(x, 0, 0, _("deleted takes no arguments"))
184 184 s = mctx.status().deleted
185 185 return [f for f in mctx.subset if f in s]
186 186
187 187 @predicate('missing()', callstatus=True)
188 188 def missing(mctx, x):
189 189 """File that is missing according to :hg:`status`.
190 190 """
191 191 # i18n: "missing" is a keyword
192 192 getargs(x, 0, 0, _("missing takes no arguments"))
193 193 s = mctx.status().deleted
194 194 return [f for f in mctx.subset if f in s]
195 195
196 196 @predicate('unknown()', callstatus=True)
197 197 def unknown(mctx, x):
198 198 """File that is unknown according to :hg:`status`. These files will only be
199 199 considered if this predicate is used.
200 200 """
201 201 # i18n: "unknown" is a keyword
202 202 getargs(x, 0, 0, _("unknown takes no arguments"))
203 203 s = mctx.status().unknown
204 204 return [f for f in mctx.subset if f in s]
205 205
206 206 @predicate('ignored()', callstatus=True)
207 207 def ignored(mctx, x):
208 208 """File that is ignored according to :hg:`status`. These files will only be
209 209 considered if this predicate is used.
210 210 """
211 211 # i18n: "ignored" is a keyword
212 212 getargs(x, 0, 0, _("ignored takes no arguments"))
213 213 s = mctx.status().ignored
214 214 return [f for f in mctx.subset if f in s]
215 215
216 216 @predicate('clean()', callstatus=True)
217 217 def clean(mctx, x):
218 218 """File that is clean according to :hg:`status`.
219 219 """
220 220 # i18n: "clean" is a keyword
221 221 getargs(x, 0, 0, _("clean takes no arguments"))
222 222 s = mctx.status().clean
223 223 return [f for f in mctx.subset if f in s]
224 224
225 225 def func(mctx, a, b):
226 226 if a[0] == 'symbol' and a[1] in symbols:
227 227 funcname = a[1]
228 228 enabled = mctx._existingenabled
229 229 mctx._existingenabled = funcname in _existingcallers
230 230 try:
231 231 return symbols[funcname](mctx, b)
232 232 finally:
233 233 mctx._existingenabled = enabled
234 234
235 235 keep = lambda fn: getattr(fn, '__doc__', None) is not None
236 236
237 237 syms = [s for (s, fn) in symbols.items() if keep(fn)]
238 238 raise error.UnknownIdentifier(a[1], syms)
239 239
240 240 def getlist(x):
241 241 if not x:
242 242 return []
243 243 if x[0] == 'list':
244 244 return getlist(x[1]) + [x[2]]
245 245 return [x]
246 246
247 247 def getargs(x, min, max, err):
248 248 l = getlist(x)
249 249 if len(l) < min or len(l) > max:
250 250 raise error.ParseError(err)
251 251 return l
252 252
253 253 @predicate('binary()', callexisting=True)
254 254 def binary(mctx, x):
255 255 """File that appears to be binary (contains NUL bytes).
256 256 """
257 257 # i18n: "binary" is a keyword
258 258 getargs(x, 0, 0, _("binary takes no arguments"))
259 259 return [f for f in mctx.existing() if util.binary(mctx.ctx[f].data())]
260 260
261 261 @predicate('exec()', callexisting=True)
262 262 def exec_(mctx, x):
263 263 """File that is marked as executable.
264 264 """
265 265 # i18n: "exec" is a keyword
266 266 getargs(x, 0, 0, _("exec takes no arguments"))
267 267 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'x']
268 268
269 269 @predicate('symlink()', callexisting=True)
270 270 def symlink(mctx, x):
271 271 """File that is marked as a symlink.
272 272 """
273 273 # i18n: "symlink" is a keyword
274 274 getargs(x, 0, 0, _("symlink takes no arguments"))
275 275 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'l']
276 276
277 277 @predicate('resolved()')
278 278 def resolved(mctx, x):
279 279 """File that is marked resolved according to :hg:`resolve -l`.
280 280 """
281 281 # i18n: "resolved" is a keyword
282 282 getargs(x, 0, 0, _("resolved takes no arguments"))
283 283 if mctx.ctx.rev() is not None:
284 284 return []
285 285 ms = merge.mergestate.read(mctx.ctx.repo())
286 286 return [f for f in mctx.subset if f in ms and ms[f] == 'r']
287 287
288 288 @predicate('unresolved()')
289 289 def unresolved(mctx, x):
290 290 """File that is marked unresolved according to :hg:`resolve -l`.
291 291 """
292 292 # i18n: "unresolved" is a keyword
293 293 getargs(x, 0, 0, _("unresolved takes no arguments"))
294 294 if mctx.ctx.rev() is not None:
295 295 return []
296 296 ms = merge.mergestate.read(mctx.ctx.repo())
297 297 return [f for f in mctx.subset if f in ms and ms[f] == 'u']
298 298
299 299 @predicate('hgignore()')
300 300 def hgignore(mctx, x):
301 301 """File that matches the active .hgignore pattern.
302 302 """
303 303 # i18n: "hgignore" is a keyword
304 304 getargs(x, 0, 0, _("hgignore takes no arguments"))
305 305 ignore = mctx.ctx.repo().dirstate._ignore
306 306 return [f for f in mctx.subset if ignore(f)]
307 307
308 308 @predicate('portable()')
309 309 def portable(mctx, x):
310 310 """File that has a portable name. (This doesn't include filenames with case
311 311 collisions.)
312 312 """
313 313 # i18n: "portable" is a keyword
314 314 getargs(x, 0, 0, _("portable takes no arguments"))
315 315 checkwinfilename = util.checkwinfilename
316 316 return [f for f in mctx.subset if checkwinfilename(f) is None]
317 317
318 318 @predicate('grep(regex)', callexisting=True)
319 319 def grep(mctx, x):
320 320 """File contains the given regular expression.
321 321 """
322 322 try:
323 323 # i18n: "grep" is a keyword
324 324 r = re.compile(getstring(x, _("grep requires a pattern")))
325 325 except re.error as e:
326 326 raise error.ParseError(_('invalid match pattern: %s') % e)
327 327 return [f for f in mctx.existing() if r.search(mctx.ctx[f].data())]
328 328
329 329 def _sizetomax(s):
330 330 try:
331 331 s = s.strip().lower()
332 332 for k, v in util._sizeunits:
333 333 if s.endswith(k):
334 334 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
335 335 n = s[:-len(k)]
336 336 inc = 1.0
337 337 if "." in n:
338 338 inc /= 10 ** len(n.split(".")[1])
339 339 return int((float(n) + inc) * v) - 1
340 340 # no extension, this is a precise value
341 341 return int(s)
342 342 except ValueError:
343 343 raise error.ParseError(_("couldn't parse size: %s") % s)
344 344
345 345 @predicate('size(expression)', callexisting=True)
346 346 def size(mctx, x):
347 347 """File size matches the given expression. Examples:
348 348
349 349 - size('1k') - files from 1024 to 2047 bytes
350 350 - size('< 20k') - files less than 20480 bytes
351 351 - size('>= .5MB') - files at least 524288 bytes
352 352 - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
353 353 """
354 354
355 355 # i18n: "size" is a keyword
356 356 expr = getstring(x, _("size requires an expression")).strip()
357 357 if '-' in expr: # do we have a range?
358 358 a, b = expr.split('-', 1)
359 359 a = util.sizetoint(a)
360 360 b = util.sizetoint(b)
361 361 m = lambda x: x >= a and x <= b
362 362 elif expr.startswith("<="):
363 363 a = util.sizetoint(expr[2:])
364 364 m = lambda x: x <= a
365 365 elif expr.startswith("<"):
366 366 a = util.sizetoint(expr[1:])
367 367 m = lambda x: x < a
368 368 elif expr.startswith(">="):
369 369 a = util.sizetoint(expr[2:])
370 370 m = lambda x: x >= a
371 371 elif expr.startswith(">"):
372 372 a = util.sizetoint(expr[1:])
373 373 m = lambda x: x > a
374 374 elif expr[0].isdigit or expr[0] == '.':
375 375 a = util.sizetoint(expr)
376 376 b = _sizetomax(expr)
377 377 m = lambda x: x >= a and x <= b
378 378 else:
379 379 raise error.ParseError(_("couldn't parse size: %s") % expr)
380 380
381 381 return [f for f in mctx.existing() if m(mctx.ctx[f].size())]
382 382
383 383 @predicate('encoding(name)', callexisting=True)
384 384 def encoding(mctx, x):
385 385 """File can be successfully decoded with the given character
386 386 encoding. May not be useful for encodings other than ASCII and
387 387 UTF-8.
388 388 """
389 389
390 390 # i18n: "encoding" is a keyword
391 391 enc = getstring(x, _("encoding requires an encoding name"))
392 392
393 393 s = []
394 394 for f in mctx.existing():
395 395 d = mctx.ctx[f].data()
396 396 try:
397 397 d.decode(enc)
398 398 except LookupError:
399 399 raise error.Abort(_("unknown encoding '%s'") % enc)
400 400 except UnicodeDecodeError:
401 401 continue
402 402 s.append(f)
403 403
404 404 return s
405 405
406 406 @predicate('eol(style)', callexisting=True)
407 407 def eol(mctx, x):
408 408 """File contains newlines of the given style (dos, unix, mac). Binary
409 409 files are excluded, files with mixed line endings match multiple
410 410 styles.
411 411 """
412 412
413 413 # i18n: "eol" is a keyword
414 414 enc = getstring(x, _("eol requires a style name"))
415 415
416 416 s = []
417 417 for f in mctx.existing():
418 418 d = mctx.ctx[f].data()
419 419 if util.binary(d):
420 420 continue
421 421 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
422 422 s.append(f)
423 423 elif enc == 'unix' and re.search('(?<!\r)\n', d):
424 424 s.append(f)
425 425 elif enc == 'mac' and re.search('\r(?!\n)', d):
426 426 s.append(f)
427 427 return s
428 428
429 429 @predicate('copied()')
430 430 def copied(mctx, x):
431 431 """File that is recorded as being copied.
432 432 """
433 433 # i18n: "copied" is a keyword
434 434 getargs(x, 0, 0, _("copied takes no arguments"))
435 435 s = []
436 436 for f in mctx.subset:
437 437 p = mctx.ctx[f].parents()
438 438 if p and p[0].path() != f:
439 439 s.append(f)
440 440 return s
441 441
442 442 @predicate('revs(revs, pattern)')
443 443 def revs(mctx, x):
444 """``revs(set, revspec)``
445
446 Evaluate set in the specified revisions. If the revset match multiple revs,
447 this will return file matching pattern in any of the revision.
444 """Evaluate set in the specified revisions. If the revset match multiple
445 revs, this will return file matching pattern in any of the revision.
448 446 """
449 447 # i18n: "revs" is a keyword
450 448 r, x = getargs(x, 2, 2, _("revs takes two arguments"))
451 449 # i18n: "revs" is a keyword
452 450 revspec = getstring(r, _("first argument to revs must be a revision"))
453 451 repo = mctx.ctx.repo()
454 452 revs = scmutil.revrange(repo, [revspec])
455 453
456 454 found = set()
457 455 result = []
458 456 for r in revs:
459 457 ctx = repo[r]
460 458 for f in getset(mctx.switch(ctx, _buildstatus(ctx, x)), x):
461 459 if f not in found:
462 460 found.add(f)
463 461 result.append(f)
464 462 return result
465 463
466 464 @predicate('status(base, rev, pattern)')
467 465 def status(mctx, x):
468 """``status(base, rev, revspec)``
469
470 Evaluate predicate using status change between ``base`` and
466 """Evaluate predicate using status change between ``base`` and
471 467 ``rev``. Examples:
472 468
473 469 - ``status(3, 7, added())`` - matches files added from "3" to "7"
474 470 """
475 471 repo = mctx.ctx.repo()
476 472 # i18n: "status" is a keyword
477 473 b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
478 474 # i18n: "status" is a keyword
479 475 baseerr = _("first argument to status must be a revision")
480 476 baserevspec = getstring(b, baseerr)
481 477 if not baserevspec:
482 478 raise error.ParseError(baseerr)
483 479 reverr = _("second argument to status must be a revision")
484 480 revspec = getstring(r, reverr)
485 481 if not revspec:
486 482 raise error.ParseError(reverr)
487 483 basenode, node = scmutil.revpair(repo, [baserevspec, revspec])
488 484 basectx = repo[basenode]
489 485 ctx = repo[node]
490 486 return getset(mctx.switch(ctx, _buildstatus(ctx, x, basectx=basectx)), x)
491 487
492 488 @predicate('subrepo([pattern])')
493 489 def subrepo(mctx, x):
494 490 """Subrepositories whose paths match the given pattern.
495 491 """
496 492 # i18n: "subrepo" is a keyword
497 493 getargs(x, 0, 1, _("subrepo takes at most one argument"))
498 494 ctx = mctx.ctx
499 495 sstate = sorted(ctx.substate)
500 496 if x:
501 497 # i18n: "subrepo" is a keyword
502 498 pat = getstring(x, _("subrepo requires a pattern or no arguments"))
503 499
504 500 from . import match as matchmod # avoid circular import issues
505 501 fast = not matchmod.patkind(pat)
506 502 if fast:
507 503 def m(s):
508 504 return (s == pat)
509 505 else:
510 506 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
511 507 return [sub for sub in sstate if m(sub)]
512 508 else:
513 509 return [sub for sub in sstate]
514 510
515 511 methods = {
516 512 'string': stringset,
517 513 'symbol': stringset,
518 514 'and': andset,
519 515 'or': orset,
520 516 'minus': minusset,
521 517 'list': listset,
522 518 'group': getset,
523 519 'not': notset,
524 520 'func': func,
525 521 }
526 522
527 523 class matchctx(object):
528 524 def __init__(self, ctx, subset, status=None):
529 525 self.ctx = ctx
530 526 self.subset = subset
531 527 self._status = status
532 528 self._existingenabled = False
533 529 def status(self):
534 530 return self._status
535 531 def matcher(self, patterns):
536 532 return self.ctx.match(patterns)
537 533 def filter(self, files):
538 534 return [f for f in files if f in self.subset]
539 535 def existing(self):
540 536 assert self._existingenabled, 'unexpected existing() invocation'
541 537 if self._status is not None:
542 538 removed = set(self._status[3])
543 539 unknown = set(self._status[4] + self._status[5])
544 540 else:
545 541 removed = set()
546 542 unknown = set()
547 543 return (f for f in self.subset
548 544 if (f in self.ctx and f not in removed) or f in unknown)
549 545 def narrow(self, files):
550 546 return matchctx(self.ctx, self.filter(files), self._status)
551 547 def switch(self, ctx, status=None):
552 548 subset = self.filter(_buildsubset(ctx, status))
553 549 return matchctx(ctx, subset, status)
554 550
555 551 class fullmatchctx(matchctx):
556 552 """A match context where any files in any revisions should be valid"""
557 553
558 554 def __init__(self, ctx, status=None):
559 555 subset = _buildsubset(ctx, status)
560 556 super(fullmatchctx, self).__init__(ctx, subset, status)
561 557 def switch(self, ctx, status=None):
562 558 return fullmatchctx(ctx, status)
563 559
564 560 # filesets using matchctx.switch()
565 561 _switchcallers = [
566 562 'revs',
567 563 'status',
568 564 ]
569 565
570 566 def _intree(funcs, tree):
571 567 if isinstance(tree, tuple):
572 568 if tree[0] == 'func' and tree[1][0] == 'symbol':
573 569 if tree[1][1] in funcs:
574 570 return True
575 571 if tree[1][1] in _switchcallers:
576 572 # arguments won't be evaluated in the current context
577 573 return False
578 574 for s in tree[1:]:
579 575 if _intree(funcs, s):
580 576 return True
581 577 return False
582 578
583 579 def _buildsubset(ctx, status):
584 580 if status:
585 581 subset = []
586 582 for c in status:
587 583 subset.extend(c)
588 584 return subset
589 585 else:
590 586 return list(ctx.walk(ctx.match([])))
591 587
592 588 def getfileset(ctx, expr):
593 589 tree = parse(expr)
594 590 return getset(fullmatchctx(ctx, _buildstatus(ctx, tree)), tree)
595 591
596 592 def _buildstatus(ctx, tree, basectx=None):
597 593 # do we need status info?
598 594
599 595 # temporaty boolean to simplify the next conditional
600 596 purewdir = ctx.rev() is None and basectx is None
601 597
602 598 if (_intree(_statuscallers, tree) or
603 599 # Using matchctx.existing() on a workingctx requires us to check
604 600 # for deleted files.
605 601 (purewdir and _intree(_existingcallers, tree))):
606 602 unknown = _intree(['unknown'], tree)
607 603 ignored = _intree(['ignored'], tree)
608 604
609 605 r = ctx.repo()
610 606 if basectx is None:
611 607 basectx = ctx.p1()
612 608 return r.status(basectx, ctx,
613 609 unknown=unknown, ignored=ignored, clean=True)
614 610 else:
615 611 return None
616 612
617 613 def prettyformat(tree):
618 614 return parser.prettyformat(tree, ('string', 'symbol'))
619 615
620 616 def loadpredicate(ui, extname, registrarobj):
621 617 """Load fileset predicates from specified registrarobj
622 618 """
623 619 for name, func in registrarobj._table.iteritems():
624 620 symbols[name] = func
625 621 if func._callstatus:
626 622 _statuscallers.add(name)
627 623 if func._callexisting:
628 624 _existingcallers.add(name)
629 625
630 626 # load built-in predicates explicitly to setup _statuscallers/_existingcallers
631 627 loadpredicate(None, None, predicate)
632 628
633 629 # tell hggettext to extract docstrings from these functions:
634 630 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now