Merged Rolf Schmidt's illysam branch
[hooke.git] / update_copyright.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Hooke.
6 #
7 # Hooke is free software: you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation, either
10 # version 3 of the License, or (at your option) any later version.
11 #
12 # Hooke is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with Hooke.  If not, see
19 # <http://www.gnu.org/licenses/>.
20
21 """Automatically update copyright boilerplate.
22
23 This script is adapted from one written for `Bugs Everywhere`_.
24
25 .. _Bugs Everywhere: http://bugseverywhere.org/
26 """
27
28 import difflib
29 import email.utils
30 import os
31 import os.path
32 import re
33 import sys
34 import time
35
36
37 PROJECT_INFO = {
38     'project': 'Hooke',
39     'vcs': 'Mercurial', 
40     }
41
42 # Break "copyright" into "copy" and "right" to avoid matching the
43 # REGEXP.
44 COPY_RIGHT_TEXT="""
45 This file is part of %(project)s.
46
47 %(project)s is free software: you can redistribute it and/or
48 modify it under the terms of the GNU Lesser General Public
49 License as published by the Free Software Foundation, either
50 version 3 of the License, or (at your option) any later version.
51
52 %(project)s is distributed in the hope that it will be useful,
53 but WITHOUT ANY WARRANTY; without even the implied warranty of
54 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
55 GNU Lesser General Public License for more details.
56
57 You should have received a copy of the GNU Lesser General Public
58 License along with %(project)s.  If not, see
59 <http://www.gnu.org/licenses/>.
60 """.strip()
61
62 COPY_RIGHT_TAG='-xyz-COPY' + '-RIGHT-zyx-' # unlikely to occur in the wild :p
63
64 # Convert author names to canonical forms.
65 # ALIASES[<canonical name>] = <list of aliases>
66 # for example,
67 # ALIASES = {
68 #     'John Doe <jdoe@a.com>':
69 #         ['John Doe', 'jdoe', 'J. Doe <j@doe.net>'],
70 #     }
71 # Git-based projects are encouraged to use .mailmap instead of
72 # ALIASES.  See git-shortlog(1) for details.
73 ALIASES = {
74     'A. Seeholzer':
75         ['A. Seeholzer'],
76     'Alberto Gomez-Casado':
77         ['albertogomcas'],
78     'Massimo Sandal <devicerandom@gmail.com>':
79         ['Massimo Sandal',
80          'devicerandom',
81          'unknown'],
82     'Fabrizio Benedetti':
83         ['fabrizio.benedetti.82'],
84     'Richard Naud <richard.naud@epfl.ch>':
85         ['Richard Naud'],
86     'Rolf Schmidt <rschmidt@alcor.concordia.ca>':
87         ['Rolf Schmidt',
88          'illysam'],
89     'Marco Brucale':
90         ['marcobrucale'],
91     'Pancaldi Paolo':
92         ['pancaldi.paolo'],
93     }
94
95 # List of paths that should not be scanned for copyright updates.
96 # IGNORED_PATHS = ['./.git/']
97 IGNORED_PATHS = ['./.hg/', './doc/img/', './test/data/',
98                  './build/', './doc/build/']
99 # List of files that should not be scanned for copyright updates.
100 # IGNORED_FILES = ['COPYING']
101 IGNORED_FILES = ['COPYING', 'COPYING.LESSER']
102
103 # Work around missing author holes in the VCS history.
104 # AUTHOR_HACKS[<path tuple>] = [<missing authors]
105 # for example, if John Doe contributed to module.py but wasn't listed
106 # in the VCS history of that file:
107 # AUTHOR_HACKS = {
108 #     ('path', 'to', 'module.py'):['John Doe'],
109 #     }
110 AUTHOR_HACKS = {
111     ('hooke','driver','hdf5.py'):['Massimo Sandal'],
112     ('hooke','driver','mcs.py'):['Allen Chen'],
113     ('hooke','driver','mfp3d.py'):['A. Seeholzer','Richard Naud','Rolf Schmidt',
114                                    'Alberto Gomez-Casado'],
115     ('hooke','util','peak.py'):['Fabrizio Benedetti'],
116     ('hooke','plugin','showconvoluted.py'):['Rolf Schmidt'],
117     ('hooke','ui','gui','formatter.py'):['Francesco Musiani','Massimo Sandal'],
118     ('hooke','ui','gui','prettyformat.py'):['Rolf Schmidt'],
119     }
120
121 # Work around missing year holes in the VCS history.
122 # YEAR_HACKS[<path tuple>] = <original year>
123 # for example, if module.py was published in 2008 but the VCS history
124 # only goes back to 2010:
125 # YEAR_HACKS = {
126 #     ('path', 'to', 'module.py'):2008,
127 #     }
128 YEAR_HACKS = {
129     ('hooke','driver','hdf5.py'):2009,
130     ('hooke','driver','mfp3d.py'):2008,
131     ('hooke','driver','picoforce.py'):2006,
132     ('hooke','driver','picoforcealt.py'):2006,
133     ('hooke','util','peak.py'):2007,
134     ('hooke','plugin','showconvoluted.py'):2009,
135     ('hooke','plugin','tutorial.py'):2007,
136     ('hooke','ui','gui','formatter.py'):2006,
137     ('hooke','ui','gui','prettyformat.py'):2009,
138     }
139
140 # Helpers for VCS-specific commands
141
142 def splitpath(path):
143     """Recursively split a path into elements.
144
145     Examples
146     --------
147
148     >>> splitpath(os.path.join('a', 'b', 'c'))
149     ('a', 'b', 'c')
150     >>> splitpath(os.path.join('.', 'a', 'b', 'c'))
151     ('a', 'b', 'c')
152     """
153     path = os.path.normpath(path)
154     elements = []
155     while True:
156         dirname,basename = os.path.split(path)
157         elements.insert(0,basename)
158         if dirname in ['', '.']:
159             break
160         path = dirname
161     return tuple(elements)
162
163 # VCS-specific commands
164
165 if PROJECT_INFO['vcs'] == 'Git':
166
167     import subprocess
168
169     _MSWINDOWS = sys.platform == 'win32'
170     _POSIX = not _MSWINDOWS
171
172     def invoke(args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, expect=(0,)):
173         """
174         expect should be a tuple of allowed exit codes.
175         """
176         try :
177             if _POSIX:
178                 q = subprocess.Popen(args, stdin=subprocess.PIPE,
179                                      stdout=stdout, stderr=stderr)
180             else:
181                 assert _MSWINDOWS == True, 'invalid platform'
182                 # win32 don't have os.execvp() so run the command in a shell
183                 q = subprocess.Popen(args, stdin=subprocess.PIPE,
184                                      stdout=stdout, stderr=stderr, shell=True)
185         except OSError, e:
186             raise ValueError([args, e])
187         stdout,stderr = q.communicate(input=stdin)
188         status = q.wait()
189         if status not in expect:
190             raise ValueError([args, status, stdout, stderr])
191         return status, stdout, stderr
192
193     def git_cmd(*args):
194         status,stdout,stderr = invoke(['git'] + list(args))
195         return stdout.rstrip('\n')
196         
197     def original_year(filename, year_hacks=YEAR_HACKS):
198         output = git_cmd('log', '--follow',
199                          '--format=format:%ad',  # Author date
200                          '--date=short',         # YYYY-MM-DD
201                          filename)
202         years = [int(line.split('-', 1)[0]) for line in output.splitlines()]
203         if splitpath(filename) in year_hacks:
204             years.append(year_hacks[splitpath(filename)])
205         years.sort()
206         return years[0]
207     
208     def authors(filename, author_hacks=AUTHOR_HACKS):
209         output = git_cmd('log', '--follow', '--format=format:%aN <%aE>',
210                          filename)   # Author name <author email>
211         ret = list(set(output.splitlines()))
212         if splitpath(filename) in author_hacks:
213             ret.extend(author_hacks[splitpath(filename)])
214         return ret
215     
216     def authors_list(author_hacks=AUTHOR_HACKS):
217         output = git_cmd('log', '--format=format:%aN <%aE>')
218         ret = list(set(output.splitlines()))
219         for path,authors in author_hacks.items():
220             ret.extend(authors)
221         return ret
222     
223     def is_versioned(filename):
224         output = git_cmd('log', '--follow', filename)
225         if len(output) == 0:
226             return False        
227         return True
228
229 elif PROJECT_INFO['vcs'] == 'Mercurial':
230
231     import StringIO
232     import mercurial
233     import mercurial.dispatch
234
235     def mercurial_cmd(*args):
236         cwd = os.getcwd()
237         stdout = sys.stdout
238         stderr = sys.stderr
239         tmp_stdout = StringIO.StringIO()
240         tmp_stderr = StringIO.StringIO()
241         sys.stdout = tmp_stdout
242         sys.stderr = tmp_stderr
243         try:
244             mercurial.dispatch.dispatch(list(args))
245         finally:
246             os.chdir(cwd)
247             sys.stdout = stdout
248             sys.stderr = stderr
249         return (tmp_stdout.getvalue().rstrip('\n'),
250                 tmp_stderr.getvalue().rstrip('\n'))
251         
252     def original_year(filename, year_hacks=YEAR_HACKS):
253         # shortdate filter: YEAR-MONTH-DAY
254         output,error = mercurial_cmd('log', '--follow',
255                                      '--template', '{date|shortdate}\n',
256                                      filename)
257         years = [int(line.split('-', 1)[0]) for line in output.splitlines()]
258         if splitpath(filename) in year_hacks:
259             years.append(year_hacks[splitpath(filename)])
260         years.sort()
261         return years[0]
262     
263     def authors(filename, author_hacks=AUTHOR_HACKS):
264         output,error = mercurial_cmd('log', '--follow',
265                                      '--template', '{author}\n',
266                                      filename)
267         ret = list(set(output.splitlines()))
268         if splitpath(filename) in author_hacks:
269             ret.extend(author_hacks[splitpath(filename)])
270         return ret
271     
272     def authors_list(author_hacks=AUTHOR_HACKS):
273         output,error = mercurial_cmd('log', '--template', '{author}\n')
274         ret = list(set(output.splitlines()))
275         for path,authors in author_hacks.items():
276             ret.extend(authors)
277         return ret
278     
279     def is_versioned(filename):
280         output,error = mercurial_cmd('log', '--follow', filename)
281         if len(error) > 0:
282             return False
283         return True
284
285 elif PROJECT_INFO['vcs'] == 'Bazaar':
286
287     import StringIO
288     import bzrlib
289     import bzrlib.builtins
290     import bzrlib.log
291
292     class LogFormatter (bzrlib.log.LogFormatter):
293         supports_merge_revisions = True
294         preferred_levels = 0
295         supports_deta = False
296         supports_tags = False
297         supports_diff = False
298
299         def log_revision(self, revision):
300             raise NotImplementedError
301
302     class YearLogFormatter (LogFormatter):
303         def log_revision(self, revision):
304             self.to_file.write(
305                 time.strftime('%Y', time.gmtime(revision.rev.timestamp))
306                 +'\n')
307
308     class AuthorLogFormatter (LogFormatter):
309         def log_revision(self, revision):
310             authors = revision.rev.get_apparent_authors()
311             self.to_file.write('\n'.join(authors)+'\n')
312
313     def original_year(filename, year_hacks=YEAR_HACKS):
314         cmd = bzrlib.builtins.cmd_log()
315         cmd.outf = StringIO.StringIO()
316         cmd.run(file_list=[filename], log_format=YearLogFormatter, levels=0)
317         years = [int(year) for year in set(cmd.outf.getvalue().splitlines())]
318         if splitpath(filename) in year_hacks:
319             years.append(year_hacks[splitpath(filename)])
320         years.sort()
321         return years[0]
322     
323     def authors(filename, author_hacks=AUTHOR_HACKS):
324         cmd = bzrlib.builtins.cmd_log()
325         cmd.outf = StringIO.StringIO()
326         cmd.run(file_list=[filename], log_format=AuthorLogFormatter, levels=0)
327         ret = list(set(cmd.outf.getvalue().splitlines()))
328         if splitpath(filename) in author_hacks:
329             ret.extend(author_hacks[splitpath(filename)])
330         return ret
331     
332     def authors_list(author_hacks=AUTHOR_HACKS):
333         cmd = bzrlib.builtins.cmd_log()
334         cmd.outf = StringIO.StringIO()
335         cmd.run(log_format=AuthorLogFormatter, levels=0)
336         output = cmd.outf.getvalue()
337         ret = list(set(cmd.outf.getvalue().splitlines()))
338         for path,authors in author_hacks.items():
339             ret.extend(authors)
340         return ret
341     
342     def is_versioned(filename):
343         cmd = bzrlib.builtins.cmd_log()
344         cmd.outf = StringIO.StringIO()
345         cmd.run(file_list=[filename])
346         return True
347
348 else:
349     raise NotImplementedError('Unrecognized VCS: %(vcs)s' % PROJECT_INFO)
350
351 # General utility commands
352
353 def _strip_email(*args):
354     """Remove email addresses from a series of names.
355
356     Examples
357     --------
358
359     >>> _strip_email('J Doe <jdoe@a.com>')
360     ['J Doe']
361     >>> _strip_email('J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>')
362     ['J Doe', 'JJJ Smith']
363     """
364     args = list(args)
365     for i,arg in enumerate(args):
366         if arg == None:
367             continue
368         author,addr = email.utils.parseaddr(arg)
369         args[i] = author
370     return args
371
372 def _reverse_aliases(aliases):
373     """Reverse an `aliases` dict.
374
375     Input:   key: canonical name,  value: list of aliases
376     Output:  key: alias,           value: canonical name
377
378     Examples
379     --------
380
381     >>> aliases = {
382     ...     'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>', 'J'],
383     ...     'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
384     ...     None:['Anonymous <a@a.com>'],
385     ...     }
386     >>> r = _reverse_aliases(aliases)
387     >>> for item in sorted(r.items()):
388     ...     print item
389     ('Anonymous <a@a.com>', None)
390     ('J', 'J Doe <jdoe@a.com>')
391     ('Jingly <jjjs@b.edu>', 'JJJ Smith <jjjs@a.com>')
392     ('Johnny <jdoe@b.edu>', 'J Doe <jdoe@a.com>')
393     """
394     output = {}
395     for canonical_name,_aliases in aliases.items():
396         for alias in _aliases:
397             output[alias] = canonical_name
398     return output
399
400 def _replace_aliases(authors, with_email=True, aliases=None):
401     """Consolidate and sort `authors`.
402
403     Make the replacements listed in the `aliases` dict (key: canonical
404     name, value: list of aliases).  If `aliases` is ``None``, default
405     to ``ALIASES``.
406     
407     >>> aliases = {
408     ...     'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>'],
409     ...     'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
410     ...     None:['Anonymous <a@a.com>'],
411     ...     }
412     >>> _replace_aliases(['JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
413     ...                   'Jingly <jjjs@b.edu>', 'Anonymous <a@a.com>'],
414     ...                  with_email=True, aliases=aliases)
415     ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
416     >>> _replace_aliases(['JJJ Smith', 'Johnny', 'Jingly', 'Anonymous'],
417     ...                  with_email=False, aliases=aliases)
418     ['J Doe', 'JJJ Smith']
419     >>> _replace_aliases(['JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
420     ...                   'Jingly <jjjs@b.edu>', 'J Doe <jdoe@a.com>'],
421     ...                  with_email=True, aliases=aliases)
422     ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
423     """
424     if aliases == None:
425         aliases = ALIASES
426     if with_email == False:
427         aliases = dict([(_strip_email(author)[0], _strip_email(*_aliases))
428                         for author,_aliases in aliases.items()])
429     rev_aliases = _reverse_aliases(aliases)
430     for i,author in enumerate(authors):
431         if author in rev_aliases:
432             authors[i] = rev_aliases[author]
433     authors = sorted(list(set(authors)))
434     if None in authors:
435         authors.remove(None)
436     return authors
437
438 def _copyright_string(original_year, final_year, authors, prefix=''):
439     """
440     >>> print _copyright_string(original_year=2005,
441     ...                         final_year=2005,
442     ...                         authors=['A <a@a.com>', 'B <b@b.edu>'],
443     ...                         prefix='# '
444     ...                        ) # doctest: +ELLIPSIS
445     # Copyright (C) 2005 A <a@a.com>
446     #                    B <b@b.edu>
447     #
448     # This file...
449     >>> print _copyright_string(original_year=2005,
450     ...                         final_year=2009,
451     ...                         authors=['A <a@a.com>', 'B <b@b.edu>']
452     ...                        ) # doctest: +ELLIPSIS
453     Copyright (C) 2005-2009 A <a@a.com>
454                             B <b@b.edu>
455     <BLANKLINE>
456     This file...
457     """
458     if original_year == final_year:
459         date_range = '%s' % original_year
460     else:
461         date_range = '%s-%s' % (original_year, final_year)
462     lines = ['Copyright (C) %s %s' % (date_range, authors[0])]
463     for author in authors[1:]:
464         lines.append(' '*(len('Copyright (C) ')+len(date_range)+1) +
465                      author)
466     lines.append('')
467     lines.extend((COPY_RIGHT_TEXT % PROJECT_INFO).splitlines())
468     for i,line in enumerate(lines):
469         lines[i] = (prefix + line).rstrip()
470     return '\n'.join(lines)
471
472 def _tag_copyright(contents):
473     """
474     >>> contents = '''Some file
475     ... bla bla
476     ... # Copyright (copyright begins)
477     ... # (copyright continues)
478     ... # bla bla bla
479     ... (copyright ends)
480     ... bla bla bla
481     ... '''
482     >>> print _tag_copyright(contents).replace('COPY-RIGHT', 'CR')
483     Some file
484     bla bla
485     -xyz-CR-zyx-
486     (copyright ends)
487     bla bla bla
488     <BLANKLINE>
489     """
490     lines = []
491     incopy = False
492     for line in contents.splitlines():
493         if incopy == False and line.startswith('# Copyright'):
494             incopy = True
495             lines.append(COPY_RIGHT_TAG)
496         elif incopy == True and not line.startswith('#'):
497             incopy = False
498         if incopy == False:
499             lines.append(line.rstrip('\n'))
500     return '\n'.join(lines)+'\n'
501
502 def _update_copyright(contents, original_year, authors):
503     """
504     >>> contents = '''Some file
505     ... bla bla
506     ... # Copyright (copyright begins)
507     ... # (copyright continues)
508     ... # bla bla bla
509     ... (copyright ends)
510     ... bla bla bla
511     ... '''
512     >>> print _update_copyright(contents, 2008, ['Jack', 'Jill']
513     ...     ) # doctest: +ELLIPSIS, +REPORT_UDIFF
514     Some file
515     bla bla
516     # Copyright (C) 2008-... Jack
517     #                         Jill
518     #
519     # This file...
520     (copyright ends)
521     bla bla bla
522     <BLANKLINE>
523     """
524     current_year = time.gmtime()[0]
525     copyright_string = _copyright_string(
526         original_year, current_year, authors, prefix='# ')
527     contents = _tag_copyright(contents)
528     return contents.replace(COPY_RIGHT_TAG, copyright_string)
529
530 def ignored_file(filename, ignored_paths=None, ignored_files=None,
531                  check_disk=True, check_vcs=True):
532     """
533     >>> ignored_paths = ['./a/', './b/']
534     >>> ignored_files = ['x', 'y']
535     >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False)
536     True
537     >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False)
538     False
539     >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False)
540     True
541     >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False)
542     False
543     >>> ignored_file('./z', ignored_paths, ignored_files, False, False)
544     False
545     """
546     if ignored_paths == None:
547         ignored_paths = IGNORED_PATHS
548     if ignored_files == None:
549         ignored_files = IGNORED_FILES
550     if check_disk == True and os.path.isfile(filename) == False:
551         return True
552     for path in ignored_paths:
553         if filename.startswith(path):
554             return True
555     if os.path.basename(filename) in ignored_files:
556         return True
557     if check_vcs == True and is_versioned(filename) == False:
558         return True
559     return False
560
561 def _set_contents(filename, contents, original_contents=None, dry_run=False,
562                   verbose=0):
563     if original_contents == None and os.path.isfile(filename):
564         f = open(filename, 'r')
565         original_contents = f.read()
566         f.close()
567     if verbose > 0:
568         print "checking %s ... " % filename,
569     if contents != original_contents:
570         if verbose > 0:
571             if original_contents == None:
572                 print "[creating]"
573             else:
574                 print "[updating]"
575         if verbose > 1 and original_contents != None:
576             print '\n'.join(
577                 difflib.unified_diff(
578                     original_contents.splitlines(), contents.splitlines(),
579                     fromfile=os.path.normpath(os.path.join('a', filename)),
580                     tofile=os.path.normpath(os.path.join('b', filename)),
581                     n=3, lineterm=''))
582         if dry_run == False:
583             f = file(filename, 'w')
584             f.write(contents)
585             f.close()
586     elif verbose > 0:
587         print "[no change]"
588
589 # Update commands
590
591 def update_authors(authors_fn=authors_list, dry_run=False, verbose=0):
592     authors = authors_fn()
593     authors = _replace_aliases(authors, with_email=True, aliases=ALIASES)
594     new_contents = '%s was written by:\n%s\n' % (
595         PROJECT_INFO['project'],
596         '\n'.join(authors)
597         )
598     _set_contents('AUTHORS', new_contents, dry_run=dry_run, verbose=verbose)
599
600 def update_file(filename, original_year_fn=original_year, authors_fn=authors,
601                 dry_run=False, verbose=0):
602     f = file(filename, 'r')
603     contents = f.read()
604     f.close()
605
606     original_year = original_year_fn(filename)
607     authors = authors_fn(filename)
608     authors = _replace_aliases(authors, with_email=True, aliases=ALIASES)
609
610     new_contents = _update_copyright(contents, original_year, authors)
611     _set_contents(filename, contents=new_contents, original_contents=contents,
612                   dry_run=dry_run, verbose=verbose)
613
614 def update_files(files=None, dry_run=False, verbose=0):
615     if files == None or len(files) == 0:
616         files = []
617         for dirpath,dirnames,filenames in os.walk('.'):
618             for filename in filenames:
619                 files.append(os.path.join(dirpath, filename))
620
621     for filename in files:
622         if ignored_file(filename) == True:
623             continue
624         update_file(filename, dry_run=dry_run, verbose=verbose)
625
626 def test():
627     import doctest
628     doctest.testmod() 
629
630 if __name__ == '__main__':
631     import optparse
632     import sys
633
634     usage = """%%prog [options] [file ...]
635
636 Update copyright information in source code with information from
637 the %(vcs)s repository.  Run from the %(project)s repository root.
638
639 Replaces every line starting with '^# Copyright' and continuing with
640 '^#' with an auto-generated copyright blurb.  If you want to add
641 #-commented material after a copyright blurb, please insert a blank
642 line between the blurb and your comment, so the next run of
643 ``update_copyright.py`` doesn't clobber your comment.
644
645 If no files are given, a list of files to update is generated
646 automatically.
647 """ % PROJECT_INFO
648     p = optparse.OptionParser(usage)
649     p.add_option('--test', dest='test', default=False,
650                  action='store_true', help='Run internal tests and exit')
651     p.add_option('--dry-run', dest='dry_run', default=False,
652                  action='store_true', help="Don't make any changes")
653     p.add_option('-v', '--verbose', dest='verbose', default=0,
654                  action='count', help='Increment verbosity')
655     options,args = p.parse_args()
656
657     if options.test == True:
658         test()
659         sys.exit(0)
660
661     update_authors(dry_run=options.dry_run, verbose=options.verbose)
662     update_files(files=args, dry_run=options.dry_run, verbose=options.verbose)