X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=update_copyright.py;h=a2dd25204578dab620923154a7a71cfddc5981c0;hp=699a155fc33ca68d4b4ba7ee323f3b69547068b3;hb=bef08c58d0c0e2482c8aa65def15f80875b78e5d;hpb=5b597a53f1ab7611e01aa8332df6b405a57ac4c5 diff --git a/update_copyright.py b/update_copyright.py old mode 100644 new mode 100755 index 699a155..a2dd252 --- a/update_copyright.py +++ b/update_copyright.py @@ -1,6 +1,22 @@ #!/usr/bin/python # -# COPYRIGHT +# Copyright (C) 2010 W. Trevor King +# +# This file is part of Hooke. +# +# Hooke is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Hooke is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General +# Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Hooke. If not, see +# . """Automatically update copyright boilerplate. @@ -13,14 +29,10 @@ import difflib import email.utils import os import os.path -import re -import StringIO import sys +import textwrap import time -import mercurial -import mercurial.dispatch - PROJECT_INFO = { 'project': 'Hooke', @@ -28,90 +40,321 @@ PROJECT_INFO = { } # Break "copyright" into "copy" and "right" to avoid matching the -# REGEXP. -COPY_RIGHT_TEXT=""" -This file is part of %(project)s. - -%(project)s is free software: you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation, either -version 3 of the License, or (at your option) any later version. - -%(project)s is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with %(project)s. If not, see -. -""".strip() - -COPY_RIGHT_TAG='-xyz-COPY-RIGHT-zyx-' # unlikely to occur in the wild :p - +# REGEXP if we decide to go back to regexps. +COPY_RIGHT_TEXT = [ + 'This file is part of %(project)s.', + '%(project)s is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.', + '%(project)s is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.', + 'You should have received a copy of the GNU Lesser General Public License along with %(project)s. If not, see .' + ] + +SHORT_COPY_RIGHT_TEXT = [ + '%(project)s comes with ABSOLUTELY NO WARRANTY and is licensed under the GNU Lesser General Public License. For details, %(get-details)s.' + ] + +COPY_RIGHT_TAG='-xyz-COPY' + '-RIGHT-zyx-' # unlikely to occur in the wild :p + +# Convert author names to canonical forms. +# ALIASES[] = +# for example, +# ALIASES = { +# 'John Doe ': +# ['John Doe', 'jdoe', 'J. Doe '], +# } +# Git-based projects are encouraged to use .mailmap instead of +# ALIASES. See git-shortlog(1) for details. ALIASES = { + 'A. Seeholzer': + ['A. Seeholzer'], 'Alberto Gomez-Casado': ['albertogomcas'], 'Massimo Sandal ': - ['devicerandom', + ['Massimo Sandal', + 'devicerandom', 'unknown'], - 'Fabrizio Benedetti':['fabrizio.benedetti'], - 'il':['illysam'], - 'Marco Brucale':['marcobrucale'], - 'pp':['pancaldi.paolo'], + 'Fabrizio Benedetti': + ['fabrizio.benedetti.82'], + 'Richard Naud ': + ['Richard Naud'], + 'Rolf Schmidt ': + ['Rolf Schmidt', + 'illysam'], + 'Marco Brucale': + ['marcobrucale'], + 'Pancaldi Paolo': + ['pancaldi.paolo'], } -IGNORED_PATHS = ['./.hg/', './doc/img', './test/data/', - './build/', '/doc/build/'] +# List of paths that should not be scanned for copyright updates. +# IGNORED_PATHS = ['./.git/'] +IGNORED_PATHS = ['./.hg/', './doc/img/', './test/data/', + './build/', './doc/build/'] +# List of files that should not be scanned for copyright updates. +# IGNORED_FILES = ['COPYING'] IGNORED_FILES = ['COPYING', 'COPYING.LESSER'] +# Work around missing author holes in the VCS history. +# AUTHOR_HACKS[] = [] = +# for example, if module.py was published in 2008 but the VCS history +# only goes back to 2010: +# YEAR_HACKS = { +# ('path', 'to', 'module.py'):2008, +# } +YEAR_HACKS = { + ('hooke','driver','hdf5.py'):2009, + ('hooke','driver','mfp3d.py'):2008, + ('hooke','driver','picoforce.py'):2006, + ('hooke','driver','picoforcealt.py'):2006, + ('hooke','util','peak.py'):2007, + ('hooke','plugin','showconvoluted.py'):2009, + ('hooke','plugin','tutorial.py'):2007, + ('hooke','ui','gui','formatter.py'):2006, + ('hooke','ui','gui','prettyformat.py'):2009, + } + +# Helpers for VCS-specific commands + +def splitpath(path): + """Recursively split a path into elements. + + Examples + -------- + + >>> splitpath(os.path.join('a', 'b', 'c')) + ('a', 'b', 'c') + >>> splitpath(os.path.join('.', 'a', 'b', 'c')) + ('a', 'b', 'c') + """ + path = os.path.normpath(path) + elements = [] + while True: + dirname,basename = os.path.split(path) + elements.insert(0,basename) + if dirname in ['', '.']: + break + path = dirname + return tuple(elements) # VCS-specific commands -def mercurial_cmd(*args): - cwd = os.getcwd() - stdout = sys.stdout - stderr = sys.stderr - tmp_stdout = StringIO.StringIO() - tmp_stderr = StringIO.StringIO() - sys.stdout = tmp_stdout - sys.stderr = tmp_stderr - try: - mercurial.dispatch.dispatch(list(args)) - finally: - os.chdir(cwd) - sys.stdout = stdout - sys.stderr = stderr - return (tmp_stdout.getvalue().rstrip('\n'), - tmp_stderr.getvalue().rstrip('\n')) +if PROJECT_INFO['vcs'] == 'Git': + + import subprocess + + _MSWINDOWS = sys.platform == 'win32' + _POSIX = not _MSWINDOWS + + def invoke(args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, expect=(0,)): + """ + expect should be a tuple of allowed exit codes. + """ + try : + if _POSIX: + q = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=stdout, stderr=stderr) + else: + assert _MSWINDOWS == True, 'invalid platform' + # win32 don't have os.execvp() so run the command in a shell + q = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=stdout, stderr=stderr, shell=True) + except OSError, e: + raise ValueError([args, e]) + stdout,stderr = q.communicate(input=stdin) + status = q.wait() + if status not in expect: + raise ValueError([args, status, stdout, stderr]) + return status, stdout, stderr + + def git_cmd(*args): + status,stdout,stderr = invoke(['git'] + list(args)) + return stdout.rstrip('\n') + + def original_year(filename=None, year_hacks=YEAR_HACKS): + args = [ + '--format=format:%ad', # Author date + '--date=short', # YYYY-MM-DD + ] + if filename != None: + args.extend(['--follow', filename]) + output = git_cmd('log', *args) + years = [int(line.split('-', 1)[0]) for line in output.splitlines()] + if filename == None: + years.extend(year_hacks.values()) + elif splitpath(filename) in year_hacks: + years.append(year_hacks[splitpath(filename)]) + years.sort() + return years[0] + + def authors(filename, author_hacks=AUTHOR_HACKS): + output = git_cmd('log', '--follow', '--format=format:%aN <%aE>', + filename) # Author name + ret = list(set(output.splitlines())) + if splitpath(filename) in author_hacks: + ret.extend(author_hacks[splitpath(filename)]) + return ret + + def authors_list(author_hacks=AUTHOR_HACKS): + output = git_cmd('log', '--format=format:%aN <%aE>') + ret = list(set(output.splitlines())) + for path,authors in author_hacks.items(): + ret.extend(authors) + return ret -def original_year(filename): - # shortdate filter: YEAR-MONTH-DAY - output,error = mercurial_cmd('log', '--follow', - '--template', '{date|shortdate}\n', - filename) - years = [int(line.split('-', 1)[0]) for line in output.splitlines()] - years.sort() - return years[0] - -def authors(filename): - output,error = mercurial_cmd('log', '--follow', - '--template', '{author}\n', - filename) - return list(set(output.splitlines())) - -def authors_list(): - output,error = mercurial_cmd('log', '--follow', - '--template', '{author}\n') - return list(set(output.splitlines())) - -def is_versioned(filename): - output,error = mercurial_cmd('log', '--follow', - '--template', '{date|shortdate}\n', - filename) - if len(error) > 0: - return False - return True + def is_versioned(filename): + output = git_cmd('log', '--follow', filename) + if len(output) == 0: + return False + return True + +elif PROJECT_INFO['vcs'] == 'Mercurial': + + import StringIO + import mercurial + import mercurial.dispatch + + def mercurial_cmd(*args): + cwd = os.getcwd() + stdout = sys.stdout + stderr = sys.stderr + tmp_stdout = StringIO.StringIO() + tmp_stderr = StringIO.StringIO() + sys.stdout = tmp_stdout + sys.stderr = tmp_stderr + try: + mercurial.dispatch.dispatch(list(args)) + finally: + os.chdir(cwd) + sys.stdout = stdout + sys.stderr = stderr + return (tmp_stdout.getvalue().rstrip('\n'), + tmp_stderr.getvalue().rstrip('\n')) + + def original_year(filename=None, year_hacks=YEAR_HACKS): + args = [ + '--template', '{date|shortdate}\n', + # shortdate filter: YEAR-MONTH-DAY + ] + if filename != None: + args.extend(['--follow', filename]) + output,error = mercurial_cmd('log', *args) + years = [int(line.split('-', 1)[0]) for line in output.splitlines()] + if filename == None: + years.extend(year_hacks.values()) + elif splitpath(filename) in year_hacks: + years.append(year_hacks[splitpath(filename)]) + years.sort() + return years[0] + + def authors(filename, author_hacks=AUTHOR_HACKS): + output,error = mercurial_cmd('log', '--follow', + '--template', '{author}\n', + filename) + ret = list(set(output.splitlines())) + if splitpath(filename) in author_hacks: + ret.extend(author_hacks[splitpath(filename)]) + return ret + + def authors_list(author_hacks=AUTHOR_HACKS): + output,error = mercurial_cmd('log', '--template', '{author}\n') + ret = list(set(output.splitlines())) + for path,authors in author_hacks.items(): + ret.extend(authors) + return ret + + def is_versioned(filename): + output,error = mercurial_cmd('log', '--follow', filename) + if len(error) > 0: + return False + return True + +elif PROJECT_INFO['vcs'] == 'Bazaar': + + import StringIO + import bzrlib + import bzrlib.builtins + import bzrlib.log + + class LogFormatter (bzrlib.log.LogFormatter): + supports_merge_revisions = True + preferred_levels = 0 + supports_deta = False + supports_tags = False + supports_diff = False + + def log_revision(self, revision): + raise NotImplementedError + + class YearLogFormatter (LogFormatter): + def log_revision(self, revision): + self.to_file.write( + time.strftime('%Y', time.gmtime(revision.rev.timestamp)) + +'\n') + + class AuthorLogFormatter (LogFormatter): + def log_revision(self, revision): + authors = revision.rev.get_apparent_authors() + self.to_file.write('\n'.join(authors)+'\n') + + def original_year(filename=None, year_hacks=YEAR_HACKS): + cmd = bzrlib.builtins.cmd_log() + cmd.outf = StringIO.StringIO() + kwargs = {'log_format':YearLogFormatter, 'levels':0} + if filename != None: + kwargs['file_list'] = [filenme] + cmd.run(**kwargs) + years = [int(year) for year in set(cmd.outf.getvalue().splitlines())] + if filename == None: + years.append(year_hacks.values()) + elif splitpath(filename) in year_hacks: + years.append(year_hacks[splitpath(filename)]) + years.sort() + return years[0] + + def authors(filename, author_hacks=AUTHOR_HACKS): + cmd = bzrlib.builtins.cmd_log() + cmd.outf = StringIO.StringIO() + cmd.run(file_list=[filename], log_format=AuthorLogFormatter, levels=0) + ret = list(set(cmd.outf.getvalue().splitlines())) + if splitpath(filename) in author_hacks: + ret.extend(author_hacks[splitpath(filename)]) + return ret + + def authors_list(author_hacks=AUTHOR_HACKS): + cmd = bzrlib.builtins.cmd_log() + cmd.outf = StringIO.StringIO() + cmd.run(log_format=AuthorLogFormatter, levels=0) + output = cmd.outf.getvalue() + ret = list(set(cmd.outf.getvalue().splitlines())) + for path,authors in author_hacks.items(): + ret.extend(authors) + return ret + + def is_versioned(filename): + cmd = bzrlib.builtins.cmd_log() + cmd.outf = StringIO.StringIO() + cmd.run(file_list=[filename]) + return True + +else: + raise NotImplementedError('Unrecognized VCS: %(vcs)s' % PROJECT_INFO) # General utility commands @@ -121,6 +364,8 @@ def _strip_email(*args): Examples -------- + >>> _strip_email('J Doe') + ['J Doe'] >>> _strip_email('J Doe ') ['J Doe'] >>> _strip_email('J Doe ', 'JJJ Smith ') @@ -131,6 +376,8 @@ def _strip_email(*args): if arg == None: continue author,addr = email.utils.parseaddr(arg) + if author == '': + author = arg args[i] = author return args @@ -174,23 +421,16 @@ def _replace_aliases(authors, with_email=True, aliases=None): ... 'JJJ Smith ':['Jingly '], ... None:['Anonymous '], ... } - >>> _replace_aliases(['JJJ Smith ', 'Johnny ', - ... 'Jingly ', 'Anonymous '], - ... with_email=True, aliases=aliases) + >>> authors = [ + ... 'JJJ Smith ', 'Johnny ', + ... 'Jingly ', 'J Doe ', 'Anonymous '] + >>> _replace_aliases(authors, with_email=True, aliases=aliases) ['J Doe ', 'JJJ Smith '] - >>> _replace_aliases(['JJJ Smith', 'Johnny', 'Jingly', 'Anonymous'], - ... with_email=False, aliases=aliases) + >>> _replace_aliases(authors, with_email=False, aliases=aliases) ['J Doe', 'JJJ Smith'] - >>> _replace_aliases(['JJJ Smith ', 'Johnny ', - ... 'Jingly ', 'J Doe '], - ... with_email=True, aliases=aliases) - ['J Doe ', 'JJJ Smith '] """ if aliases == None: aliases = ALIASES - if with_email == False: - aliases = dict([(_strip_email(author)[0], _strip_email(*_aliases)) - for author,_aliases in aliases.items()]) rev_aliases = _reverse_aliases(aliases) for i,author in enumerate(authors): if author in rev_aliases: @@ -198,9 +438,39 @@ def _replace_aliases(authors, with_email=True, aliases=None): authors = sorted(list(set(authors))) if None in authors: authors.remove(None) + if with_email == False: + authors = _strip_email(*authors) return authors -def _copyright_string(original_year, final_year, authors, prefix=''): +def _long_author_formatter(copyright_year_string, authors): + """ + >>> print '\\n'.join(_long_author_formatter( + ... copyright_year_string='Copyright (C) 1990-2010', + ... authors=['Jack', 'Jill', 'John'])) + Copyright (C) 1990-2010 Jack + Jill + John + """ + lines = ['%s %s' % (copyright_year_string, authors[0])] + for author in authors[1:]: + lines.append(' '*(len(copyright_year_string)+1) + author) + return lines + +def _short_author_formatter(copyright_year_string, authors): + """ + >>> print '\\n'.join(_short_author_formatter( + ... copyright_year_string='Copyright (C) 1990-2010', + ... authors=['Jack', 'Jill', 'John']*5)) + Copyright (C) 1990-2010 Jack, Jill, John, Jack, Jill, John, Jack, Jill, John, Jack, Jill, John, Jack, Jill, John + """ + blurb = '%s %s' % (copyright_year_string, ', '.join(authors)) + return [blurb] + +def _copyright_string(original_year, final_year, authors, + text=COPY_RIGHT_TEXT, extra_info={}, + author_format_fn=_long_author_formatter, + formatter_kwargs={}, prefix='', wrap=True, + **wrap_kwargs): """ >>> print _copyright_string(original_year=2005, ... final_year=2005, @@ -219,20 +489,60 @@ def _copyright_string(original_year, final_year, authors, prefix=''): B This file... + >>> print _copyright_string(original_year=2005, + ... final_year=2005, + ... authors=['A ', 'B '], + ... text=SHORT_COPY_RIGHT_TEXT, + ... author_format_fn=_short_author_formatter, + ... extra_info={'get-details':'%(get-details)s'}, + ... prefix='', + ... width=50, + ... ) + Copyright (C) 2005 A , B + + Hooke comes with ABSOLUTELY NO WARRANTY and is + licensed under the GNU Lesser General Public + License. For details, %(get-details)s. + >>> print _copyright_string(original_year=2005, + ... final_year=2005, + ... authors=['A ', 'B '], + ... text=SHORT_COPY_RIGHT_TEXT, + ... extra_info={'get-details':'%(get-details)s'}, + ... author_format_fn=_short_author_formatter, + ... wrap=False, + ... prefix='', + ... ) + Copyright (C) 2005 A , B + + Hooke comes with ABSOLUTELY NO WARRANTY and is licensed under the GNU Lesser General Public License. For details, %(get-details)s. """ + for key in ['initial_indent', 'subsequent_indent']: + if key not in wrap_kwargs: + wrap_kwargs[key] = prefix + if original_year == final_year: date_range = '%s' % original_year else: date_range = '%s-%s' % (original_year, final_year) - lines = ['Copyright (C) %s %s' % (date_range, authors[0])] - for author in authors[1:]: - lines.append(' '*(len('Copyright (C) ')+len(date_range)+1) + - author) - lines.append('') - lines.extend((COPY_RIGHT_TEXT % PROJECT_INFO).splitlines()) + copyright_year_string = 'Copyright (C) %s' % date_range + + lines = author_format_fn(copyright_year_string, authors, + **formatter_kwargs) for i,line in enumerate(lines): - lines[i] = (prefix + line).rstrip() - return '\n'.join(lines) + lines[i] = prefix + line + + info = dict(PROJECT_INFO) + for key,value in extra_info.items(): + info[key] = value + text = [paragraph % info for paragraph in text] + + if wrap == True: + text = [textwrap.fill(p, **wrap_kwargs) for p in text] + else: + assert wrap_kwargs['subsequent_indent'] == '', \ + wrap_kwargs['subsequent_indent'] + sep = '\n%s\n' % prefix.rstrip() + return sep.join(['\n'.join(lines)] + text) def _tag_copyright(contents): """ @@ -244,12 +554,13 @@ def _tag_copyright(contents): ... (copyright ends) ... bla bla bla ... ''' - >>> print _tag_copyright(contents), + >>> print _tag_copyright(contents).replace('COPY-RIGHT', 'CR') Some file bla bla - -xyz-COPY-RIGHT-zyx- + -xyz-CR-zyx- (copyright ends) bla bla bla + """ lines = [] incopy = False @@ -291,33 +602,34 @@ def _update_copyright(contents, original_year, authors): contents = _tag_copyright(contents) return contents.replace(COPY_RIGHT_TAG, copyright_string) -def ignored_file(filename, ignored_paths=None, ignored_files=None): +def ignored_file(filename, ignored_paths=None, ignored_files=None, + check_disk=True, check_vcs=True): """ >>> ignored_paths = ['./a/', './b/'] >>> ignored_files = ['x', 'y'] - >>> ignored_file('./a/z', ignored_paths, ignored_files) + >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False) True - >>> ignored_file('./ab/z', ignored_paths, ignored_files) + >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False) False - >>> ignored_file('./ab/x', ignored_paths, ignored_files) + >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False) True - >>> ignored_file('./ab/xy', ignored_paths, ignored_files) + >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False) False - >>> ignored_file('./z', ignored_paths, ignored_files) + >>> ignored_file('./z', ignored_paths, ignored_files, False, False) False """ if ignored_paths == None: ignored_paths = IGNORED_PATHS if ignored_files == None: ignored_files = IGNORED_FILES - if os.path.isfile(filename) == False: + if check_disk == True and os.path.isfile(filename) == False: return True for path in ignored_paths: if filename.startswith(path): return True if os.path.basename(filename) in ignored_files: return True - if is_versioned(filename) == False: + if check_vcs == True and is_versioned(filename) == False: return True return False @@ -352,9 +664,11 @@ def _set_contents(filename, contents, original_contents=None, dry_run=False, # Update commands def update_authors(authors_fn=authors_list, dry_run=False, verbose=0): + authors = authors_fn() + authors = _replace_aliases(authors, with_email=True, aliases=ALIASES) new_contents = '%s was written by:\n%s\n' % ( PROJECT_INFO['project'], - '\n'.join(authors_fn()) + '\n'.join(authors) ) _set_contents('AUTHORS', new_contents, dry_run=dry_run, verbose=verbose) @@ -384,6 +698,41 @@ def update_files(files=None, dry_run=False, verbose=0): continue update_file(filename, dry_run=dry_run, verbose=verbose) +def update_pyfile(path, original_year_fn=original_year, + authors_fn=authors_list, dry_run=False, verbose=0): + original_year = original_year_fn() + current_year = time.gmtime()[0] + authors = authors_fn() + authors = _replace_aliases(authors, with_email=False, aliases=ALIASES) + paragraphs = _copyright_string( + original_year, current_year, authors, + text=SHORT_COPY_RIGHT_TEXT, + extra_info={'get-details':'%(get-details)s'}, + author_format_fn=_short_author_formatter, wrap=False, + ).split('\n\n') + lines = [ + _copyright_string(original_year, current_year, authors, prefix='# '), + '', 'import textwrap', '', '', + 'LICENSE = """', + _copyright_string(original_year, current_year, authors, prefix=''), + '""".strip()', + '', + 'def short_license(extra_info, wrap=True, **kwargs):', + ' paragraphs = [', + ] + for p in paragraphs: + lines.append(" '%s' %% extra_info," % p.replace("'", r"\'")) + lines.extend([ + ' ]', + ' if wrap == True:', + ' for i,p in enumerate(paragraphs):', + ' paragraphs[i] = textwrap.fill(p, **kwargs)', + r" return '\n\n'.join(paragraphs)", + ]) + new_contents = '\n'.join(lines)+'\n' + _set_contents(path, new_contents, dry_run=dry_run, verbose=verbose) + + def test(): import doctest doctest.testmod() @@ -407,6 +756,9 @@ If no files are given, a list of files to update is generated automatically. """ % PROJECT_INFO p = optparse.OptionParser(usage) + p.add_option('--pyfile', dest='pyfile', default='hooke/license.py', + metavar='PATH', + help='Write project license info to a Python module at PATH') p.add_option('--test', dest='test', default=False, action='store_true', help='Run internal tests and exit') p.add_option('--dry-run', dest='dry_run', default=False, @@ -421,3 +773,6 @@ automatically. update_authors(dry_run=options.dry_run, verbose=options.verbose) update_files(files=args, dry_run=options.dry_run, verbose=options.verbose) + if options.pyfile != None: + update_pyfile(path=options.pyfile, + dry_run=options.dry_run, verbose=options.verbose)