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.82'],
59 'Rolf Schmidt <rschmidt@alcor.concordia.ca>':['illysam'],
60 'Marco Brucale':['marcobrucale'],
61 'Pancaldi Paolo':['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).replace('COPY-RIGHT', 'CR')
257 for line in contents.splitlines():
258 if incopy == False and line.startswith('# Copyright'):
260 lines.append(COPY_RIGHT_TAG)
261 elif incopy == True and not line.startswith('#'):
264 lines.append(line.rstrip('\n'))
265 return '\n'.join(lines)+'\n'
267 def _update_copyright(contents, original_year, authors):
269 >>> contents = '''Some file
271 ... # Copyright (copyright begins)
272 ... # (copyright continues)
277 >>> print _update_copyright(contents, 2008, ['Jack', 'Jill']
278 ... ) # doctest: +ELLIPSIS, +REPORT_UDIFF
281 # Copyright (C) 2008-... Jack
289 current_year = time.gmtime()[0]
290 copyright_string = _copyright_string(
291 original_year, current_year, authors, prefix='# ')
292 contents = _tag_copyright(contents)
293 return contents.replace(COPY_RIGHT_TAG, copyright_string)
295 def ignored_file(filename, ignored_paths=None, ignored_files=None,
296 check_disk=True, check_vcs=True):
298 >>> ignored_paths = ['./a/', './b/']
299 >>> ignored_files = ['x', 'y']
300 >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False)
302 >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False)
304 >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False)
306 >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False)
308 >>> ignored_file('./z', ignored_paths, ignored_files, False, False)
311 if ignored_paths == None:
312 ignored_paths = IGNORED_PATHS
313 if ignored_files == None:
314 ignored_files = IGNORED_FILES
315 if check_disk == True and os.path.isfile(filename) == False:
317 for path in ignored_paths:
318 if filename.startswith(path):
320 if os.path.basename(filename) in ignored_files:
322 if check_vcs == True and is_versioned(filename) == False:
326 def _set_contents(filename, contents, original_contents=None, dry_run=False,
328 if original_contents == None and os.path.isfile(filename):
329 f = open(filename, 'r')
330 original_contents = f.read()
333 print "checking %s ... " % filename,
334 if contents != original_contents:
336 if original_contents == None:
340 if verbose > 1 and original_contents != None:
342 difflib.unified_diff(
343 original_contents.splitlines(), contents.splitlines(),
344 fromfile=os.path.normpath(os.path.join('a', filename)),
345 tofile=os.path.normpath(os.path.join('b', filename)),
348 f = file(filename, 'w')
356 def update_authors(authors_fn=authors_list, dry_run=False, verbose=0):
357 new_contents = '%s was written by:\n%s\n' % (
358 PROJECT_INFO['project'],
359 '\n'.join(authors_fn())
361 _set_contents('AUTHORS', new_contents, dry_run=dry_run, verbose=verbose)
363 def update_file(filename, original_year_fn=original_year, authors_fn=authors,
364 dry_run=False, verbose=0):
365 f = file(filename, 'r')
369 original_year = original_year_fn(filename)
370 authors = authors_fn(filename)
371 authors = _replace_aliases(authors, with_email=True, aliases=ALIASES)
373 new_contents = _update_copyright(contents, original_year, authors)
374 _set_contents(filename, contents=new_contents, original_contents=contents,
375 dry_run=dry_run, verbose=verbose)
377 def update_files(files=None, dry_run=False, verbose=0):
378 if files == None or len(files) == 0:
380 for dirpath,dirnames,filenames in os.walk('.'):
381 for filename in filenames:
382 files.append(os.path.join(dirpath, filename))
384 for filename in files:
385 if ignored_file(filename) == True:
387 update_file(filename, dry_run=dry_run, verbose=verbose)
393 if __name__ == '__main__':
397 usage = """%%prog [options] [file ...]
399 Update copyright information in source code with information from
400 the %(vcs)s repository. Run from the %(project)s repository root.
402 Replaces every line starting with '^# Copyright' and continuing with
403 '^#' with an auto-generated copyright blurb. If you want to add
404 #-commented material after a copyright blurb, please insert a blank
405 line between the blurb and your comment, so the next run of
406 ``update_copyright.py`` doesn't clobber your comment.
408 If no files are given, a list of files to update is generated
411 p = optparse.OptionParser(usage)
412 p.add_option('--test', dest='test', default=False,
413 action='store_true', help='Run internal tests and exit')
414 p.add_option('--dry-run', dest='dry_run', default=False,
415 action='store_true', help="Don't make any changes")
416 p.add_option('-v', '--verbose', dest='verbose', default=0,
417 action='count', help='Increment verbosity')
418 options,args = p.parse_args()
420 if options.test == True:
424 update_authors(dry_run=options.dry_run, verbose=options.verbose)
425 update_files(files=args, dry_run=options.dry_run, verbose=options.verbose)