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 import ConfigParser as _configparser
22 import fnmatch as _fnmatch
23 import os.path as _os_path
27 from . import LOG as _LOG
28 from . import utils as _utils
29 from .vcs.git import GitBackend as _GitBackend
31 from .vcs.bazaar import BazaarBackend as _BazaarBackend
32 except ImportError, _bazaar_import_error:
35 from .vcs.mercurial import MercurialBackend as _MercurialBackend
36 except ImportError, _mercurial_import_error:
37 _MercurialBackend = None
40 class Project (object):
41 def __init__(self, name=None, vcs=None, copyright=None,
42 short_copyright=None):
45 self._author_hacks = None
46 self._year_hacks = None
48 self._copyright = None
49 self._short_copyright = None
50 self.with_authors = False
51 self.with_files = False
52 self._ignored_paths = None
57 # unlikely to occur in the wild :p
58 self._copyright_tag = u'-xyz-COPY' + u'-RIGHT-zyx-'
60 def load_config(self, stream):
61 parser = _configparser.RawConfigParser()
63 for section in parser.sections():
64 clean_section = section.replace('-', '_')
66 loader = getattr(self, '_load_{}_conf'.format(clean_section))
67 except AttributeError, e:
68 _LOG.error('invalid {} section'.format(section))
72 def _load_project_conf(self, parser):
74 self._name = parser.get('project', 'name')
75 except _configparser.NoOptionError:
78 vcs = parser.get('project', 'vcs')
79 except _configparser.NoOptionError:
83 'author_hacks': self._author_hacks,
84 'year_hacks': self._year_hacks,
85 'aliases': self._aliases,
88 self._vcs = _GitBackend(**kwargs)
90 if _BazaarBackend is None:
91 raise _bazaar_import_error
92 self._vcs = _BazaarBackend(**kwargs)
93 elif vcs == 'Mercurial':
94 if _MercurialBackend is None:
95 raise _mercurial_import_error
96 self._vcs = _MercurialBackend(**kwargs)
98 raise NotImplementedError('vcs: {}'.format(vcs))
100 def _load_copyright_conf(self, parser):
102 self._copyright = parser.get('copyright', 'long').splitlines()
103 except _configparser.NoOptionError:
106 self._short_copyright = parser.get(
107 'copyright', 'short').splitlines()
108 except _configparser.NoOptionError:
111 def _load_files_conf(self, parser):
113 self.with_authors = parser.get('files', 'authors')
114 except _configparser.NoOptionError:
117 self.with_files = parser.get('files', 'files')
118 except _configparser.NoOptionError:
121 ignored = parser.get('files', 'ignored')
122 except _configparser.NoOptionError:
125 self._ignored_paths = [pth.strip() for pth in ignored.split(',')]
127 self._pyfile = parser.get('files', 'pyfile')
128 except _configparser.NoOptionError:
131 def _load_author_hacks_conf(self, parser, encoding=None):
133 encoding = self._encoding or _utils.ENCODING
135 for path in parser.options('author-hacks'):
136 authors = parser.get('author-hacks', path)
137 author_hacks[tuple(path.split('/'))] = set(
138 unicode(a.strip(), encoding) for a in authors.split(','))
139 self._author_hacks = author_hacks
140 if self._vcs is not None:
141 self._vcs._author_hacks = self._author_hacks
143 def _load_year_hacks_conf(self, parser):
145 for path in parser.options('year-hacks'):
146 year = parser.get('year-hacks', path)
147 year_hacks[tuple(path.split('/'))] = int(year)
148 self._year_hacks = year_hacks
149 if self._vcs is not None:
150 self._vcs._year_hacks = self._year_hacks
152 def _load_aliases_conf(self, parser, encoding=None):
154 encoding = self._encoding or _utils.ENCODING
156 for author in parser.options('aliases'):
157 _aliases = parser.get('aliases', author)
158 aliases[author] = set(
159 unicode(a.strip(), encoding) for a in _aliases.split(','))
160 self._aliases = aliases
161 if self._vcs is not None:
162 self._vcs._aliases = self._aliases
166 'project': self._name,
167 'vcs': self._vcs.name,
170 def update_authors(self, dry_run=False):
171 _LOG.info('update AUTHORS')
172 authors = self._vcs.authors()
173 new_contents = u'{} was written by:\n{}\n'.format(
174 self._name, u'\n'.join(authors))
176 'AUTHORS', new_contents, unicode=True, encoding=self._encoding,
179 def update_file(self, filename, dry_run=False):
180 _LOG.info('update {}'.format(filename))
181 contents = _utils.get_contents(
182 filename=filename, unicode=True, encoding=self._encoding)
183 original_year = self._vcs.original_year(filename=filename)
184 authors = self._vcs.authors(filename=filename)
185 new_contents = _utils.update_copyright(
186 contents=contents, original_year=original_year, authors=authors,
187 text=self._copyright, info=self._info(), prefix='# ',
188 width=self._width, tag=self._copyright_tag)
190 filename=filename, contents=new_contents,
191 original_contents=contents, unicode=True, encoding=self._encoding,
194 def update_files(self, files=None, dry_run=False):
195 if files is None or len(files) == 0:
196 files = _utils.list_files(root='.')
197 for filename in files:
198 if self._ignored_file(filename=filename):
200 self.update_file(filename=filename, dry_run=dry_run)
202 def update_pyfile(self, dry_run=False):
203 if self._pyfile is None:
204 _LOG.info('no pyfile location configured, skip `update_pyfile`')
206 _LOG.info('update pyfile at {}'.format(self._pyfile))
207 current_year = _time.gmtime()[0]
208 original_year = self._vcs.original_year()
209 authors = self._vcs.authors()
211 _utils.copyright_string(
212 original_year=original_year, final_year=current_year,
213 authors=authors, text=self._copyright, info=self._info(),
214 prefix=u'# ', width=self._width),
215 u'', u'import textwrap as _textwrap', u'', u'',
217 _utils.copyright_string(
218 original_year=original_year, final_year=current_year,
219 authors=authors, text=self._copyright, info=self._info(),
220 prefix=u'', width=self._width),
223 u'def short_license(info, wrap=True, **kwargs):',
226 paragraphs = _utils.copyright_string(
227 original_year=original_year, final_year=current_year,
228 authors=authors, text=self._short_copyright, info=self._info(),
229 author_format_fn=_utils.short_author_formatter, wrap=False,
232 lines.append(u" '{}' % info,".format(
233 p.replace(u"'", ur"\'")))
237 u' for i,p in enumerate(paragraphs):',
238 u' paragraphs[i] = _textwrap.fill(p, **kwargs)',
239 ur" return '\n\n'.join(paragraphs)",
240 u'', # for terminal endline
242 new_contents = u'\n'.join(lines)
244 filename=self._pyfile, contents=new_contents, unicode=True,
245 encoding=self._encoding, dry_run=dry_run)
247 def _ignored_file(self, filename):
249 >>> ignored_paths = ['./a/', './b/']
250 >>> ignored_files = ['x', 'y']
251 >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False)
253 >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False)
255 >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False)
257 >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False)
259 >>> ignored_file('./z', ignored_paths, ignored_files, False, False)
262 if self._ignored_paths is not None:
263 for path in self._ignored_paths:
264 if _fnmatch.fnmatch(filename, path):
265 _LOG.debug('ignoring {} (matched {})'.format(
268 if self._vcs and not self._vcs.is_versioned(filename):
269 _LOG.debug('ignoring {} (not versioned))'.format(filename))