1 # Copyright (C) 2012-2013 W. Trevor King <wking@tremily.us>
3 # This file is part of update-copyright.
5 # update-copyright is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
10 # update-copyright is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # update-copyright. If not, see <http://www.gnu.org/licenses/>.
18 """Project-specific configuration."""
20 import ConfigParser as _configparser
21 import fnmatch as _fnmatch
22 import os.path as _os_path
26 from . import LOG as _LOG
27 from . import utils as _utils
28 from .vcs.git import GitBackend as _GitBackend
30 from .vcs.bazaar import BazaarBackend as _BazaarBackend
31 except ImportError as _bazaar_import_error:
34 from .vcs.mercurial import MercurialBackend as _MercurialBackend
35 except ImportError as _mercurial_import_error:
36 _MercurialBackend = None
39 class Project (object):
40 def __init__(self, root='.', name=None, vcs=None, copyright=None,
41 short_copyright=None):
42 self._root = _os_path.normpath(_os_path.abspath(root))
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()
62 parser.optionxform = str
64 for section in parser.sections():
65 clean_section = section.replace('-', '_')
67 loader = getattr(self, '_load_{}_conf'.format(clean_section))
68 except AttributeError as e:
69 _LOG.error('invalid {} section'.format(section))
73 def _load_project_conf(self, parser):
75 self._name = parser.get('project', 'name')
76 except _configparser.NoOptionError:
79 vcs = parser.get('project', 'vcs')
80 except _configparser.NoOptionError:
85 'author_hacks': self._author_hacks,
86 'year_hacks': self._year_hacks,
87 'aliases': self._aliases,
90 self._vcs = _GitBackend(**kwargs)
92 if _BazaarBackend is None:
93 raise _bazaar_import_error
94 self._vcs = _BazaarBackend(**kwargs)
95 elif vcs == 'Mercurial':
96 if _MercurialBackend is None:
97 raise _mercurial_import_error
98 self._vcs = _MercurialBackend(**kwargs)
100 raise NotImplementedError('vcs: {}'.format(vcs))
102 def _load_copyright_conf(self, parser, encoding=None):
104 encoding = self._encoding or _utils.ENCODING
106 self._copyright = self._split_paragraphs(
107 unicode(parser.get('copyright', 'long'), encoding))
108 except _configparser.NoOptionError:
111 self._short_copyright = self._split_paragraphs(
112 unicode(parser.get('copyright', 'short'), encoding))
113 except _configparser.NoOptionError:
116 def _split_paragraphs(self, text):
117 return [p.strip() for p in text.split(u'\n')]
119 def _load_files_conf(self, parser):
121 self.with_authors = parser.getboolean('files', 'authors')
122 except _configparser.NoOptionError:
125 self.with_files = parser.getboolean('files', 'files')
126 except _configparser.NoOptionError:
129 ignored = parser.get('files', 'ignored')
130 except _configparser.NoOptionError:
133 self._ignored_paths = [pth.strip() for pth in ignored.split('|')]
135 pyfile = parser.get('files', 'pyfile')
136 except _configparser.NoOptionError:
139 self._pyfile = _os_path.join(self._root, pyfile)
141 def _load_author_hacks_conf(self, parser, encoding=None):
143 encoding = self._encoding or _utils.ENCODING
145 for path in parser.options('author-hacks'):
146 authors = parser.get('author-hacks', path)
147 author_hacks[tuple(path.split('/'))] = set(
148 unicode(a.strip(), encoding) for a in authors.split('|'))
149 self._author_hacks = author_hacks
150 if self._vcs is not None:
151 self._vcs._author_hacks = self._author_hacks
153 def _load_year_hacks_conf(self, parser):
155 for path in parser.options('year-hacks'):
156 year = parser.get('year-hacks', path)
157 year_hacks[tuple(path.split('/'))] = int(year)
158 self._year_hacks = year_hacks
159 if self._vcs is not None:
160 self._vcs._year_hacks = self._year_hacks
162 def _load_aliases_conf(self, parser, encoding=None):
164 encoding = self._encoding or _utils.ENCODING
166 for author in parser.options('aliases'):
167 _aliases = parser.get('aliases', author)
168 author = unicode(author, encoding)
169 aliases[author] = set(
170 unicode(a.strip(), encoding) for a in _aliases.split('|'))
171 self._aliases = aliases
172 if self._vcs is not None:
173 self._vcs._aliases = self._aliases
177 'project': self._name,
178 'vcs': self._vcs.name,
181 def update_authors(self, dry_run=False):
182 _LOG.info('update AUTHORS')
183 authors = self._vcs.authors()
184 new_contents = u'{} was written by:\n{}\n'.format(
185 self._name, u'\n'.join(authors))
187 _os_path.join(self._root, 'AUTHORS'),
188 new_contents, unicode=True, encoding=self._encoding,
191 def update_file(self, filename, dry_run=False):
192 _LOG.info('update {}'.format(filename))
193 contents = _utils.get_contents(
194 filename=filename, unicode=True, encoding=self._encoding)
195 original_year = self._vcs.original_year(filename=filename)
196 authors = self._vcs.authors(filename=filename)
197 new_contents = _utils.update_copyright(
198 contents=contents, original_year=original_year, authors=authors,
199 text=self._copyright, info=self._info(), prefix=('# ', '# ', None),
200 width=self._width, tag=self._copyright_tag)
201 new_contents = _utils.update_copyright(
202 contents=new_contents, original_year=original_year,
203 authors=authors, text=self._copyright, info=self._info(),
204 prefix=('/* ', ' * ', ' */'), width=self._width,
205 tag=self._copyright_tag)
207 filename=filename, contents=new_contents,
208 original_contents=contents, unicode=True, encoding=self._encoding,
211 def update_files(self, files=None, dry_run=False):
212 if files is None or len(files) == 0:
213 files = _utils.list_files(root=self._root)
214 for filename in files:
215 if self._ignored_file(filename=filename):
217 self.update_file(filename=filename, dry_run=dry_run)
219 def update_pyfile(self, dry_run=False):
220 if self._pyfile is None:
221 _LOG.info('no pyfile location configured, skip `update_pyfile`')
223 _LOG.info('update pyfile at {}'.format(self._pyfile))
224 current_year = _time.gmtime()[0]
225 original_year = self._vcs.original_year()
226 authors = self._vcs.authors()
228 _utils.copyright_string(
229 original_year=original_year, final_year=current_year,
230 authors=authors, text=self._copyright, info=self._info(),
231 prefix=(u'# ', u'# ', None), width=self._width),
232 u'', u'import textwrap as _textwrap', u'', u'',
234 _utils.copyright_string(
235 original_year=original_year, final_year=current_year,
236 authors=authors, text=self._copyright, info=self._info(),
237 prefix=(u'', u'', None), width=self._width),
240 u'def short_license(info, wrap=True, **kwargs):',
243 paragraphs = _utils.copyright_string(
244 original_year=original_year, final_year=current_year,
245 authors=authors, text=self._short_copyright, info=self._info(),
246 author_format_fn=_utils.short_author_formatter, wrap=False,
249 lines.append(u" '{}'.format(**info),".format(
250 p.replace(u"'", ur"\'")))
254 u' for i,p in enumerate(paragraphs):',
255 u' paragraphs[i] = _textwrap.fill(p, **kwargs)',
256 ur" return '\n\n'.join(paragraphs)",
257 u'', # for terminal endline
259 new_contents = u'\n'.join(lines)
261 filename=self._pyfile, contents=new_contents, unicode=True,
262 encoding=self._encoding, dry_run=dry_run)
264 def _ignored_file(self, filename):
267 >>> p._ignored_paths = ['a', './b/']
268 >>> p._ignored_file('./a/')
270 >>> p._ignored_file('b')
272 >>> p._ignored_file('a/z')
274 >>> p._ignored_file('ab/z')
276 >>> p._ignored_file('./ab/a')
278 >>> p._ignored_file('./z')
281 filename = _os_path.relpath(filename, self._root)
282 if self._ignored_paths is not None:
284 while base not in ['', '.', '..']:
285 for path in self._ignored_paths:
286 if _fnmatch.fnmatch(base, _os_path.normpath(path)):
287 _LOG.debug('ignoring {} (matched {})'.format(
290 base = _os_path.split(base)[0]
291 if self._vcs and not self._vcs.is_versioned(filename):
292 _LOG.debug('ignoring {} (not versioned))'.format(filename))