credits.py
#!/usr/bin/env python3
# coding: utf-8
"""
Recursively searches the input directory for 'LICENSE.*' files and compiles
them into a Settings.bundle friendly plist. Inspired by JosephH
and Sean's comments on stackoverflow: http://stackoverflow.com/q/6428353
:usage: ./credits.py -s project/ -o project/Settings.bundle/Credits.plist
:author: Carlo Eugster (http://carlo.io)
:license: MIT, see LICENSE for more details.
"""
import os
import sys
import plistlib
import re
import codecs
from optparse import OptionParser
from optparse import Option, OptionValueError
from copy import deepcopy
VERSION = '0.11.0'
PROG = os.path.basename(os.path.splitext(__file__)[0])
DESCRIPTION = """Generate a `Settings.bundle` friendly plist file from all
'LICENSE.*' files in a given directory. Inspired by JosephH and Sean's
comments on stackoverflow: http://stackoverflow.com/q/6428353"""
class MultipleOption(Option):
ACTIONS = Option.ACTIONS + ("extend",)
STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",)
TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",)
ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",)
def take_action(self, action, dest, opt, value, values, parser):
if action == "extend":
values.ensure_value(dest, []).append(value)
else:
Option.take_action(self, action, dest, opt, value, values, parser)
def main(_):
def list_callback(option, _, value, option_parser):
values = [item.strip() for item in value.split(',')]
setattr(option_parser.values, option.dest, values)
parser = OptionParser(option_class=MultipleOption,
usage='usage: %prog -s source_path -o output_plist -e [exclude_paths]',
version='%s %s' % (PROG, VERSION),
description=DESCRIPTION)
parser.add_option('-s', '--source',
action="callback", type="string",
dest='input_path',
metavar='source_path',
help='comma separated list of directories to \
recursively search for licenses',
callback=list_callback)
parser.add_option('-o', '--output-plist',
type="string",
dest='output_file',
metavar='output_plist',
help='path to the plist to be generated')
parser.add_option('-e', '--exclude',
action="callback", type="string",
dest='excludes',
metavar='path1, ...',
help='comma separated list of paths to be excluded',
callback=list_callback)
parser.add_option('-t', '--test',
action="store_true",
dest='include_tests',
metavar='include_tests',
default=False,
help='include files in the `Tests` directory for unit testing')
if len(sys.argv) == 1:
parser.parse_args(['--help'])
options, args = parser.parse_args()
for path in options.input_path:
if(not os.path.isdir(path)):
print("Error: Source path does not exist: %s" % path)
sys.exit(2)
if not options.output_file.endswith('.plist'):
print("Error: Outputfile must end in .plist")
sys.exit(2)
plist = plist_from_dirs(
options.input_path,
options.excludes,
options.include_tests
)
with open(options.output_file, 'wb') as f:
plistlib.dump(plist, f)
return 0
def plist_from_dirs(directories, excludes, include_tests):
"""
Recursively searches each directory in 'directories' and
generates plist objects from any LICENSE files found.
"""
plist = {'PreferenceSpecifiers': [], 'StringsTable': 'Acknowledgements'}
for directory in directories:
license_paths = license_paths_from_dir(directory)
plist_paths = [plist_path for plist_path in license_paths if not exclude_path(directory, plist_path, excludes, include_tests)]
for plist_path in plist_paths:
license_dict = plist_from_file(plist_path)
plist['PreferenceSpecifiers'].append(license_dict)
plist['PreferenceSpecifiers'] = sorted(plist['PreferenceSpecifiers'], key=lambda x: x['Title'])
return plist
def license_paths_from_dir(directory):
return_dict = []
os.chdir(sys.path[0])
for dir_path, _, file_names in os.walk(directory):
file_names = (file_name for file_name in file_names if file_name.startswith("LICENSE"))
for file_name in file_names:
return_dict.append(os.path.join(dir_path, file_name))
return return_dict
def plist_from_file(path):
"""
Returns a plist representation of the file at 'path'. Uses the name of the
parent folder for the title property.
"""
base_group = {'Type': 'PSGroupSpecifier', 'FooterText': '', 'Title': ''}
current_file = open(path, 'r')
group = deepcopy(base_group)
title = path.split("/")[-2]
group['Title'] = title
src_body = current_file.read()
body = ""
for match in re.finditer(r'(?s)((?:[^\n][\n]?)+)', src_body):
body = body + re.sub("(\\n)", " ", match.group()) + "\n\n"
body = body
group['FooterText'] = rchop(body, " \n\n")
return group
def exclude_path(source_path, plist_path, excludes, is_testing):
if "/LicenseGenerator-iOS/Example/" in plist_path and '/LicenseGenerator-iOS/Example/Pods' not in source_path:
return True
elif "/LicenseGenerator-iOS/Tests/" in plist_path:
return not is_testing
elif excludes is None:
return False
for pattern in excludes:
if re.search(pattern.strip(), plist_path, re.S) is not None:
return True
return False
def rchop(str, ending):
if str.endswith(ending):
return str[:-len(ending)]
return str
if __name__ == "__main__":
main(sys.argv[1:])