Package svnmailer :: Module util
[hide private]

Source Code for Module svnmailer.util

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable-msg = W0613, W0622, W0704 
  3  # 
  4  # Copyright 2004-2006 André Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  Utilities 
 19  ========= 
 20   
 21  This module contains some utility functions and classes used in several 
 22  places of the svnmailer. These functions have a quite general character 
 23  and can be used easily outside of the svnmailer as well. 
 24  """ 
 25  __author__    = "André Malo" 
 26  __docformat__ = "epytext en" 
 27  __all__       = [ 
 28      'TempFile', 
 29      'getPipe4', 
 30      'getSuitableCommandLine', 
 31      'splitCommand', 
 32      'filename', 
 33      'extractX509User', 
 34      'substitute', 
 35      'filterForXml', 
 36      'getParentDirList', 
 37      'getGlobValue', 
 38      'parseQuery', 
 39      'modifyQuery', 
 40      'inherit', 
 41      'commonPaths', 
 42      'ReadOnlyDict', 
 43      'SafeDict', 
 44  ] 
 45   
 46  # global imports 
 47  import locale, os, sys 
 48   
 49   
50 -class TempFile(object):
51 """ Tempfile container class 52 53 The class contains a destructor that removes the created 54 file. This differs from the stuff in tempfile, which removes 55 the file, when it's closed. 56 57 The mode is fixed to C{w+}; a C{b} is added if the C{text} 58 argument is false (see C{__init__}) 59 60 @cvar name: C{None} 61 @ivar name: The full name of the file 62 @type name: C{str} 63 64 @cvar fp: C{None} 65 @ivar fp: The file descriptor 66 @type fp: file like object 67 68 @cvar _unlink: C{None} 69 @ivar _unlink: C{os.unlink} 70 @type _unlink: callable 71 """ 72 name = None 73 fp = None 74 _unlink = None 75
76 - def __init__(self, tempdir = None, text = False):
77 """ Initialization 78 79 @param tempdir: The temporary directory 80 @type tempdir: C{str} 81 82 @param text: want to write text? 83 @type text: C{bool} 84 """ 85 import tempfile 86 87 self._unlink = os.unlink # make sure, unlink is available in __del__ 88 89 fd, self.name = tempfile.mkstemp(dir = tempdir, text = text) 90 self.fp = os.fdopen(fd, "w+%s" % ["b", ""][bool(text)])
91 92
93 - def __del__(self):
94 """ Unlink the file name """ 95 if self.fp: 96 try: 97 self.fp.close() 98 except ValueError: 99 # ok 100 pass 101 102 if self.name and self._unlink: 103 try: 104 self._unlink(self.name) 105 except OSError: 106 # don't even ignore 107 pass
108 109
110 - def close(self):
111 """ Close the file (but don't delete it) 112 113 @exception ValueError: The file was already closed 114 """ 115 if self.fp: 116 self.fp.close()
117 118
119 -def getPipe2(command):
120 """ Returns a pipe object (C{Popen3} or C{_DummyPopen3} on win32) 121 122 @param command: The command list (the first item is the command 123 itself, the rest represents the arguments) 124 @type command: C{list} 125 126 @return: The pipe object 127 @rtype: C{popen2.Popen3} or C{_DummyPopen3} 128 """ 129 import popen2 130 131 try: 132 cls = popen2.Popen3 133 except AttributeError: 134 cls = _DummyPopen3 135 136 return cls(getSuitableCommandLine(command))
137 138
139 -def getPipe4(command):
140 """ Returns a pipe object (C{Popen4} or C{_DummyPopen4} on win32) 141 142 @param command: The command list (the first item is the command 143 itself, the rest represents the arguments) 144 @type command: C{list} 145 146 @return: The pipe object 147 @rtype: C{popen2.Popen4} or C{_DummyPopen4} 148 """ 149 import popen2 150 151 try: 152 cls = popen2.Popen4 153 except AttributeError: 154 cls = _DummyPopen4 155 156 return cls(getSuitableCommandLine(command))
157 158
159 -class _DummyPopen4(object):
160 """ Dummy Popen4 class for platforms which don't provide one in popen2 """ 161
162 - def __init__(self, cmd, bufsize = -1):
163 """ Initialization """ 164 bufsize = -1 # otherwise error on win32 165 self.tochild, self.fromchild = os.popen4(cmd, 'b', bufsize)
166 167
168 - def wait(self):
169 """ Dummy wait """ 170 return 0
171 172
173 -class _DummyPopen3(object):
174 """ Dummy Popen3 class for platforms which don't provide one in popen2 """ 175
176 - def __init__(self, cmd, capturestderr = False, bufsize = -1):
177 """ Initialization """ 178 bufsize = -1 # otherwise error on win32 179 capturestderr = False # we don't do this on win32 180 self.tochild, self.fromchild = os.popen2(cmd, 'b', bufsize) 181 self.childerr = None
182 183
184 - def wait(self):
185 """ Dummy wait """ 186 return 0
187 188
189 -def getSuitableCommandLine(command, _platform = None):
190 """ Return the revised command suitable for being exec'd 191 192 Currently this means, it's escaped and converted to a string 193 only for Win32, because on this system the shell is called. 194 For other systems the list is just returned. 195 196 @note: This is more or less the same as the stuff in 197 svn.fs._escape_msvcrt_shell_command/arg. But it 198 belongs somewhere else - e.g. into a util module... 199 200 Perhaps once a day the whole package goes directly 201 into the subversion distribution and then it's all 202 cool. 203 204 @param command: The command to escape 205 @type command: C{list} 206 207 @param _platform: A platform string (for testing purposes only) 208 @type _platform: C{str} 209 210 @return: The escaped command string or the original list 211 @rtype: C{str} or C{list} 212 """ 213 platform = _platform or sys.platform 214 if platform != "win32": 215 return command 216 217 try: 218 slashre = getSuitableCommandLine._slashre 219 except AttributeError: 220 import re 221 slashre = getSuitableCommandLine._slashre = re.compile(r'(\\+)("|$)') 222 223 # What we do here is: 224 # (1) double up slashes, but only before quotes or the string end 225 # (since we surround it by quotes afterwards) 226 # (2) Escape " as "^"" 227 # This means "string end", "Escaped quote", "string begin" in that 228 # order 229 # (See also http://www.microsoft.com/technet/archive/winntas 230 # /deploy/prodspecs/shellscr.mspx) 231 232 # Original comments from the svn.fs functions: 233 # ============================================ 234 # According cmd's usage notes (cmd /?), it parses the command line by 235 # "seeing if the first character is a quote character and if so, stripping 236 # the leading character and removing the last quote character." 237 # So to prevent the argument string from being changed we add an extra set 238 # of quotes around it here. 239 240 # The (very strange) parsing rules used by the C runtime library are 241 # described at: 242 # http://msdn.microsoft.com/library/en-us/vclang/html 243 # /_pluslang_Parsing_C.2b2b_.Command.2d.Line_Arguments.asp 244 245 return '"%s"' % " ".join([ 246 '"%s"' % slashre.sub(r'\1\1\2', arg).replace('"', '"^""') 247 for arg in command 248 ])
249 250
251 -def splitCommand(command):
252 r"""Split a command string with respect to quotes and such 253 254 The command string consists of several tokens: 255 - whitespace: Those are separators except inside quoted items 256 - unquoted items: every token that doesn't start with 257 a double quote (") 258 - quoted items: every token that starts with a double quote ("). 259 Those items must be closed with a double quote and may contain 260 whitespaces. The enclosing quotes are stripped. To put a double 261 quote character inside such a token, it has to be escaped with 262 a backslash (\). Therefore - backslashes themselves have to be 263 escaped as well. The escapes are also stripped from the result. 264 265 Here's an example: C{r'foo bar "baz" "zo\"" "\\nk"'} resolves 266 to C{['foo', 'bar', 'baz', 'zo"', r'\nk']} 267 268 @param command: The command string 269 @type command: C{str} 270 271 @return: The splitted command 272 @rtype: C{list} 273 274 @exception ValueError: The command string is not valid 275 (unclosed quote or the like) 276 """ 277 try: 278 argre, checkre, subre = splitCommand._regexps 279 except AttributeError: 280 import re 281 argre = r'[^"\s]\S*|"[^\\"]*(?:\\[\\"][^\\"]*)*"' 282 checkre = r'\s*(?:%(arg)s)(?:\s+(?:%(arg)s))*\s*$' % {'arg': argre} 283 subre = r'\\([\\"])' 284 285 argre, checkre, subre = splitCommand._regexps = ( 286 re.compile(argre), re.compile(checkre), re.compile(subre) 287 ) 288 289 if not checkre.match(command or ''): 290 raise ValueError("Command string %r is not valid" % command) 291 292 return [ 293 (arg.startswith('"') and [subre.sub(r'\1', arg[1:-1])] or [arg])[0] 294 for arg in argre.findall(command or '') 295 ]
296 297
298 -class _LocaleFile(object):
299 """ Transform filenames according to locale """
300 - def __init__(self, _locale = locale, _os = os, _sys = sys):
301 """ Initialization """ 302 self.unicode_system = _os.path.supports_unicode_filenames 303 self.from_enc = _locale.getpreferredencoding(False) or "us-ascii" 304 self.to_enc = _sys.getfilesystemencoding() or "us-ascii"
305 306
307 - def toLocale(self, name, name_enc = None, locale_enc = None):
308 """ Transforms a file name to the locale representation 309 310 @param name: The name to consider 311 @type name: C{str} / C{unicode} 312 313 @param name_enc: The source encoding of C{name}, if it's 314 not unicode already 315 @type name_enc: C{str} 316 317 @param locale_enc: The file system encoding (used only 318 if it's not a unicode supporting OS) 319 @type locale_enc: C{str} 320 321 @return: The name in locale representation 322 @rtype: C{str}/C{unicode} 323 324 @exception UnicodeError: An error happened while recoding 325 """ 326 if locale_enc is None: 327 locale_enc = self.to_enc 328 if name_enc is None: 329 name_enc = self.from_enc 330 331 if self.unicode_system: 332 if isinstance(name, unicode): 333 return name 334 else: 335 return name.decode(name_enc, "strict") 336 337 if locale_enc.lower() == "none": 338 if isinstance(name, unicode): 339 raise RuntimeError("Illegal call") 340 else: 341 return name 342 343 if not isinstance(name, unicode): 344 name = name.decode(name_enc, "strict") 345 346 return name.encode(locale_enc, "strict")
347 348
349 - def fromLocale(self, name, locale_enc = None):
350 """ Transform a file name from locale repr to unicode (hopefully) 351 352 @param name: The name to decode 353 @type name: C{str}/C{unicode} 354 355 @param locale_enc: The locale encoding 356 @type locale_enc: C{str} 357 358 @return: The decoded name 359 @rtype: C{unicode}/C{str} 360 361 @exception UnicodeError: An error happend while recoding 362 """ 363 if isinstance(name, unicode): 364 return name 365 366 if locale_enc is None: 367 locale_enc = self.from_enc 368 369 if locale_enc.lower() == "none": 370 return name # no unicode. 371 372 return name.decode(locale_enc, "strict")
373 374 filename = _LocaleFile() 375 376
377 -def extractX509User(author):
378 """ Returns user data extracted from x509 subject string 379 380 @param author: The author string 381 @type author: C{str} 382 383 @return: user name, mail address (user name maybe C{None}) 384 @rtype: C{tuple} or C{None} 385 """ 386 if author: 387 try: 388 cnre, eare = extractX509User._regexps 389 except AttributeError: 390 import re 391 cnre = re.compile(ur'/CN=([^/]+)', re.I) 392 eare = re.compile(ur'/emailAddress=([^/]+)', re.I) 393 extractX509User._regexps = (cnre, eare) 394 395 author = author.decode('utf-8', 'replace') 396 ea_match = eare.search(author) 397 if ea_match: 398 cn_match = cnre.search(author) 399 return (cn_match and cn_match.group(1), ea_match.group(1)) 400 401 return None
402 403
404 -def substitute(template, subst):
405 """ Returns a filled template 406 407 If the L{template} is C{None}, this function returns C{None} 408 as well. 409 410 @param template: The temlate to fill 411 @type template: C{unicode} 412 413 @param subst: The substitution parameters 414 @type subst: C{dict} 415 416 @return: The filled template (The return type depends on the 417 template and the parameters) 418 @rtype: C{str} or C{unicode} 419 """ 420 if template is None: 421 return None 422 423 return template % SafeDict(subst.items())
424 425
426 -def filterForXml(value):
427 """ Replaces control characters with replace characters 428 429 @param value: The value to filter 430 @type value: C{unicode} 431 432 @return: The filtered value 433 @rtype: C{unicode} 434 """ 435 try: 436 regex = filterForXml._regex 437 except AttributeError: 438 import re 439 chars = u''.join([chr(num) for num in range(32) 440 if num not in (9, 10, 13) # XML 1.0 441 ]) 442 regex = filterForXml._regex = re.compile("[%s]" % chars) 443 444 return regex.sub(u'\ufffd', value)
445 446
447 -def getParentDirList(path):
448 """ Returns the directories up to a (posix) path 449 450 @param path: The path to process 451 @type path: C{str} 452 453 @return: The directory list 454 @rtype: C{list} 455 """ 456 import posixpath 457 458 path = posixpath.normpath("/%s" % path) 459 if path[:2] == '//': 460 path = path[1:] 461 462 dirs = [] 463 path = posixpath.dirname(path) 464 while path != '/': 465 dirs.append(path) 466 path = posixpath.dirname(path) 467 dirs.append('/') 468 469 return dirs
470 471
472 -def getGlobValue(globs, path):
473 """ Returns the value of the glob, where path matches 474 475 @param globs: The glob list (C{[(glob, associated value)]}) 476 @type globs: C{list} of C{tuple} 477 478 @param path: The path to match 479 @type path: C{str} 480 481 @return: The matched value or C{None} 482 @rtype: any 483 """ 484 import fnmatch 485 486 result = None 487 for glob in globs: 488 if fnmatch.fnmatchcase(path, glob[0]): 489 result = glob[1] 490 break 491 492 return result
493 494
495 -def modifyQuery(query, rem = None, add = None, set = None, delim = '&'):
496 """ Returns a modified query string 497 498 @note: set is a convenience parameter, it's actually a combination of 499 C{rem} and C{add}. The order of processing is: 500 1. append the set parameters to C{rem} and C{add} 501 2. apply C{rem} 502 3. apply C{add} 503 504 @warning: query parameters containing no C{=} character are silently 505 dropped. 506 507 @param query: The query string to modify 508 @type query: C{str} or C{dict} 509 510 @param rem: parameters to remove (if present) 511 @type rem: C{list} of C{str} 512 513 @param add: parameters to add 514 @type add: C{list} of C{tuple} 515 516 @param set: parameters to override 517 @type set: C{list} of C{tuple} 518 519 @param delim: Delimiter to use when rebuilding the query string 520 @type delim: C{str} 521 """ 522 rem = list(rem or []) 523 add = list(add or []) 524 set = list(set or []) 525 526 # parse query string 527 query_dict = (isinstance(query, dict) and 528 [query.copy()] or [parseQuery(query)] 529 )[0] 530 531 # append set list to rem and add 532 rem.extend([tup[0] for tup in set]) 533 add.extend(set) 534 535 # apply rem 536 for key in rem: 537 try: 538 del query_dict[key] 539 except KeyError: 540 # don't even ignore 541 pass 542 543 # apply add 544 for key, val in add: 545 query_dict.setdefault(key, []).append(val) 546 547 # rebuild query and return 548 return delim.join([ 549 delim.join(["%s=%s" % (key, str(val)) for val in vals]) 550 for key, vals in query_dict.items() 551 ])
552 553
554 -def parseQuery(query):
555 """ Parses a query string 556 557 @warning: query parameters containing no C{=} character are silently 558 dropped. 559 560 @param query: The query string to parse 561 @type query: C{str} 562 563 @return: The parsed query (C{{key: [values]}}) 564 @rtype: C{dict} 565 """ 566 try: 567 queryre = parseQuery._regex 568 except AttributeError: 569 import re 570 parseQuery._regex = queryre = re.compile(r'[&;]') 571 572 query_dict = {} 573 for key, val in [pair.split('=', 1) 574 for pair in queryre.split(query) if '=' in pair]: 575 query_dict.setdefault(key, []).append(val) 576 577 return query_dict
578 579
580 -def commonPaths(paths):
581 """ Returns the common component and the stripped paths 582 583 It expects that directories do always end with a trailing slash and 584 paths never begin with a slash (except root). 585 586 @param paths: The list of paths (C{[str, str, ...]}) 587 @type paths: C{list} 588 589 @return: The common component (always a directory) and the stripped 590 paths (C{(str, [str, str, ...])}) 591 @rtype: C{tuple} 592 """ 593 import posixpath 594 595 common = '' 596 if len(paths) > 1 and "/" not in paths: 597 common = posixpath.commonprefix(paths) 598 if common[-1:] != "/": 599 common = common[:common.rfind("/") + 1] 600 601 idx = len(common) 602 if idx > 0: 603 paths = [path[idx:] or "./" for path in paths] 604 common = common[:-1] # chop the trailing slash 605 606 return (common, paths)
607 608
609 -def inherit(cls, *bases):
610 """ Inherits class cls from *bases 611 612 @note: cls needs a __dict__, so __slots__ is tabu 613 614 @param cls: The class to inherit from *bases 615 @type cls: C{class} 616 617 @param bases: The base class(es) 618 @type bases: C{list} 619 """ 620 newdict = dict([(key, value) 621 for key, value in cls.__dict__.items() 622 if key != '__module__' 623 ]) 624 cls = type(cls.__name__, tuple(bases), newdict) 625 setattr(cls, "_%s__decorator_class" % cls.__name__, cls) 626 627 return cls
628 629
630 -def parseContentType(value):
631 """ Parses a content type 632 633 (the email module unfortunately doesn't provide a public 634 interface for this) 635 636 @warning: comments are not recognized yet 637 638 @param value: The value to parse - must be ascii compatible 639 @type value: C{basestring} 640 641 @return: The parsed header (C{(value, {key, [value, value, ...]})}) 642 or C{None} 643 @rtype: C{tuple} 644 """ 645 try: 646 if isinstance(value, unicode): 647 value.encode('us-ascii') 648 else: 649 value.decode('us-ascii') 650 except (AttributeError, UnicodeError): 651 return None 652 653 try: 654 typere, pairre, stripre = parseContentType._regexps 655 except AttributeError: 656 import re 657 # a bit more lenient than RFC 2045 658 tokenres = r'[^\000-\040()<>@,;:\\"/[\]?=]+' 659 qcontent = r'[^\000\\"]' 660 qsres = r'"%(qc)s*(?:\\"%(qc)s*)*"' % {'qc': qcontent} 661 valueres = r'(?:%(token)s|%(quoted-string)s)' % { 662 'token': tokenres, 'quoted-string': qsres, 663 } 664 665 typere = re.compile( 666 r'\s*([^;/\s]+/[^;/\s]+)((?:\s*;\s*%(key)s\s*=\s*%(val)s)*)\s*$' % 667 {'key': tokenres, 'val': valueres,} 668 ) 669 pairre = re.compile(r'\s*;\s*(%(key)s)\s*=\s*(%(val)s)' % { 670 'key': tokenres, 'val': valueres 671 }) 672 stripre = re.compile(r'\r?\n') 673 parseContentType._regexps = (typere, pairre, stripre) 674 675 match = typere.match(value) 676 if not match: 677 return None 678 679 parsed = (match.group(1).lower(), {}) 680 match = match.group(2) 681 if match: 682 for key, val in pairre.findall(match): 683 if val[:1] == '"': 684 val = stripre.sub(r'', val[1:-1]).replace(r'\"', '"') 685 parsed[1].setdefault(key.lower(), []).append(val) 686 687 return parsed
688 689
690 -class ReadOnlyDict(dict):
691 """ Read only dictionary """ 692 __msg = "The dictionary is read-only" 693
694 - def __setitem__(self, key, value):
695 """ modifiying is not allowed """ 696 raise TypeError(self.__msg)
697 698
699 - def __delitem__(self, key):
700 """ deleting is not allowed """ 701 raise TypeError(self.__msg)
702 703
704 - def clear(self):
705 """ clearing is not allowed """ 706 raise TypeError(self.__msg)
707 708
709 - def fromkeys(cls, seq, value = None):
710 """ Chokes by default, so work around it """ 711 return cls(dict.fromkeys(seq, value))
712 fromkeys = classmethod(fromkeys) 713 714
715 - def pop(self, key, default = None):
716 """ popping is not allowed """ 717 raise TypeError(self.__msg)
718 719
720 - def popitem(self):
721 """ popping is not allowed """ 722 raise TypeError(self.__msg)
723 724
725 - def setdefault(self, default = None):
726 """ modifying is not allowed """ 727 raise TypeError(self.__msg)
728 729
730 - def update(self, newdict):
731 """ updating is not allowed """ 732 raise TypeError(self.__msg)
733 734
735 -class SafeDict(dict):
736 """ A dict, which returns '' on unknown keys or false values """ 737
738 - def __getitem__(self, key):
739 """ Returns an empty string on false values or unknown keys """ 740 return dict.get(self, key) or ''
741