1 # Copyright (C) 2012-2015 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.mercurial import MercurialBackend as _MercurialBackend
31 except ImportError as _mercurial_import_error:
32 _MercurialBackend = None
35 class Project (object):
36 def __init__(self, root='.', name=None, vcs=None, copyright=None,
37 short_copyright=None):
38 self._root = _os_path.normpath(_os_path.abspath(root))
41 self._author_hacks = None
42 self._year_hacks = None
44 self._copyright = None
45 self._short_copyright = None
46 self.with_authors = False
47 self.with_files = False
48 self._ignored_paths = None
53 # unlikely to occur in the wild :p
54 self._copyright_tag = '-xyz-COPY' + '-RIGHT-zyx-'
56 def load_config(self, stream):
57 parser = _configparser.RawConfigParser()
58 parser.optionxform = str
60 for section in parser.sections():
61 clean_section = section.replace('-', '_')
63 loader = getattr(self, '_load_{}_conf'.format(clean_section))
64 except AttributeError as e:
65 _LOG.error('invalid {} section'.format(section))
69 def _load_project_conf(self, parser):
71 project = parser['project']
74 self._name = project.get('name', _os_path.basename(self._root))
75 vcs = project.get('vcs')
78 'author_hacks': self._author_hacks,
79 'year_hacks': self._year_hacks,
80 'aliases': self._aliases,
83 self._vcs = _GitBackend(**kwargs)
84 elif vcs == 'Mercurial':
85 if _MercurialBackend is None:
86 raise _mercurial_import_error
87 self._vcs = _MercurialBackend(**kwargs)
89 raise NotImplementedError('vcs: {}'.format(vcs))
91 def _load_copyright_conf(self, parser):
93 copyright = parser['copyright']
96 if 'long' in copyright:
97 self._copyright = self._split_paragraphs(copyright['long'])
98 if 'short' in copyright:
99 self._short_copyright = self._split_paragraphs(copyright['short'])
101 def _split_paragraphs(self, text):
102 return [p.strip() for p in text.split('\n\n')]
104 def _load_files_conf(self, parser):
105 files = parser['files']
106 self.with_authors = files.getboolean('authors')
107 self.with_files = files.getboolean('files')
108 ignored = files.get('ignored')
110 self._ignored_paths = [pth.strip() for pth in ignored.split('|')]
111 pyfile = files.get('pyfile')
113 self._pyfile = _os_path.join(self._root, pyfile)
115 def _load_author_hacks_conf(self, parser):
117 section = parser['author-hacks']
121 for path, authors in section.items():
122 author_hacks[tuple(path.split('/'))] = set(
123 a.strip() for a in authors.split('|'))
124 self._author_hacks = author_hacks
125 if self._vcs is not None:
126 self._vcs._author_hacks = self._author_hacks
128 def _load_year_hacks_conf(self, parser):
130 section = parser['year-hacks']
134 for path, year in section.items():
135 year_hacks[tuple(path.split('/'))] = int(year)
136 self._year_hacks = year_hacks
137 if self._vcs is not None:
138 self._vcs._year_hacks = self._year_hacks
140 def _load_aliases_conf(self, parser):
142 section = parser['aliases']
146 for author, _aliases in section.items():
147 aliases[author] = set(
148 a.strip() for a in _aliases.split('|'))
149 self._aliases = aliases
150 if self._vcs is not None:
151 self._vcs._aliases = self._aliases
155 'project': self._name,
156 'vcs': self._vcs.name,
159 def update_authors(self, dry_run=False):
160 _LOG.info('update AUTHORS')
161 authors = self._vcs.authors()
162 new_contents = '{} was written by:\n{}\n'.format(
163 self._name, '\n'.join(authors))
165 _os_path.join(self._root, 'AUTHORS'),
166 new_contents, unicode=True, encoding=self._encoding,
169 def update_file(self, filename, dry_run=False):
170 _LOG.info('update {}'.format(filename))
171 contents = _utils.get_contents(
172 filename=filename, unicode=True, encoding=self._encoding)
173 years = self._vcs.years(filename=filename)
174 authors = self._vcs.authors(filename=filename)
175 new_contents = _utils.update_copyright(
176 contents=contents, years=years, authors=authors,
177 text=self._copyright, info=self._info(), prefix=('# ', '# ', None),
178 width=self._width, tag=self._copyright_tag)
179 new_contents = _utils.update_copyright(
180 contents=new_contents, years=years,
181 authors=authors, text=self._copyright, info=self._info(),
182 prefix=('/* ', ' * ', ' */'), width=self._width,
183 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=self._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 years = self._vcs.years()
204 authors = self._vcs.authors()
206 _utils.copyright_string(
207 years=years, authors=authors, text=self._copyright,
208 info=self._info(), prefix=('# ', '# ', None),
210 '', 'import textwrap as _textwrap', '', '',
212 _utils.copyright_string(
213 years=years, authors=authors, text=self._copyright,
214 info=self._info(), prefix=('', '', None), width=self._width),
217 'def short_license(info, wrap=True, **kwargs):',
220 paragraphs = _utils.copyright_string(
221 years=years, authors=authors, text=self._short_copyright,
222 info=self._info(), author_format_fn=_utils.short_author_formatter,
226 lines.append(" '{}'.format(**info),".format(
227 p.replace("'", r"\'")))
231 ' for i,p in enumerate(paragraphs):',
232 ' paragraphs[i] = _textwrap.fill(p, **kwargs)',
233 r" return '\n\n'.join(paragraphs)",
234 '', # for terminal endline
236 new_contents = '\n'.join(lines)
238 filename=self._pyfile, contents=new_contents, unicode=True,
239 encoding=self._encoding, dry_run=dry_run)
241 def _ignored_file(self, filename):
244 >>> p._ignored_paths = ['a', './b/']
245 >>> p._ignored_file('./a/')
247 >>> p._ignored_file('b')
249 >>> p._ignored_file('a/z')
251 >>> p._ignored_file('ab/z')
253 >>> p._ignored_file('./ab/a')
255 >>> p._ignored_file('./z')
258 filename = _os_path.relpath(filename, self._root)
259 if self._ignored_paths is not None:
261 while base not in ['', '.', '..']:
262 for path in self._ignored_paths:
263 if _fnmatch.fnmatch(base, _os_path.normpath(path)):
264 _LOG.debug('ignoring {} (matched {})'.format(
267 base = _os_path.split(base)[0]
268 if self._vcs and not self._vcs.is_versioned(filename):
269 _LOG.debug('ignoring {} (not versioned))'.format(filename))