1 # Copyright (C) 2012 W. Trevor King
3 # This file is part of update-copyright.
5 # update-copyright is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # update-copyright is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with update-copyright. If not, see
17 # <http://www.gnu.org/licenses/>.
19 """Project-specific configuration.
21 # Convert author names to canonical forms.
22 # ALIASES[<canonical name>] = <list of aliases>
25 # 'John Doe <jdoe@a.com>':
26 # ['John Doe', 'jdoe', 'J. Doe <j@doe.net>'],
28 # Git-based projects are encouraged to use .mailmap instead of
29 # ALIASES. See git-shortlog(1) for details.
31 # List of paths that should not be scanned for copyright updates.
32 # IGNORED_PATHS = ['./.git/']
33 IGNORED_PATHS = ['./.git']
34 # List of files that should not be scanned for copyright updates.
35 # IGNORED_FILES = ['COPYING']
36 IGNORED_FILES = ['COPYING']
38 # Work around missing author holes in the VCS history.
39 # AUTHOR_HACKS[<path tuple>] = [<missing authors]
40 # for example, if John Doe contributed to module.py but wasn't listed
41 # in the VCS history of that file:
43 # ('path', 'to', 'module.py'):['John Doe'],
47 # Work around missing year holes in the VCS history.
48 # YEAR_HACKS[<path tuple>] = <original year>
49 # for example, if module.py was published in 2008 but the VCS history
50 # only goes back to 2010:
52 # ('path', 'to', 'module.py'):2008,
57 import ConfigParser as _configparser
58 import fnmatch as _fnmatch
59 import os.path as _os_path
63 from . import LOG as _LOG
64 from . import utils as _utils
65 from .vcs.git import GitBackend as _GitBackend
67 from .vcs.bazaar import BazaarBackend as _BazaarBackend
68 except ImportError, _bazaar_import_error:
71 from .vcs.mercurial import MercurialBackend as _MercurialBackend
72 except ImportError, _mercurial_import_error:
73 _MercurialBackend = None
76 class Project (object):
77 def __init__(self, name=None, vcs=None, copyright=None,
78 short_copyright=None):
81 self._copyright = None
82 self._short_copyright = None
83 self.with_authors = False
84 self.with_files = False
85 self._ignored_paths = None
90 # unlikely to occur in the wild :p
91 self._copyright_tag = u'-xyz-COPY' + u'-RIGHT-zyx-'
93 def load_config(self, stream):
94 parser = _configparser.RawConfigParser()
96 for section in parser.sections():
97 clean_section = section.replace('-', '_')
99 loader = getattr(self, '_load_{}_conf'.format(clean_section))
100 except AttributeError, e:
101 _LOG.error('invalid {} section'.format(section))
103 loader(parser=parser)
105 def _load_project_conf(self, parser):
107 self._name = parser.get('project', 'name')
108 except _configparser.NoOptionError:
111 vcs = parser.get('project', 'vcs')
112 except _configparser.NoOptionError:
116 self._vcs = _GitBackend()
117 elif vcs == 'Bazaar':
118 if _BazaarBackend is None:
119 raise _bazaar_import_error
120 self._vcs = _BazaarBackend()
121 elif vcs == 'Mercurial':
122 if _MercurialBackend is None:
123 raise _mercurial_import_error
124 self._vcs = _MercurialBackend()
126 raise NotImplementedError('vcs: {}'.format(vcs))
128 def _load_copyright_conf(self, parser):
130 self._copyright = parser.get('copyright', 'long').splitlines()
131 except _configparser.NoOptionError:
134 self._short_copyright = parser.get(
135 'copyright', 'short').splitlines()
136 except _configparser.NoOptionError:
139 def _load_files_conf(self, parser):
141 self.with_authors = parser.get('files', 'authors')
142 except _configparser.NoOptionError:
145 self.with_files = parser.get('files', 'files')
146 except _configparser.NoOptionError:
149 ignored = parser.get('files', 'ignored')
150 except _configparser.NoOptionError:
153 self._ignored_paths = [pth.strip() for pth in ignored.split(',')]
155 self._pyfile = parser.get('files', 'pyfile')
156 except _configparser.NoOptionError:
161 'project': self._name,
162 'vcs': self._vcs.name,
165 def update_authors(self, dry_run=False):
166 _LOG.info('update AUTHORS')
167 authors = self._vcs.authors()
168 new_contents = u'{} was written by:\n{}\n'.format(
169 self._name, u'\n'.join(authors))
171 'AUTHORS', new_contents, unicode=True, encoding=self._encoding,
174 def update_file(self, filename, dry_run=False):
175 _LOG.info('update {}'.format(filename))
176 contents = _utils.get_contents(
177 filename=filename, unicode=True, encoding=self._encoding)
178 original_year = self._vcs.original_year(filename=filename)
179 authors = self._vcs.authors(filename=filename)
180 new_contents = _utils.update_copyright(
181 contents=contents, original_year=original_year, authors=authors,
182 text=self._copyright, info=self._info(), prefix='# ',
183 width=self._width, tag=self._copyright_tag)
185 filename=filename, contents=new_contents,
186 original_contents=contents, unicode=True, encoding=self._encoding,
189 def update_files(self, files=None, dry_run=False):
190 if files is None or len(files) == 0:
191 files = _utils.list_files(root='.')
192 for filename in files:
193 if self._ignored_file(filename=filename):
195 self.update_file(filename=filename, dry_run=dry_run)
197 def update_pyfile(self, dry_run=False):
198 if self._pyfile is None:
199 _LOG.info('no pyfile location configured, skip `update_pyfile`')
201 _LOG.info('update pyfile at {}'.format(self._pyfile))
202 current_year = _time.gmtime()[0]
203 original_year = self._vcs.original_year()
204 authors = self._vcs.authors()
206 _utils.copyright_string(
207 original_year=original_year, final_year=current_year,
208 authors=authors, text=self._copyright, info=self._info(),
209 prefix=u'# ', width=self._width),
210 u'', u'import textwrap as _textwrap', u'', u'',
212 _utils.copyright_string(
213 original_year=original_year, final_year=current_year,
214 authors=authors, text=self._copyright, info=self._info(),
215 prefix=u'', width=self._width),
218 u'def short_license(info, wrap=True, **kwargs):',
221 paragraphs = _utils.copyright_string(
222 original_year=original_year, final_year=current_year,
223 authors=authors, text=self._short_copyright, info=self._info(),
224 author_format_fn=_utils.short_author_formatter, wrap=False,
227 lines.append(u" '{}' % info,".format(
228 p.replace(u"'", ur"\'")))
232 u' for i,p in enumerate(paragraphs):',
233 u' paragraphs[i] = _textwrap.fill(p, **kwargs)',
234 ur" return '\n\n'.join(paragraphs)",
235 u'', # for terminal endline
237 new_contents = u'\n'.join(lines)
239 filename=self._pyfile, contents=new_contents, unicode=True,
240 encoding=self._encoding, dry_run=dry_run)
242 def _ignored_file(self, filename):
244 >>> ignored_paths = ['./a/', './b/']
245 >>> ignored_files = ['x', 'y']
246 >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False)
248 >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False)
250 >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False)
252 >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False)
254 >>> ignored_file('./z', ignored_paths, ignored_files, False, False)
257 if self._ignored_paths is not None:
258 for path in self._ignored_paths:
259 if _fnmatch.fnmatch(filename, path):
260 _LOG.debug('ignoring {} (matched {})'.format(
263 if self._vcs and not self._vcs.is_versioned(filename):
264 _LOG.debug('ignoring {} (not versioned))'.format(filename))