5 """Automatically update copyright boilerplate.
7 This script is adapted from one written for `Bugs Everywhere`_.
9 .. _Bugs Everywhere: http://bugseverywhere.org/
22 import mercurial.dispatch
30 # Break "copyright" into "copy" and "right" to avoid matching the
33 This file is part of %(project)s.
35 %(project)s is free software: you can redistribute it and/or
36 modify it under the terms of the GNU Lesser General Public
37 License as published by the Free Software Foundation, either
38 version 3 of the License, or (at your option) any later version.
40 %(project)s is distributed in the hope that it will be useful,
41 but WITHOUT ANY WARRANTY; without even the implied warranty of
42 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
43 GNU Lesser General Public License for more details.
45 You should have received a copy of the GNU Lesser General Public
46 License along with %(project)s. If not, see
47 <http://www.gnu.org/licenses/>.
50 COPY_RIGHT_TAG='-xyz-COPY-RIGHT-zyx-' # unlikely to occur in the wild :p
53 'Alberto Gomez-Casado':
55 'Massimo Sandal <devicerandom@gmail.com>':
58 'Fabrizio Benedetti':['fabrizio.benedetti'],
60 'Marco Brucale':['marcobrucale'],
61 'pp':['pancaldi.paolo'],
64 IGNORED_PATHS = ['./.hg/', './doc/img', './test/data/',
65 './build/', '/doc/build/']
66 IGNORED_FILES = ['COPYING', 'COPYING.LESSER']
69 # VCS-specific commands
71 def mercurial_cmd(*args):
75 tmp_stdout = StringIO.StringIO()
76 tmp_stderr = StringIO.StringIO()
77 sys.stdout = tmp_stdout
78 sys.stderr = tmp_stderr
80 mercurial.dispatch.dispatch(list(args))
85 return (tmp_stdout.getvalue().rstrip('\n'),
86 tmp_stderr.getvalue().rstrip('\n'))
88 def original_year(filename):
89 # shortdate filter: YEAR-MONTH-DAY
90 output,error = mercurial_cmd('log', '--follow',
91 '--template', '{date|shortdate}\n',
93 years = [int(line.split('-', 1)[0]) for line in output.splitlines()]
97 def authors(filename):
98 output,error = mercurial_cmd('log', '--follow',
99 '--template', '{author}\n',
101 return list(set(output.splitlines()))
104 output,error = mercurial_cmd('log', '--follow',
105 '--template', '{author}\n')
106 return list(set(output.splitlines()))
108 def is_versioned(filename):
109 output,error = mercurial_cmd('log', '--follow',
110 '--template', '{date|shortdate}\n',
116 # General utility commands
118 def _strip_email(*args):
119 """Remove email addresses from a series of names.
124 >>> _strip_email('J Doe <jdoe@a.com>')
126 >>> _strip_email('J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>')
127 ['J Doe', 'JJJ Smith']
130 for i,arg in enumerate(args):
133 author,addr = email.utils.parseaddr(arg)
137 def _reverse_aliases(aliases):
138 """Reverse an `aliases` dict.
140 Input: key: canonical name, value: list of aliases
141 Output: key: alias, value: canonical name
147 ... 'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>', 'J'],
148 ... 'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
149 ... None:['Anonymous <a@a.com>'],
151 >>> r = _reverse_aliases(aliases)
152 >>> for item in sorted(r.items()):
154 ('Anonymous <a@a.com>', None)
155 ('J', 'J Doe <jdoe@a.com>')
156 ('Jingly <jjjs@b.edu>', 'JJJ Smith <jjjs@a.com>')
157 ('Johnny <jdoe@b.edu>', 'J Doe <jdoe@a.com>')
160 for canonical_name,_aliases in aliases.items():
161 for alias in _aliases:
162 output[alias] = canonical_name
165 def _replace_aliases(authors, with_email=True, aliases=None):
166 """Consolidate and sort `authors`.
168 Make the replacements listed in the `aliases` dict (key: canonical
169 name, value: list of aliases). If `aliases` is ``None``, default
173 ... 'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>'],
174 ... 'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
175 ... None:['Anonymous <a@a.com>'],
177 >>> _replace_aliases(['JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
178 ... 'Jingly <jjjs@b.edu>', 'Anonymous <a@a.com>'],
179 ... with_email=True, aliases=aliases)
180 ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
181 >>> _replace_aliases(['JJJ Smith', 'Johnny', 'Jingly', 'Anonymous'],
182 ... with_email=False, aliases=aliases)
183 ['J Doe', 'JJJ Smith']
184 >>> _replace_aliases(['JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
185 ... 'Jingly <jjjs@b.edu>', 'J Doe <jdoe@a.com>'],
186 ... with_email=True, aliases=aliases)
187 ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
191 if with_email == False:
192 aliases = dict([(_strip_email(author)[0], _strip_email(*_aliases))
193 for author,_aliases in aliases.items()])
194 rev_aliases = _reverse_aliases(aliases)
195 for i,author in enumerate(authors):
196 if author in rev_aliases:
197 authors[i] = rev_aliases[author]
198 authors = sorted(list(set(authors)))
203 def _copyright_string(original_year, final_year, authors, prefix=''):
205 >>> print _copyright_string(original_year=2005,
207 ... authors=['A <a@a.com>', 'B <b@b.edu>'],
209 ... ) # doctest: +ELLIPSIS
210 # Copyright (C) 2005 A <a@a.com>
214 >>> print _copyright_string(original_year=2005,
216 ... authors=['A <a@a.com>', 'B <b@b.edu>']
217 ... ) # doctest: +ELLIPSIS
218 Copyright (C) 2005-2009 A <a@a.com>
223 if original_year == final_year:
224 date_range = '%s' % original_year
226 date_range = '%s-%s' % (original_year, final_year)
227 lines = ['Copyright (C) %s %s' % (date_range, authors[0])]
228 for author in authors[1:]:
229 lines.append(' '*(len('Copyright (C) ')+len(date_range)+1) +
232 lines.extend((COPY_RIGHT_TEXT % PROJECT_INFO).splitlines())
233 for i,line in enumerate(lines):
234 lines[i] = (prefix + line).rstrip()
235 return '\n'.join(lines)
237 def _tag_copyright(contents):
239 >>> contents = '''Some file
241 ... # Copyright (copyright begins)
242 ... # (copyright continues)
247 >>> print _tag_copyright(contents),
256 for line in contents.splitlines():
257 if incopy == False and line.startswith('# Copyright'):
259 lines.append(COPY_RIGHT_TAG)
260 elif incopy == True and not line.startswith('#'):
263 lines.append(line.rstrip('\n'))
264 return '\n'.join(lines)+'\n'
266 def _update_copyright(contents, original_year, authors):
268 >>> contents = '''Some file
270 ... # Copyright (copyright begins)
271 ... # (copyright continues)
276 >>> print _update_copyright(contents, 2008, ['Jack', 'Jill']
277 ... ) # doctest: +ELLIPSIS, +REPORT_UDIFF
280 # Copyright (C) 2008-... Jack
288 current_year = time.gmtime()[0]
289 copyright_string = _copyright_string(
290 original_year, current_year, authors, prefix='# ')
291 contents = _tag_copyright(contents)
292 return contents.replace(COPY_RIGHT_TAG, copyright_string)
294 def ignored_file(filename, ignored_paths=None, ignored_files=None):
296 >>> ignored_paths = ['./a/', './b/']
297 >>> ignored_files = ['x', 'y']
298 >>> ignored_file('./a/z', ignored_paths, ignored_files)
300 >>> ignored_file('./ab/z', ignored_paths, ignored_files)
302 >>> ignored_file('./ab/x', ignored_paths, ignored_files)
304 >>> ignored_file('./ab/xy', ignored_paths, ignored_files)
306 >>> ignored_file('./z', ignored_paths, ignored_files)
309 if ignored_paths == None:
310 ignored_paths = IGNORED_PATHS
311 if ignored_files == None:
312 ignored_files = IGNORED_FILES
313 if os.path.isfile(filename) == False:
315 for path in ignored_paths:
316 if filename.startswith(path):
318 if os.path.basename(filename) in ignored_files:
320 if is_versioned(filename) == False:
324 def _set_contents(filename, contents, original_contents=None, dry_run=False,
326 if original_contents == None and os.path.isfile(filename):
327 f = open(filename, 'r')
328 original_contents = f.read()
331 print "checking %s ... " % filename,
332 if contents != original_contents:
334 if original_contents == None:
338 if verbose > 1 and original_contents != None:
340 difflib.unified_diff(
341 original_contents.splitlines(), contents.splitlines(),
342 fromfile=os.path.normpath(os.path.join('a', filename)),
343 tofile=os.path.normpath(os.path.join('b', filename)),
346 f = file(filename, 'w')
354 def update_authors(authors_fn=authors_list, dry_run=False, verbose=0):
355 new_contents = '%s was written by:\n%s\n' % (
356 PROJECT_INFO['project'],
357 '\n'.join(authors_fn())
359 _set_contents('AUTHORS', new_contents, dry_run=dry_run, verbose=verbose)
361 def update_file(filename, original_year_fn=original_year, authors_fn=authors,
362 dry_run=False, verbose=0):
363 f = file(filename, 'r')
367 original_year = original_year_fn(filename)
368 authors = authors_fn(filename)
369 authors = _replace_aliases(authors, with_email=True, aliases=ALIASES)
371 new_contents = _update_copyright(contents, original_year, authors)
372 _set_contents(filename, contents=new_contents, original_contents=contents,
373 dry_run=dry_run, verbose=verbose)
375 def update_files(files=None, dry_run=False, verbose=0):
376 if files == None or len(files) == 0:
378 for dirpath,dirnames,filenames in os.walk('.'):
379 for filename in filenames:
380 files.append(os.path.join(dirpath, filename))
382 for filename in files:
383 if ignored_file(filename) == True:
385 update_file(filename, dry_run=dry_run, verbose=verbose)
391 if __name__ == '__main__':
395 usage = """%%prog [options] [file ...]
397 Update copyright information in source code with information from
398 the %(vcs)s repository. Run from the %(project)s repository root.
400 Replaces every line starting with '^# Copyright' and continuing with
401 '^#' with an auto-generated copyright blurb. If you want to add
402 #-commented material after a copyright blurb, please insert a blank
403 line between the blurb and your comment, so the next run of
404 ``update_copyright.py`` doesn't clobber your comment.
406 If no files are given, a list of files to update is generated
409 p = optparse.OptionParser(usage)
410 p.add_option('--test', dest='test', default=False,
411 action='store_true', help='Run internal tests and exit')
412 p.add_option('--dry-run', dest='dry_run', default=False,
413 action='store_true', help="Don't make any changes")
414 p.add_option('-v', '--verbose', dest='verbose', default=0,
415 action='count', help='Increment verbosity')
416 options,args = p.parse_args()
418 if options.test == True:
422 update_authors(dry_run=options.dry_run, verbose=options.verbose)
423 update_files(files=args, dry_run=options.dry_run, verbose=options.verbose)