Package trac :: Package util

Source Code for Package trac.util

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2003-2009 Edgewall Software 
  4  # Copyright (C) 2003-2006 Jonas Borgström <jonas@edgewall.com> 
  5  # Copyright (C) 2006 Matthew Good <trac@matt-good.net> 
  6  # Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr> 
  7  # All rights reserved. 
  8  # 
  9  # This software is licensed as described in the file COPYING, which 
 10  # you should have received as part of this distribution. The terms 
 11  # are also available at http://trac.edgewall.org/wiki/TracLicense. 
 12  # 
 13  # This software consists of voluntary contributions made by many 
 14  # individuals. For the exact contribution history, see the revision 
 15  # history and logs, available at http://trac.edgewall.org/log/. 
 16  # 
 17  # Author: Jonas Borgström <jonas@edgewall.com> 
 18  #         Matthew Good <trac@matt-good.net> 
 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  # Imports for backward compatibility 
 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  # -- req/session utils 
 44   
45 -def get_reporter_id(req, arg_name=None):
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 # == 'anonymous'
57 58 if os.name == 'nt': 59 from getpass import getuser 60 else: 61 import pwd
62 - def getuser():
63 try: 64 return pwd.getpwuid(os.geteuid())[0] 65 except KeyError: 66 return 'unknown'
67 68 # -- algorithmic utilities 69 70 DIGITS = re.compile(r'(\d+)')
71 -def embedded_numbers(s):
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 # -- os utilities 80 81 try: 82 WindowsError = WindowsError 83 except NameError:
84 - class WindowsError(OSError):
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
99 - def _rename(src, dst):
100 if not isinstance(src, unicode): 101 src = unicode(src, sys.getfilesystemencoding()) 102 if not isinstance(dst, unicode): 103 dst = unicode(dst, sys.getfilesystemencoding()) 104 if _rename_atomic(src, dst): 105 return True 106 return MoveFileEx(src, dst, MOVEFILE_REPLACE_EXISTING 107 | MOVEFILE_WRITE_THROUGH)
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
115 - def _rename_atomic(src, dst):
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
129 - def rename(src, dst):
130 # Try atomic or pseudo-atomic rename 131 if _rename(src, dst): 132 return 133 # Fall back to "move away and replace" 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
151 -class AtomicFile(object):
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 # Try to preserve permissions and group ownership, but failure 167 # should not be fatal 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
179 - def __getattr__(self, name):
180 return getattr(self._file, name)
181
182 - def commit(self):
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
193 - def rollback(self):
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
209 -def read_file(path, mode='r'):
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
218 -def create_file(path, data='', mode='w'):
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
228 -def create_unique_file(path):
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 # A sanity check 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
248 -class NaivePopen:
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 # -- sys utils 290
291 -def arity(f):
292 return f.func_code.co_argcount
293
294 -def get_last_traceback():
295 import traceback 296 from StringIO import StringIO 297 tb = StringIO() 298 traceback.print_exc(file=tb) 299 return to_unicode(tb.getvalue())
300
301 -def get_lines_from_file(filename, lineno, context=0):
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
334 -def safe__import__(module_name):
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 # -- setuptools utils 353
354 -def get_module_path(module):
355 # Determine the plugin that this component belongs to 356 path = module.__file__ 357 module_name = module.__name__ 358 if path.endswith('.pyc') or path.endswith('.pyo'): 359 path = path[:-1] 360 if os.path.basename(path) == '__init__.py': 361 path = os.path.dirname(path) 362 base_path = os.path.splitext(path)[0] 363 while base_path.replace(os.sep, '.').endswith(module_name): 364 base_path = os.path.dirname(base_path) 365 module_name = '.'.join(module_name.split('.')[:-1]) 366 if not module_name: 367 break 368 return base_path
369
370 -def get_pkginfo(dist):
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 # -- crypto utils 415
416 -def hex_entropy(bytes=32):
417 import random 418 return sha1(str(random.random())).hexdigest()[:bytes]
419 420 421 # Original license for md5crypt: 422 # Based on FreeBSD src/lib/libcrypt/crypt.c 1.2 423 # 424 # "THE BEER-WARE LICENSE" (Revision 42): 425 # <phk@login.dknet.dk> wrote this file. As long as you retain this notice you 426 # can do whatever you want with this stuff. If we meet some day, and you think 427 # this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
428 -def md5crypt(password, salt, magic='$1$'):
429 # /* The password first, since that is what is most unknown */ 430 # /* Then our magic string */ 431 # /* Then the raw salt */ 432 m = md5(password + magic + salt) 433 434 # /* Then just as many characters of the MD5(pw,salt,pw) */ 435 mixin = md5(password + salt + password).digest() 436 for i in range(0, len(password)): 437 m.update(mixin[i % 16]) 438 439 # /* Then something really weird... */ 440 # Also really broken, as far as I can tell. -m 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 # /* and now, just to make sure things don't run too fast */ 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 # This is the bit that uses to64() in the original code. 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 # -- misc. utils 490
491 -class Ranges(object):
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
566 - def appendrange(self, r):
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
582 - def _reduce(self):
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]: # this item overlaps with the next 589 # make the first include the second 590 p[i] = (p[i][0], max(p[i][1], p[i+1][1])) 591 del p[i+1] # delete the second, after adjusting my endpoint 592 else: 593 i += 1 594 if p: 595 self.a = p[0][0] # min value 596 self.b = p[-1][1] # max value 597 else: 598 self.a = self.b = None
599
600 - def __iter__(self):
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
611 - def __contains__(self, x):
612 """ 613 >>> 55 in Ranges() 614 False 615 """ 616 # short-circuit if outside the possible range 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: # short-circuit if we've gone too far 622 break 623 return False
624
625 - def __str__(self):
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
641 - def __len__(self):
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
652 - def truncate(self, max):
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
683 -def to_ranges(revs):
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
718 -def pairwise(iterable):
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
731 -def partition(iterable, order=None):
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