1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import errno
21 import locale
22 import os.path
23 import random
24 import re
25 import sys
26 import time
27 import tempfile
28 from urllib import quote, unquote, urlencode
29 from itertools import izip
30
31
32 from trac.core import TracError
33 from trac.util.compat import md5, reversed, sha1, sorted, tee
34 from trac.util.html import escape, unescape, Markup, Deuglifier
35 from trac.util.text import CRLF, to_utf8, to_unicode, shorten_line, \
36 wrap, pretty_size
37 from trac.util.datefmt import pretty_timedelta, format_datetime, \
38 format_date, format_time, \
39 get_date_format_hint, \
40 get_datetime_format_hint, http_date, \
41 parse_date
42
43
44
46 if req.authname != 'anonymous':
47 return req.authname
48 if arg_name:
49 r = req.args.get(arg_name)
50 if r:
51 return r
52 name = req.session.get('name', None)
53 email = req.session.get('email', None)
54 if name and email:
55 return '%s <%s>' % (name, email)
56 return name or email or req.authname
57
58 if os.name == 'nt':
59 from getpass import getuser
60 else:
61 import pwd
63 try:
64 return pwd.getpwuid(os.geteuid())[0]
65 except KeyError:
66 return 'unknown'
67
68
69
70 DIGITS = re.compile(r'(\d+)')
72 """Comparison function for natural order sorting based on
73 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/214202."""
74 pieces = DIGITS.split(s)
75 pieces[1::2] = map(int, pieces[1::2])
76 return pieces
77
78
79
80
81 try:
82 WindowsError = WindowsError
83 except NameError:
85 """Dummy exception replacing WindowsError on non-Windows platforms"""
86
87
88 can_rename_open_file = False
89 if os.name == 'nt':
90 _rename = lambda src, dst: False
91 _rename_atomic = lambda src, dst: False
92
93 try:
94 import ctypes
95 MOVEFILE_REPLACE_EXISTING = 0x1
96 MOVEFILE_WRITE_THROUGH = 0x8
97 MoveFileEx = ctypes.windll.kernel32.MoveFileExW
98
108
109 CreateTransaction = ctypes.windll.ktmw32.CreateTransaction
110 CommitTransaction = ctypes.windll.ktmw32.CommitTransaction
111 MoveFileTransacted = ctypes.windll.kernel32.MoveFileTransactedW
112 CloseHandle = ctypes.windll.kernel32.CloseHandle
113 can_rename_open_file = True
114
116 ta = CreateTransaction(None, 0, 0, 0, 0, 1000, 'Trac rename')
117 if ta == -1:
118 return False
119 try:
120 return (MoveFileTransacted(src, dst, None, None,
121 MOVEFILE_REPLACE_EXISTING
122 | MOVEFILE_WRITE_THROUGH, ta)
123 and CommitTransaction(ta))
124 finally:
125 CloseHandle(ta)
126 except Exception:
127 pass
128
130
131 if _rename(src, dst):
132 return
133
134 try:
135 os.rename(src, dst)
136 except OSError, e:
137 if e.errno != errno.EEXIST:
138 raise
139 old = "%s-%08x" % (dst, random.randint(0, sys.maxint))
140 os.rename(dst, old)
141 os.rename(src, dst)
142 try:
143 os.unlink(old)
144 except Exception:
145 pass
146 else:
147 rename = os.rename
148 can_rename_open_file = True
149
150
152 """A file that appears atomically with its full content.
153
154 This file-like object writes to a temporary file in the same directory
155 as the final file. If the file is committed, the temporary file is renamed
156 atomically (on Unix, at least) to its final name. If it is rolled back,
157 the temporary file is removed.
158 """
159 - def __init__(self, path, mode='w', bufsize=-1):
160 self._file = None
161 self._path = path
162 (dir, name) = os.path.split(path)
163 (fd, self._temp) = tempfile.mkstemp(prefix=name + '-', dir=dir)
164 self._file = os.fdopen(fd, mode, bufsize)
165
166
167
168 try:
169 st = os.stat(path)
170 if hasattr(os, 'chmod'):
171 os.chmod(self._temp, st.st_mode)
172 if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
173 os.chflags(self._temp, st.st_flags)
174 if hasattr(os, 'chown'):
175 os.chown(self._temp, -1, st.st_gid)
176 except OSError:
177 pass
178
180 return getattr(self._file, name)
181
183 if self._file is None:
184 return
185 try:
186 f, self._file = self._file, None
187 f.close()
188 rename(self._temp, self._path)
189 except Exception:
190 os.unlink(self._temp)
191 raise
192
194 if self._file is None:
195 return
196 try:
197 f, self._file = self._file, None
198 f.close()
199 finally:
200 try:
201 os.unlink(self._temp)
202 except:
203 pass
204
205 close = commit
206 __del__ = rollback
207
208
210 """Read a file and return its content."""
211 f = open(path, mode)
212 try:
213 return f.read()
214 finally:
215 f.close()
216
217
219 """Create a new file with the given data."""
220 f = open(path, mode)
221 try:
222 if data:
223 f.write(data)
224 finally:
225 f.close()
226
227
229 """Create a new file. An index is added if the path exists"""
230 parts = os.path.splitext(path)
231 idx = 1
232 while 1:
233 try:
234 flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
235 if hasattr(os, 'O_BINARY'):
236 flags += os.O_BINARY
237 return path, os.fdopen(os.open(path, flags, 0666), 'w')
238 except OSError, e:
239 if e.errno != errno.EEXIST:
240 raise
241 idx += 1
242
243 if idx > 100:
244 raise Exception('Failed to create unique name: ' + path)
245 path = '%s.%d%s' % (parts[0], idx, parts[1])
246
247
249 """This is a deadlock-safe version of popen that returns an object with
250 errorlevel, out (a string) and err (a string).
251
252 The optional `input`, which must be a `str` object, is first written
253 to a temporary file from which the process will read.
254
255 (`capturestderr` may not work under Windows 9x.)
256
257 Example: print Popen3('grep spam','\n\nhere spam\n\n').out
258 """
259 - def __init__(self, command, input=None, capturestderr=None):
260 outfile = tempfile.mktemp()
261 command = '( %s ) > %s' % (command, outfile)
262 if input is not None:
263 infile = tempfile.mktemp()
264 tmp = open(infile, 'w')
265 tmp.write(input)
266 tmp.close()
267 command = command + ' <' + infile
268 if capturestderr:
269 errfile = tempfile.mktemp()
270 command = command + ' 2>' + errfile
271 try:
272 self.err = None
273 self.errorlevel = os.system(command) >> 8
274 outfd = file(outfile, 'r')
275 self.out = outfd.read()
276 outfd.close()
277 if capturestderr:
278 errfd = file(errfile,'r')
279 self.err = errfd.read()
280 errfd.close()
281 finally:
282 if os.path.isfile(outfile):
283 os.remove(outfile)
284 if input and os.path.isfile(infile):
285 os.remove(infile)
286 if capturestderr and os.path.isfile(errfile):
287 os.remove(errfile)
288
289
290
292 return f.func_code.co_argcount
293
295 import traceback
296 from StringIO import StringIO
297 tb = StringIO()
298 traceback.print_exc(file=tb)
299 return to_unicode(tb.getvalue())
300
302 """Return `content` number of lines before and after the specified
303 `lineno` from the file identified by `filename`.
304
305 Returns a `(lines_before, line, lines_after)` tuple.
306 """
307 if os.path.isfile(filename):
308 fileobj = open(filename, 'U')
309 try:
310 lines = fileobj.readlines()
311 lbound = max(0, lineno - context)
312 ubound = lineno + 1 + context
313
314
315 charset = None
316 rep = re.compile('coding[=:]\s*([-\w.]+)')
317 for linestr in lines[0], lines[1]:
318 match = rep.search(linestr)
319 if match:
320 charset = match.group(1)
321 break
322
323 before = [to_unicode(l.rstrip('\n'), charset)
324 for l in lines[lbound:lineno]]
325 line = to_unicode(lines[lineno].rstrip('\n'), charset)
326 after = [to_unicode(l.rstrip('\n'), charset) \
327 for l in lines[lineno + 1:ubound]]
328
329 return before, line, after
330 finally:
331 fileobj.close()
332 return (), None, ()
333
335 """
336 Safe imports: rollback after a failed import.
337
338 Initially inspired from the RollbackImporter in PyUnit,
339 but it's now much simpler and works better for our needs.
340
341 See http://pyunit.sourceforge.net/notes/reloading.html
342 """
343 already_imported = sys.modules.copy()
344 try:
345 return __import__(module_name, globals(), locals(), [])
346 except Exception, e:
347 for modname in sys.modules.copy():
348 if not already_imported.has_key(modname):
349 del(sys.modules[modname])
350 raise e
351
352
353
369
371 """Get a dictionary containing package information for a package
372
373 `dist` can be either a Distribution instance or, as a shortcut,
374 directly the module instance, if one can safely infer a Distribution
375 instance from it.
376
377 Always returns a dictionary but it will be empty if no Distribution
378 instance can be created for the given module.
379 """
380 import types
381 if isinstance(dist, types.ModuleType):
382 try:
383 from pkg_resources import find_distributions
384 module = dist
385 module_path = get_module_path(module)
386 for dist in find_distributions(module_path, only=True):
387 if os.path.isfile(module_path) or \
388 dist.key == module.__name__.lower():
389 break
390 else:
391 return {}
392 except ImportError:
393 return {}
394 import email
395 attrs = ('author', 'author-email', 'license', 'home-page', 'summary',
396 'description', 'version')
397 info = {}
398 def normalize(attr):
399 return attr.lower().replace('-', '_')
400 try:
401 pkginfo = email.message_from_string(dist.get_metadata('PKG-INFO'))
402 for attr in [key for key in attrs if key in pkginfo]:
403 info[normalize(attr)] = pkginfo[attr]
404 except IOError, e:
405 err = 'Failed to read PKG-INFO file for %s: %s' % (dist, e)
406 for attr in attrs:
407 info[normalize(attr)] = err
408 except email.Errors.MessageError, e:
409 err = 'Failed to parse PKG-INFO file for %s: %s' % (dist, e)
410 for attr in attrs:
411 info[normalize(attr)] = err
412 return info
413
414
415
417 import random
418 return sha1(str(random.random())).hexdigest()[:bytes]
419
420
421
422
423
424
425
426
427
428 -def md5crypt(password, salt, magic='$1$'):
429
430
431
432 m = md5(password + magic + salt)
433
434
435 mixin = md5(password + salt + password).digest()
436 for i in range(0, len(password)):
437 m.update(mixin[i % 16])
438
439
440
441 i = len(password)
442 while i:
443 if i & 1:
444 m.update('\x00')
445 else:
446 m.update(password[0])
447 i >>= 1
448
449 final = m.digest()
450
451
452 for i in range(1000):
453 m2 = md5()
454 if i & 1:
455 m2.update(password)
456 else:
457 m2.update(final)
458
459 if i % 3:
460 m2.update(salt)
461
462 if i % 7:
463 m2.update(password)
464
465 if i & 1:
466 m2.update(final)
467 else:
468 m2.update(password)
469
470 final = m2.digest()
471
472
473
474 itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
475
476 rearranged = ''
477 for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
478 v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c])
479 for i in range(4):
480 rearranged += itoa64[v & 0x3f]; v >>= 6
481
482 v = ord(final[11])
483 for i in range(2):
484 rearranged += itoa64[v & 0x3f]; v >>= 6
485
486 return magic + salt + '$' + rearranged
487
488
489
490
492 """
493 Holds information about ranges parsed from a string
494
495 >>> x = Ranges("1,2,9-15")
496 >>> 1 in x
497 True
498 >>> 5 in x
499 False
500 >>> 10 in x
501 True
502 >>> 16 in x
503 False
504 >>> [i for i in range(20) if i in x]
505 [1, 2, 9, 10, 11, 12, 13, 14, 15]
506
507 Also supports iteration, which makes that last example a bit simpler:
508
509 >>> list(x)
510 [1, 2, 9, 10, 11, 12, 13, 14, 15]
511
512 Note that it automatically reduces the list and short-circuits when the
513 desired ranges are a relatively small portion of the entire set:
514
515 >>> x = Ranges("99")
516 >>> 1 in x # really fast
517 False
518 >>> x = Ranges("1, 2, 1-2, 2") # reduces this to 1-2
519 >>> x.pairs
520 [(1, 2)]
521 >>> x = Ranges("1-9,2-4") # handle ranges that completely overlap
522 >>> list(x)
523 [1, 2, 3, 4, 5, 6, 7, 8, 9]
524
525 The members 'a' and 'b' refer to the min and max value of the range, and
526 are None if the range is empty:
527
528 >>> x.a
529 1
530 >>> x.b
531 9
532 >>> e = Ranges()
533 >>> e.a, e.b
534 (None, None)
535
536 Empty ranges are ok, and ranges can be constructed in pieces, if you
537 so choose:
538
539 >>> x = Ranges()
540 >>> x.appendrange("1, 2, 3")
541 >>> x.appendrange("5-9")
542 >>> x.appendrange("2-3") # reduce'd away
543 >>> list(x)
544 [1, 2, 3, 5, 6, 7, 8, 9]
545
546 ''Code contributed by Tim Hatch''
547
548 Reversed ranges are ignored, unless the Ranges has the `reorder` property
549 set.
550
551 >>> str(Ranges("20-10"))
552 ''
553 >>> str(Ranges("20-10", reorder=True))
554 '10-20'
555
556 """
557
558 RE_STR = r"""\d+(?:[-:]\d+)?(?:,\d+(?:[-:]\d+)?)*"""
559
560 - def __init__(self, r=None, reorder=False):
561 self.pairs = []
562 self.a = self.b = None
563 self.reorder = reorder
564 self.appendrange(r)
565
567 """Add a range (from a string or None) to the current one"""
568 if not r:
569 return
570 p = self.pairs
571 for x in r.split(","):
572 try:
573 a, b = map(int, x.split('-', 1))
574 except ValueError:
575 a, b = int(x), int(x)
576 if b >= a:
577 p.append((a, b))
578 elif self.reorder:
579 p.append((b, a))
580 self._reduce()
581
583 """Come up with the minimal representation of the ranges"""
584 p = self.pairs
585 p.sort()
586 i = 0
587 while i + 1 < len(p):
588 if p[i+1][0]-1 <= p[i][1]:
589
590 p[i] = (p[i][0], max(p[i][1], p[i+1][1]))
591 del p[i+1]
592 else:
593 i += 1
594 if p:
595 self.a = p[0][0]
596 self.b = p[-1][1]
597 else:
598 self.a = self.b = None
599
601 """
602 This is another way I came up with to do it. Is it faster?
603
604 from itertools import chain
605 return chain(*[xrange(a, b+1) for a, b in self.pairs])
606 """
607 for a, b in self.pairs:
608 for i in range(a, b+1):
609 yield i
610
612 """
613 >>> 55 in Ranges()
614 False
615 """
616
617 if self.a is not None and self.a <= x <= self.b:
618 for a, b in self.pairs:
619 if a <= x <= b:
620 return True
621 if b > x:
622 break
623 return False
624
626 """Provide a compact string representation of the range.
627
628 >>> (str(Ranges("1,2,3,5")), str(Ranges()), str(Ranges('2')))
629 ('1-3,5', '', '2')
630 >>> str(Ranges('99-1')) # only nondecreasing ranges allowed
631 ''
632 """
633 r = []
634 for a, b in self.pairs:
635 if a == b:
636 r.append(str(a))
637 else:
638 r.append("%d-%d" % (a, b))
639 return ",".join(r)
640
642 """The length of the entire span, ignoring holes.
643
644 >>> (len(Ranges('99')), len(Ranges('1-2')), len(Ranges('')))
645 (1, 2, 0)
646 """
647 if self.a is not None and self.b is not None:
648 return self.b - self.a + 1
649 else:
650 return 0
651
653 """Truncate the Ranges by setting a maximal allowed value.
654
655 Note that this `max` can be a value in a gap, so the only guarantee
656 is that `self.b` will be lesser than or equal to `max`.
657
658 >>> r = Ranges("10-20,25-45")
659 >>> str(r.truncate(30))
660 '10-20,25-30'
661
662 >>> str(r.truncate(22))
663 '10-20'
664
665 >>> str(r.truncate(10))
666 '10'
667 """
668 r = Ranges()
669 r.a, r.b, r.reorder = self.a, self.b, self.reorder
670 r.pairs = []
671 for a, b in self.pairs:
672 if a <= max:
673 if b > max:
674 r.pairs.append((a, max))
675 r.b = max
676 break
677 r.pairs.append((a, b))
678 else:
679 break
680 return r
681
682
684 """Converts a list of revisions to a minimal set of ranges.
685
686 >>> to_ranges([2, 12, 3, 6, 9, 1, 5, 11])
687 '1-3,5-6,9,11-12'
688 >>> to_ranges([])
689 ''
690 """
691 ranges = []
692 begin = end = None
693 def store():
694 if end == begin:
695 ranges.append(str(begin))
696 else:
697 ranges.append('%d-%d' % (begin, end))
698 for rev in sorted(revs):
699 if begin is None:
700 begin = end = rev
701 elif rev == end + 1:
702 end = rev
703 else:
704 store()
705 begin = end = rev
706 if begin is not None:
707 store()
708 return ','.join(ranges)
709
710 -def content_disposition(type, filename=None):
711 """Generate a properly escaped Content-Disposition header"""
712 if filename is not None:
713 if isinstance(filename, unicode):
714 filename = filename.encode('utf-8')
715 type += '; filename=' + quote(filename, safe='')
716 return type
717
719 """s -> (s0,s1), (s1,s2), (s2, s3), ...
720
721 :deprecated: since 0.11 (if this really needs to be used, rewrite it
722 without izip)
723 """
724 a, b = tee(iterable)
725 try:
726 b.next()
727 except StopIteration:
728 pass
729 return izip(a, b)
730
732 result = {}
733 if order is not None:
734 for key in order:
735 result[key] = []
736 for item, category in iterable:
737 result.setdefault(category, []).append(item)
738 if order is None:
739 return result
740 return [result[key] for key in order]
741