Fix Project._ignored_file() doctest, and get it to match directories.
[update-copyright.git] / update_copyright / project.py
index 240270077c770674d07eab09cf0bfda5d5113a64..2a408f5bb47b16a521fec80432247620fd4ead9a 100644 (file)
 # along with update-copyright.  If not, see
 # <http://www.gnu.org/licenses/>.
 
-"""Project-specific configuration.
-
-# Convert author names to canonical forms.
-# ALIASES[<canonical name>] = <list of aliases>
-# for example,
-# ALIASES = {
-#     'John Doe <jdoe@a.com>':
-#         ['John Doe', 'jdoe', 'J. Doe <j@doe.net>'],
-#     }
-# Git-based projects are encouraged to use .mailmap instead of
-# ALIASES.  See git-shortlog(1) for details.
-
-# List of paths that should not be scanned for copyright updates.
-# IGNORED_PATHS = ['./.git/']
-IGNORED_PATHS = ['./.git']
-# List of files that should not be scanned for copyright updates.
-# IGNORED_FILES = ['COPYING']
-IGNORED_FILES = ['COPYING']
-
-# Work around missing author holes in the VCS history.
-# AUTHOR_HACKS[<path tuple>] = [<missing authors]
-# for example, if John Doe contributed to module.py but wasn't listed
-# in the VCS history of that file:
-# AUTHOR_HACKS = {
-#     ('path', 'to', 'module.py'):['John Doe'],
-#     }
-AUTHOR_HACKS = {}
-
-# Work around missing year holes in the VCS history.
-# YEAR_HACKS[<path tuple>] = <original year>
-# for example, if module.py was published in 2008 but the VCS history
-# only goes back to 2010:
-# YEAR_HACKS = {
-#     ('path', 'to', 'module.py'):2008,
-#     }
-YEAR_HACKS = {}
-"""
+"""Project-specific configuration."""
 
 import ConfigParser as _configparser
 import fnmatch as _fnmatch
@@ -74,10 +38,14 @@ except ImportError, _mercurial_import_error:
 
 
 class Project (object):
-    def __init__(self, name=None, vcs=None, copyright=None,
+    def __init__(self, root='.', name=None, vcs=None, copyright=None,
                  short_copyright=None):
+        self._root = _os_path.normpath(_os_path.abspath(root))
         self._name = name
         self._vcs = vcs
+        self._author_hacks = None
+        self._year_hacks = None
+        self._aliases = None
         self._copyright = None
         self._short_copyright = None
         self.with_authors = False
@@ -94,8 +62,9 @@ class Project (object):
         parser = _configparser.RawConfigParser()
         parser.readfp(stream)
         for section in parser.sections():
+            clean_section = section.replace('-', '_')
             try:
-                loader = getattr(self, '_load_{}_conf'.format(section))
+                loader = getattr(self, '_load_{}_conf'.format(clean_section))
             except AttributeError, e:
                 _LOG.error('invalid {} section'.format(section))
                 raise
@@ -111,16 +80,22 @@ class Project (object):
         except _configparser.NoOptionError:
             pass
         else:
+            kwargs = {
+                'root': self._root,
+                'author_hacks': self._author_hacks,
+                'year_hacks': self._year_hacks,
+                'aliases': self._aliases,
+                }
             if vcs == 'Git':
-                self._vcs = _GitBackend()
+                self._vcs = _GitBackend(**kwargs)
             elif vcs == 'Bazaar':
                 if _BazaarBackend is None:
                     raise _bazaar_import_error
-                self._vcs = _BazaarBackend()
+                self._vcs = _BazaarBackend(**kwargs)
             elif vcs == 'Mercurial':
                 if _MercurialBackend is None:
                     raise _mercurial_import_error
-                self._vcs = _MercurialBackend()
+                self._vcs = _MercurialBackend(**kwargs)
             else:
                 raise NotImplementedError('vcs: {}'.format(vcs))
 
@@ -151,9 +126,44 @@ class Project (object):
         else:
             self._ignored_paths = [pth.strip() for pth in ignored.split(',')]
         try:
-            self._pyfile = parser.get('files', 'pyfile')
+            pyfile = parser.get('files', 'pyfile')
         except _configparser.NoOptionError:
             pass
+        else:
+            self._pyfile = _os_path.join(self._root, pyfile)
+
+    def _load_author_hacks_conf(self, parser, encoding=None):
+        if encoding is None:
+            encoding = self._encoding or _utils.ENCODING
+        author_hacks = {}
+        for path in parser.options('author-hacks'):
+            authors = parser.get('author-hacks', path)
+            author_hacks[tuple(path.split('/'))] = set(
+                unicode(a.strip(), encoding) for a in authors.split(','))
+        self._author_hacks = author_hacks
+        if self._vcs is not None:
+            self._vcs._author_hacks = self._author_hacks
+
+    def _load_year_hacks_conf(self, parser):
+        year_hacks = {}
+        for path in parser.options('year-hacks'):
+            year = parser.get('year-hacks', path)
+            year_hacks[tuple(path.split('/'))] = int(year)
+        self._year_hacks = year_hacks
+        if self._vcs is not None:
+            self._vcs._year_hacks = self._year_hacks
+
+    def _load_aliases_conf(self, parser, encoding=None):
+        if encoding is None:
+            encoding = self._encoding or _utils.ENCODING
+        aliases = {}
+        for author in parser.options('aliases'):
+            _aliases = parser.get('aliases', author)
+            aliases[author] = set(
+                unicode(a.strip(), encoding) for a in _aliases.split(','))
+        self._aliases = aliases
+        if self._vcs is not None:
+            self._vcs._aliases = self._aliases
 
     def _info(self):
         return {
@@ -167,7 +177,8 @@ class Project (object):
         new_contents = u'{} was written by:\n{}\n'.format(
             self._name, u'\n'.join(authors))
         _utils.set_contents(
-            'AUTHORS', new_contents, unicode=True, encoding=self._encoding,
+            _os_path.join(self._root, 'AUTHORS'),
+            new_contents, unicode=True, encoding=self._encoding,
             dry_run=dry_run)
 
     def update_file(self, filename, dry_run=False):
@@ -187,7 +198,7 @@ class Project (object):
 
     def update_files(self, files=None, dry_run=False):
         if files is None or len(files) == 0:
-            files = _utils.list_files(root='.')
+            files = _utils.list_files(root=self._root)
         for filename in files:
             if self._ignored_file(filename=filename):
                 continue
@@ -240,25 +251,31 @@ class Project (object):
 
     def _ignored_file(self, filename):
         """
-        >>> ignored_paths = ['./a/', './b/']
-        >>> ignored_files = ['x', 'y']
-        >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False)
+        >>> p = Project()
+        >>> p._ignored_paths = ['a', './b/']
+        >>> p._ignored_file('./a/')
         True
-        >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False)
-        False
-        >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False)
+        >>> p._ignored_file('b')
         True
-        >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False)
+        >>> p._ignored_file('a/z')
+        True
+        >>> p._ignored_file('ab/z')
+        False
+        >>> p._ignored_file('./ab/a')
         False
-        >>> ignored_file('./z', ignored_paths, ignored_files, False, False)
+        >>> p._ignored_file('./z')
         False
         """
+        filename = _os_path.relpath(filename, self._root)
         if self._ignored_paths is not None:
-            for path in self._ignored_paths:
-                if _fnmatch.fnmatch(filename, path):
-                    _LOG.debug('ignoring {} (matched {})'.format(
-                            filename, path))
-                    return True
+            base = filename
+            while base not in ['', '.', '..']:
+                for path in self._ignored_paths:
+                    if _fnmatch.fnmatch(base, _os_path.normpath(path)):
+                        _LOG.debug('ignoring {} (matched {})'.format(
+                                filename, path))
+                        return True
+                base = _os_path.split(base)[0]
         if self._vcs and not self._vcs.is_versioned(filename):
             _LOG.debug('ignoring {} (not versioned))'.format(filename))
             return True