[Zope-Checkins] CVS: Zope3/utilities - license_check.py:1.1.2.1

Janko Hauser jh@comunit.de
Mon, 1 Apr 2002 06:53:24 -0500


Update of /cvs-repository/Zope3/utilities
In directory cvs.zope.org:/tmp/cvs-serv14716

Added Files:
      Tag: Zope-3x-branch
	license_check.py 
Log Message:
Script to change the license of all Python files according to zpl.py



=== Added File Zope3/utilities/license_check.py ===
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""

$Id: license_check.py,v 1.1.2.1 2002/04/01 11:53:23 janko Exp $ 
"""

# small script to check all Python files for a correct header,
# specifically a presence of the correct license text from
# $ZOPE_HOME/zpl.py

usage = """\
%s looks for the presence of the right license text in
all Python files. It can be run in different modes.

usage: %s -c|-w|--undo [-s, -v, --nobackup] path 

-c : put a license text at the top of each file
-w : print the names of files without a license text
-s : strict checking. If this option is not set, only handle files
     without any form of license text. If set, an exact license text
     needs to be present
-v : be verbose
-h : Print this help

-l=path : Give the absolute path to zpl.py

--nobackup : Do not generate backup files
--undo : Rename the backup files to the original files, mainly for
         testing.

path : If not given start in the current directory. 
"""


import os, fnmatch, sys
import re

class ZLicenseCheckError(Exception):
    fname = ''
    msg = ''
    def __init__(self, msg, fname):
        self.msg = msg
        self.fname = fname
        Exception.__init__(self, msg, fname)

    def __str__(self):
        return self.msg
 
class GlobDirectoryWalker:
    # a forward iterator that traverses a directory tree
    # snippet posted by Frederik Lundh in c.l.p.
    #
    def __init__(self, directory, pattern="*"):
        self.stack = [directory]
        self.pattern = pattern
        self.files = []
        self.index = 0


    def __getitem__(self, index):
        while 1:
            try:
                file = self.files[self.index]
                self.index = self.index + 1
            except IndexError:
                # pop next directory from stack
                self.directory = self.stack.pop()
                self.files = os.listdir(self.directory)
                self.index = 0
            else:
                # got a filename
                fullname = os.path.join(self.directory, file)
                if os.path.isdir(fullname) and not os.path.islink(fullname):
                    self.stack.append(fullname)
                if fnmatch.fnmatch(file, self.pattern):
                    return fullname

class HeaderCheck:
    """Make the header of a given file have a license text."""

    def __init__(self, fname, zpl, verbose=0, backup=1):
	"""\
	fname -> name of the checked file
	zpl   -> instance of ZPL, class representing the license
	"""
	self.fname = fname
	self.header_length = 700
	self.license = zpl.license_text
	self.verbose = verbose
	self.backup = backup
    

    def get_header(self):
	"""Get a number if lines of the file. The number is an
	instance attribute"""
        header = open(self.fname,'r').read(self.header_length)
        return header
	

    def has_some_license(self):
	"""Search the file for some license text. If the text is
	found, return 1"""
	header = self.get_header()
        if not re.search("Copyright", header, re.I):
	    return 0
	else:
	    return 1


    def has_license(self):
	"""Fast check for the exact license text in the header"""
        header = self.get_header()
	if header.find(self.license) == -1:
	    return 0
        else:
	    return 1
	    

    def include(self):
	"""Put a license text at the top of the lines. If the first line
	starts with a bang-path start inserting at the second line"""
	lines = open(self.fname,'r').readlines()
	start = 0
	if lines and re.match('#!', lines[0]):
	    start=1
	lines.insert(start, self.license)
	# There can already be a backup file from the removal pass
	if self.backup and not os.path.isfile(self.fname+'.changed'):
	    os.rename(self.fname, self.fname+'.changed')
	open(self.fname, 'w').write(''.join(lines))
	if self.verbose:
	    print 'License included: %s' % self.fname
        return 
 

    def change(self):
	"""Try to change the license text in the file, raise an exception
	if not possible. 
	"""
	if self.has_some_license():
	    # try to remove the old license
	    try:
		self.remove()
		self.include()
	    except ZLicenseCheckError:
	        open(self.fname+'.pathological','w')
                raise ZLicenseCheckError('License could not be changed',
                                         self.fname)

	else:
	    self.include()
	return


    def remove(self):
	lines = open(self.fname, 'r').readlines()
	if not lines:
	    return
	start = 0
	save = []
	if re.match('#!',lines[0]):
	    start = 1
	    save.extend(lines[0])
	end=start
	for line in lines[start:]:
	    if line[0] == '#' or line.isspace():
	        end += 1
	    else:
		break

	license = ''.join(lines[start:end])
	# test if we really have the license
	lookfor = 'copyright|Zope Public|license|All rights reserved'
	if not re.search(lookfor, license, re.I):
	    raise ZLicenseCheckError('No clear license text', self.fname)
	else:
	    save.extend(lines[end:])
	    # keep the current stat_mod
	    fmode = os.stat(self.fname)[0]
	    if self.backup:
		os.rename(self.fname, self.fname+'.changed')
	    if self.verbose:
		print 'License removed: %s' % self.fname
            open(self.fname,'w').write(''.join(save))
	    os.chmod(self.fname, fmode)
	return
	

    def warn(self):
	print 'File %s has no license text' % self.fname

class Config:
    """Container to keep configuration options"""
    def __init__(self, **kws):
	self.verbose = 0
	self.warning = 0
	self.strict = 0
	self.backup = 1
	self.undo = 0
	self.include_init = 0
        self.license_path='./'

	for key,value in kws:
	    setattr(self, key, value)

	return


class ZPL:    
    def __init__(self, path='./'):
	self.path = path
	self.license_text = self.get_text()

    def get_text(self):
	# overdone
	zhome = os.environ.get('ZOPE_HOME', self.path)
	try:
	    data = open(os.path.join(zhome,'zpl.py'),'r').read()
	except IOError:
	    sys.exit('Could not open license file zpl.py')

	license = data[:data.find('"""')]
	return license


class CheckerApp:
    def __init__(self, config):
	self.conf = config
	self.zpl = ZPL()
	self.pathological = []
	
	# Which test (uses unbound methods)
	if self.conf.strict:
	    self.condition = HeaderCheck.has_license
	else:
	    self.condition = HeaderCheck.has_some_license

	# Wich action
        if self.conf.warning:
	    self.action = HeaderCheck.warn
	else:
	    if self.conf.strict:
		self.action = HeaderCheck.change
	    else:
		self.action = HeaderCheck.include
	    
    def run(self):
	if self.conf.undo:
	    for fname in GlobDirectoryWalker(self.conf.path,"*.changed"):
		old_name = os.path.splitext(fname)[0]
		os.rename(fname, old_name)
        else:
	    for fname in GlobDirectoryWalker(".", "*.py"):
		if (os.path.split(fname)[-1] == '__init__.py') and \
		        not self.conf.include_init:
                    pass

		hc = HeaderCheck(fname, self.zpl, 
				 verbose=self.conf.verbose,
				 backup=self.conf.backup)

		# unbound methods need an instance
		if not self.condition(hc):
		    try:
			self.action(hc)
                    except ZLicenseCheckError, error:
			print error, '(%s)' %error.fname
			self.pathological.append(fname)

        if self.conf.verbose:
	    for fname in self.pathological:
		print 'Could not be changed: %s' % fname
	    print 'Number of pathological files: %s' % \
		   len(self.pathological) 


def print_usage(msg=0):
    print usage % (sys.argv[0], sys.argv[0])
    if msg:
	print msg
	msg=1
    sys.exit(msg)

    
def main():
    import getopt
    try:
	opts, args = getopt.getopt(sys.argv[1:], "vwcshl:",
	["nobackup","undo","include_init"])
    except getopt.GetoptError, error:
	print_usage(str(error).capitalize())

    if not opts:
	print_usage('Need at least optin -w OR -c OR --undo')

    if (('-w','') in opts) and (('-c','') in opts):
	print_usage('Only option -w OR -c can be used')
	
    conf=Config()
    for o,a in opts:
	if o == '-v' : conf.verbose = 1
	elif o == '-c': conf.change = 1
	elif o == '-w': conf.warning = 1
	elif o == '-s': conf.strict = 1
	elif o == '-h': print_usage()
        elif o == '--include_init': conf.include_init = 1
        elif o == '--nobackup': conf.backup = 0
        elif o == '--undo': conf.undo = 1
        elif o == '-l':
	    if not a:
		print_usage('Need to get a path for option -l')
            conf.license_path = a

    if not args:
	conf.path = os.getcwd()
    else:
	conf.path = args[0]

    # test presence of working directory
    if not os.path.isdir(conf.path):
	print_usage('Can not find directory %s' % conf.path)
	
    checker = CheckerApp(conf)
    checker.run()
    
    
if __name__ == '__main__':   
    main()