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