X-Git-Url: http://git.tremily.us/?p=update-copyright.git;a=blobdiff_plain;f=update_copyright%2Fproject.py;h=dd9b2a898af0103bf38de30ad3b2ae3a05cc587a;hp=ac93c2ff069b20df7ce8c8823b73f844813418db;hb=acfec4340bfa7c668e6c06c208ad6ba924300f91;hpb=082f9f53d65388d1d9128cef8ec6ebbcb0aa3b33 diff --git a/update_copyright/project.py b/update_copyright/project.py index ac93c2f..dd9b2a8 100644 --- a/update_copyright/project.py +++ b/update_copyright/project.py @@ -1,58 +1,21 @@ -# Copyright (C) 2012 W. Trevor King +# Copyright (C) 2012 W. Trevor King # # This file is part of update-copyright. # -# update-copyright is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. +# update-copyright is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. # -# update-copyright is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. +# update-copyright is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. # -# You should have received a copy of the GNU General Public License -# along with update-copyright. If not, see -# . +# You should have received a copy of the GNU General Public License along with +# update-copyright. If not, see . -"""Project-specific configuration. - -# Convert author names to canonical forms. -# ALIASES[] = -# for example, -# ALIASES = { -# 'John Doe ': -# ['John Doe', 'jdoe', 'J. Doe '], -# } -# 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[] = [] = -# 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 +37,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 @@ -92,6 +59,7 @@ class Project (object): def load_config(self, stream): parser = _configparser.RawConfigParser() + parser.optionxform = str parser.readfp(stream) for section in parser.sections(): clean_section = section.replace('-', '_') @@ -112,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)) @@ -152,9 +126,45 @@ 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) + author = unicode(author, encoding) + 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 { @@ -168,7 +178,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): @@ -179,8 +190,13 @@ class Project (object): authors = self._vcs.authors(filename=filename) new_contents = _utils.update_copyright( contents=contents, original_year=original_year, authors=authors, - text=self._copyright, info=self._info(), prefix='# ', + text=self._copyright, info=self._info(), prefix=('# ', '# ', None), width=self._width, tag=self._copyright_tag) + new_contents = _utils.update_copyright( + contents=new_contents, original_year=original_year, + authors=authors, text=self._copyright, info=self._info(), + prefix=('/* ', ' * ', ' */'), width=self._width, + tag=self._copyright_tag) _utils.set_contents( filename=filename, contents=new_contents, original_contents=contents, unicode=True, encoding=self._encoding, @@ -188,7 +204,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 @@ -241,25 +257,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 + >>> p._ignored_file('a/z') True - >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False) + >>> 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