Package svnmailer :: Package notifier :: Module _base
[hide private]

Source Code for Module svnmailer.notifier._base

  1  # -*- coding: utf-8 -*- 
  2  # pylint: disable-msg = W0613, 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  Base notifier class 
 19   
 20  @var EMPTY_TABLE: provides an empty translation table 
 21  @type EMPTY_TABLE: C{str} 
 22   
 23  @var CTRL_CHARS: provides a list of control chars (< ascii 32) 
 24  @type CTRL_CHARS: C{str} 
 25  """ 
 26  __author__    = "André Malo" 
 27  __docformat__ = "epytext en" 
 28  __all__       = ['BaseNotifier'] 
 29   
 30  # global imports 
 31  from svnmailer import util 
 32   
 33  import string 
 34  EMPTY_TABLE = string.maketrans('', '') 
 35  CTRL_CHARS = ''.join([ 
 36      chr(num) for num in range(32) 
 37      if chr(num) not in "\r\n\t\f" 
 38  ]) 
 39  del string 
 40   
 41   
42 -def addFunc(change):
43 """ diffable add """ 44 return change.wasAdded() and not change.wasCopied() and not \ 45 change.isDirectory()
46
47 -def delFunc(change):
48 """ diffable del """ 49 return change.wasDeleted() and not change.isDirectory()
50
51 -def copyFunc(change):
52 """ diffable copy """ 53 return change.wasCopied() and change.hasContentChanges()
54
55 -def modFunc(change):
56 """ diffable modify """ 57 return change.wasModified() and change.hasContentChanges()
58
59 -def propFunc(change):
60 """ diffable propchange """ 61 return change.hasPropertyChanges()
62
63 -def noneFunc(change):
64 """ not diffable """ 65 return False
66 67
68 -class BaseNotifier(object):
69 """ Base class for notifiers 70 71 Custom notifiers must implement this interface 72 (that is just the run method, however). 73 74 Additionally it contains some useful utility methods, 75 which can be used. 76 77 @ivar _settings: The settings to use 78 @type _settings: C{svnmailer.settings.Settings} 79 80 @ivar _groupset: The groupset to process 81 @type _groupset: C{list} 82 83 @ivar _penc_cache: The (path, rev) -> property encoding cache 84 @type _penc_cache: C{dict} 85 86 @cvar _diffable_tests: Map C{generate_diffs} list entrys to change 87 methods 88 @type _diffable_tests: C{tuple} 89 90 @cvar ADD: "add" token 91 @type ADD: C{unicode} 92 93 @cvar DELETE: "delete" token 94 @type DELETE: C{unicode} 95 96 @cvar COPY: "copy" token 97 @type COPY: C{unicode} 98 99 @cvar MODIFY: "modify" token 100 @type MODIFY: C{unicode} 101 102 @cvar PROPCHANGE: "propchange" token 103 @type PROPCHANGE: C{unicode} 104 105 @cvar NONE: "none" token 106 @type NONE: C{unicode} 107 108 @cvar ENC_CONFIG: magic value, meaning that the content encoding should 109 be retrieved from the config 110 @type ENC_CONFIG: C{str} 111 112 @cvar ENC_PROPERTY: The property name, where encodings could be stored 113 @type ENC_PROPERTY: C{str} 114 """ 115 ADD = u"add" 116 DELETE = u"delete" 117 COPY = u"copy" 118 MODIFY = u"modify" 119 PROPCHANGE = u"propchange" 120 NONE = u"none" 121 122 ENC_CONFIG = "retrieve encoding from config" 123 ENC_DEFAULT = "show default encoding" 124 ENC_PROPERTY = "svnmailer:content-charset" 125 126 _diffable_tests = ( 127 (ADD, addFunc), 128 (DELETE, delFunc), 129 (COPY, copyFunc), 130 (MODIFY, modFunc), 131 (PROPCHANGE, propFunc), 132 (NONE, noneFunc), 133 ) 134 135
136 - def __init__(self, settings, groupset):
137 """ Initialization """ 138 self._settings = settings 139 self._groupset = groupset 140 self._penc_cache = {}
141 142
143 - def run(self):
144 """ Runs the notifier """ 145 raise NotImplementedError()
146 147
148 - def getAuthor(self):
149 """ Returns the author of the revision 150 151 @return: The author or C{None} if there's no author 152 @rtype: C{str} 153 """ 154 author = self._settings.runtime.author 155 if not author: 156 author = self._settings.runtime._repos.getRevisionAuthor( 157 self._settings.runtime.revision 158 ) 159 160 return author
161 162
163 - def getTime(self):
164 """ Returns the time of the revision in seconds since epoch 165 166 @return: The time 167 @rtype: C{int} 168 """ 169 return self._settings.runtime._repos.getRevisionTime( 170 self._settings.runtime.revision 171 )
172 173
174 - def getLog(self):
175 """ Returns the log entry of the revision 176 177 @return: The log entry 178 @rtype: C{str} 179 """ 180 return self._settings.runtime._repos.getRevisionLog( 181 self._settings.runtime.revision 182 ) or ''
183 184
185 - def getUrl(self, config):
186 """ Returns the revision URL 187 188 @return: The URL or C{None} 189 @rtype: C{str} 190 """ 191 from svnmailer import browser 192 generator = browser.getBrowserUrlGenerator(config) 193 if generator: 194 return generator.getRevisionUrl(self._settings.runtime.revision) 195 196 return None
197 198
199 - def getDiffer(self, command = None):
200 """ Returns the initialized differ 201 202 @param command: The diff command to use (if any) 203 @type command: C{tuple} or C{None} 204 205 @return: The differ type 206 @rtype: C{svnmailer.differ.*} 207 """ 208 from svnmailer import differ 209 210 if command: 211 return differ.ExternalDiffer(command, self.getTempDir()) 212 else: 213 return differ.InternalDiffer()
214 215
216 - def getTempFile(self):
217 """ Returns an open temporary file container object 218 219 @return: The filename and an descriptor 220 @rtype: C{svnmailer.util.TempFile} 221 """ 222 return util.TempFile(tempdir = self.getTempDir(), text = False)
223 224
225 - def getTempDir(self):
226 """ Returns the temporary directory 227 228 @return: The directory or C{None} 229 @rtype: C{unicode} or {str} 230 """ 231 if not self._settings.general.tempdir: 232 return None 233 234 return self._settings.general.tempdir
235 236
237 - def getDiffTokens(self, config):
238 """ Returns valid diff tokens and tests 239 240 @param config: group config 241 @type config: C{svnmailer.settings.GroupSettingsContainer} 242 243 @return: The diff tokens and diffable tests 244 The first element of the tuple contains a list 245 of diff tokens, the second element the diff tests 246 @rtype: C{tuple} 247 """ 248 diff_tokens = config.generate_diffs 249 if not diff_tokens: 250 diff_tokens = diff_tokens is not None and ['none'] or [] 251 252 # unknown tokens from the config are just ignored 253 # if the result is empty, we assume either an 254 # empty generate_diffs option or a nasty typo 255 # However, in that case we active all possible tokens 256 diff_test_list = [(token.lower(), test) 257 for token, test in self._diffable_tests 258 if token.lower() in diff_tokens 259 ] or self._diffable_tests 260 261 diff_tokens = [] 262 diff_tests = [] 263 for token, test in diff_test_list: 264 if token != self.NONE: 265 diff_tokens.append(token) 266 diff_tests.append(test) 267 268 return (diff_tokens, diff_tests)
269 270
271 - def dumpContent(self, change, enc = 'utf-8', default = False):
272 """ Dump the two revisions of a particular change 273 274 (This dumps the files, not the properties) 275 276 @param change: The particular change to process 277 @type change: C{svnmailer.subversion.VersionedPathDescriptor} 278 279 @param enc: The file data encoding (The data will be recoded 280 to UTF-8; but by default it isn't recoded, because utf-8 281 is assumed) 282 @type enc: C{str} 283 284 @param default: Return the default encoding (iso-8859-1) if the 285 determined is C{None} 286 @type default: C{bool} 287 288 @return: Two file container objects plus their recoding state 289 (file1, file2, rec1, rec2), where rec? is either the 290 accompanying original encoding or C{None} 291 @rtype: C{tuple} 292 """ 293 from svnmailer import stream 294 295 rec1 = rec2 = None 296 if enc not in (self.ENC_CONFIG, self.ENC_DEFAULT): 297 rec1 = rec2 = enc1 = enc2 = enc 298 else: 299 if enc == self.ENC_CONFIG: 300 enc1, enc2 = self.getContentEncodings(change) 301 rec1, rec2 = (enc1, enc2) 302 enc1 = enc1 or 'iso-8859-1' 303 enc2 = enc2 or 'iso-8859-1' 304 if rec1 and not rec2: 305 rec2 = enc2 306 elif rec2 and not rec1: 307 rec1 = enc1 308 elif enc == self.ENC_DEFAULT: 309 enc1 = enc2 = 'iso-8859-1' 310 311 if default: 312 rec1 = rec1 or enc1 313 rec2 = rec2 or enc2 314 315 file1 = self.getTempFile() 316 if not change.wasAdded() or change.wasCopied(): 317 fp = (enc1 and enc1.lower() != 'utf-8') and \ 318 stream.UnicodeStream(file1.fp, enc1) or file1.fp 319 self._settings.runtime._repos.dumpPathContent( 320 fp, change.getBasePath(), change.getBaseRevision() 321 ) 322 file1.close() 323 324 file2 = self.getTempFile() 325 if not change.wasDeleted(): 326 fp = (enc2 and enc2.lower() != 'utf-8') and \ 327 stream.UnicodeStream(file2.fp, enc2) or file2.fp 328 self._settings.runtime._repos.dumpPathContent( 329 fp, change.path, change.revision 330 ) 331 file2.close() 332 333 return (file1, file2, rec1, rec2)
334 335
336 - def getContentEncodings(self, change, default = None):
337 """ Returns the encodings of the change content (base and current rev) 338 339 @param change: The change to process 340 @type change: C{svnmailer.subversion.VersionedPathDescriptor} 341 342 @param default: The default encoding, if nothing is specified 343 @type default: C{str} 344 345 @return: The two encodings 346 @rtype: C{tuple} of C{str} 347 """ 348 enc1 = enc2 = default 349 if not change.wasAdded() or change.wasCopied(): 350 try: 351 enc1 = self._getContentEncoding( 352 change.getBasePath(), change.getBaseRevision() 353 ) 354 except LookupError: 355 # fall back 356 pass 357 358 if change.wasDeleted(): 359 enc2 = enc1 360 else: 361 try: 362 enc2 = self._getContentEncoding( 363 change.path, change.revision 364 ) 365 except LookupError: 366 # fall back 367 pass 368 369 if change.wasAdded() and not change.wasCopied(): 370 enc1 = enc2 371 372 return (enc1, enc2)
373 374
375 - def _getContentEncoding(self, path, revision):
376 """ Returns the encoding for the specified path and revision 377 378 @param path: The path 379 @type path: C{str} 380 381 @param revision: The revision number 382 @type revision: C{int} 383 384 @return: The encoding 385 @rtype: C{str} 386 387 @exception LookupError: The specified encoding 388 is not implemented or no encoding was specified 389 """ 390 # first try the svn:mime-type 391 enc = self.getEncodingFromMimeType(path, revision) 392 if enc: 393 enc = enc.strip() 394 395 # try svnmailer:content-charset on the file itself 396 if not enc: 397 enc = self.getContentEncodingProperty(path, revision) 398 if enc: 399 enc = enc.strip() 400 401 # nope... traverse the path 402 if not enc: 403 globs = [] 404 for dirpath in util.getParentDirList(path): 405 globlist = self.getContentEncodingProperty(dirpath, revision) 406 if globlist: 407 globs.extend([(glob.strip(), supp_enc.strip()) 408 for glob, supp_enc in [ 409 glob.split('=', 1) 410 for glob in globlist.splitlines(False) 411 if glob and not glob.lstrip()[:1] == '#' 412 and '=' in glob 413 ] if glob.strip() and supp_enc.strip() 414 ]) 415 416 if path[:1] != '/': 417 path = "/%s" % path 418 enc = util.getGlobValue(globs, path) 419 420 if enc: 421 # try a lookup, it raises a LookupError in case of question 422 import codecs 423 codecs.lookup(enc) 424 return enc 425 426 raise LookupError("No Encoding configured")
427 428
429 - def getEncodingFromMimeType(self, path, revision):
430 """ Returns the encoding extracted from svn:mime-type 431 432 @param path: The path 433 @type path: C{str} 434 435 @param revision: The revision number 436 @type revision: C{int} 437 438 @return: The encoding or C{None} 439 @rtype: C{str} 440 """ 441 result = None 442 repos = self._settings.runtime._repos 443 mtype = repos.getPathMimeType(path, revision) 444 445 if mtype: 446 parsed = util.parseContentType(mtype) 447 enc = parsed and parsed[1].get('charset') 448 if enc and len(enc) == 1: 449 result = enc[0] 450 451 return result
452 453
454 - def getContentEncodingProperty(self, path, revision):
455 """ Returns the content encoding property for a path/rev 456 457 @param path: The path 458 @type path: C{str} 459 460 @param revision: The revision number 461 @type revision: C{int} 462 463 @return: The encoding or C{None} 464 @rtype: C{str} 465 """ 466 try: 467 result = self._penc_cache[(path, revision)] 468 except KeyError: 469 repos = self._settings.runtime._repos 470 result = repos.getPathProperty(self.ENC_PROPERTY, path, revision) 471 if result is not None: 472 try: 473 result = result.decode('utf-8') 474 except UnicodeError: 475 # ugh. 476 result = result.decode('iso-8859-1') 477 result = result.encode('utf-8') 478 479 self._penc_cache[(path, revision)] = result 480 481 return result
482 483
484 - def getContentDiffUrl(self, config, change):
485 """ Returns the content diff url for a particular change 486 487 @param config: group config 488 @type config: C{svnmailer.settings.GroupSettingsContainer} 489 490 @param change: The particular change to process 491 @type change: C{svnmailer.subversion.VersionedPathDescriptor} 492 493 @return: The URL or C{None} if there's no base URL configured 494 @rtype: C{str} 495 """ 496 from svnmailer import browser 497 498 generator = browser.getBrowserUrlGenerator(config) 499 if generator: 500 return generator.getContentDiffUrl(change)
501 502
503 - def isUTF8Property(self, name):
504 """ Returns if the supplied property name represents an UTF-8 property 505 506 @param name: The property name 507 @type name: C{str} 508 509 @return: The decision 510 @rtype: C{bool} 511 """ 512 from svnmailer import subversion 513 return subversion.isUnicodeProperty(name)
514 515
516 - def isBinaryProperty(self, values):
517 """ Returns if the supplied property seems to be binary 518 519 Note that is a very rudimentary check, just to not 520 pollute diff output with garbage 521 522 @param values: The value tuple 523 @type values: C{tuple} 524 525 @return: binary property? 526 @rtype: C{bool} 527 """ 528 for value in values: 529 if value is None: 530 continue 531 532 # look for control characters 533 if value != value.translate(EMPTY_TABLE, CTRL_CHARS): 534 return True 535 536 # look for a newline 537 if len(value) > 255 and "\n" not in value[:255]: 538 return True 539 540 # ok, could be text 541 return False
542 543
544 - def isOneLineProperty(self, name, value):
545 """ Returns if the supplied property value takes just one line 546 547 @param value: The property value 548 @type value: C{str} 549 550 @param name: Property name 551 @type name: C{str} 552 553 @return: one line property? 554 @rtype: C{bool} 555 """ 556 # TODO: make one-line-property line length configurable? 557 return bool(len(name + value) <= 75 and value.find("\n") == -1)
558 559
560 - def getContentDiffAction(self, change):
561 """ Returns the content diff action for a particular change 562 563 @param change: The particular change to process 564 @type change: C{svnmailer.subversion.VersionedPathDescriptor} 565 566 @return: The diff token or C{None} if there nothing 567 @rtype: C{unicode} 568 """ 569 if change.wasDeleted(): 570 return self.DELETE 571 else: 572 if change.wasCopied(): 573 return self.COPY 574 elif change.wasAdded(): 575 return self.ADD 576 elif change.hasContentChanges(): 577 return self.MODIFY 578 579 return None
580 581
582 - def getPropertyDiffAction(self, values):
583 """ Returns the property diff action for a particular change 584 585 @param values: The two values of the property 586 @type values: C{tuple} 587 588 @return: The diff token 589 @rtype: C{unicode} 590 """ 591 if values[0] is None: 592 return self.ADD 593 elif values[1] is None: 594 return self.DELETE 595 596 return self.MODIFY
597