Newer
Older

Nathan Hertz
committed
import setuptools, importlib, subprocess, os, re
def write_metafile(metadata, filepath):
"""
Writes given metadata to file with given path.
"""

Nathan Hertz
committed
try:
os.makedirs(filepath[:-10])

Nathan Hertz
committed
except FileExistsError:

Nathan Hertz
committed
pass

Nathan Hertz
committed
with open(filepath, 'w') as f:

Nathan Hertz
committed
f.write(metadata)
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def generate_metadata(setup_dict, path):
"""
Uses given info extracted from setup.py file to fill out metadata template.
:param setup_dict: Dictionary of info extracted from setup.py.
:param path: Path to root directory of the subproject.
:return: String of metadata, ready to be written to a file.
"""
def fmt_ep():
"""
Format entry points section of metadata.
:return: Formatted string if entry points exists; else empty string.
"""
ep_string = ''
if 'entry_points' in setup_dict.keys():
ep_string += 'entry_points:\n'
for ep in setup_dict['entry_points']:
ep_string += ' - {}\n'.format(ep)
return ep_string
def fmt_reqs():
"""
Format requirements section of metadata.
:return: Formatted string if requirements exists; else empty string.
"""
reqs_string = ''
if 'install_requires' in setup_dict.keys():
reqs_string += 'requirements:\n'
build_reqs = ' build:\n'
run_reqs = ' run:\n'
for req in setup_dict['install_requires'].split(', '):
build_reqs += ' - {}\n'.format(req)
run_reqs += ' - {}\n'.format(req)
reqs_string += build_reqs + run_reqs
return reqs_string
def fmt_test():
"""
Format test section of metadata.
:return: Formatted string if tests_require exists; else empty string.
"""
test_string = ''
if 'tests_require' in setup_dict.keys():
test_string += (
'test:\n'
' source_files:\n'
' - test/\n'
' requires:\n'
)
for req in setup_dict['tests_require'].split(', '):
test_string += ' - {}\n'.format(req)
test_string += (
' commands:\n'
' - pytest -vv --log-level=DEBUG --showlocals\n'
)
return test_string
name = setup_dict['name']
version = setup_dict['version']
entry_points = fmt_ep()
path = path.replace("./", "")
requirements = fmt_reqs()
test = fmt_test()
license = setup_dict['license']
summary = setup_dict['description']
with open('tools/metafile_template.txt', 'r') as f:
metadata = f.read()
return metadata.format(
name = name,
version = version,
entry_points = entry_points,
path = path,
requirements = requirements,
test = test,
license = license,
summary = summary

Nathan Hertz
committed
)

Nathan Hertz
committed
def data_to_dict(setup_data):
"""
Translates parsed setup.py data from unformatted list to formatted dictionary.
:param setup_data: Data extracted from setup.py using parse_setup.py
:return: Dictionary of formatted data.
"""

Nathan Hertz
committed
data_dict = {}
r_entry = r"([a-z-_]+): "
r_id = r"[a-zA-Z0-9-_]"

Nathan Hertz
committed
r_ep = r"{{(?:[a-zA-Z0-9-_.]+):\s*(?P<values>(?:{0}+\s*=\s*[a-zA-Z0-9-_.]+:{0}+(?:,\s*)?)*)}}".format(r_id)

Nathan Hertz
committed
for d in setup_data:
result = re.match(r_entry, d)
if result:
substrings = [result.group(0), '[', ']', '\'', '"']
field = result.group(1)
value = del_substrings(d, substrings)
if value == "None":
continue
if re.match(r_ep, value):
# Parse entry points
value = re.match(r_ep, value).group('values')
value = re.split(',\s*', value)
data_dict[field] = value
return data_dict
def parse_setup(d):
"""
Function for running parse_setup.py on each directory with a setup.py file.
NOTE: Contains a hack for getting parse_setup.py to run in each directory.
:param d: Directory with a setup.py file.
:return: Data collected from parse_setup.py.
"""
subprocess.run(['cp', 'tools/parse_setup.py', d])
os.chdir(d)
proc = subprocess.run(['python3', 'parse_setup.py'], stdout=subprocess.PIPE)
setup_data = (proc.stdout.decode('utf-8')).split('\n')
os.chdir(root)
subprocess.run(['rm', '{}/parse_setup.py'.format(d)])
return setup_data

Nathan Hertz
committed
def get_outputs(names):
"""
Generate list of metadata files that will be created.
:param names: List of names of all subprojects with a setup.py file.
:return: List of paths to output files as strings.
"""

Nathan Hertz
committed
outputs = []
for name in names:
outputs.append("metadata/{}/meta.yaml".format(name))

Nathan Hertz
committed
return outputs

Nathan Hertz
committed
def get_dirs():
"""
Finds all subdirectories containing setup.py files.
:return: List of directories as strings.
"""
find = subprocess.run([
'find', '.', '-name', 'setup.py', '-not', '-path', './src/*'
], stdout=subprocess.PIPE)

Nathan Hertz
committed
dirs = find.stdout.decode('utf-8').split('\n')

Nathan Hertz
committed
for i, d in enumerate(dirs_cpy):

Nathan Hertz
committed
dirs[i] = d.replace('/setup.py', '')
return dirs
def get_names(dirs):
"""
Generate list of subproject names based on the rule that the name of the
subproject directory will be the name of the subproject.
:return: List of names as strings.
"""

Nathan Hertz
committed
names = []
for d in dirs:
if d != '':

Nathan Hertz
committed
name = d.split('/')[-1]
if name == "archive":
# Case with ./services/archive having special dir structure
name = "services"
names.append(name)

Nathan Hertz
committed
return names
def del_substrings(s, substrings):
"""
Function for deleting multiple substrings from a string.
:param s: String to be modified.
:param substrings: List of substrings to be targeted for deletion.
:return: Modified string.
"""

Nathan Hertz
committed
for replace in substrings:
s = s.replace(replace, '')
return s
root = os.getcwd()

Nathan Hertz
committed
class Recipe:
"""
Buildout Recipe class.
For more detailed information, see the link.
http://www.buildout.org/en/latest/topics/writing-recipes.html
"""

Nathan Hertz
committed
def __init__(self, buildout, name, options):
"""
Initializes fields needed for recipe.
:param buildout: (Boilerplate) Dictionary of options from buildout section
of buildout.cfg
:param name: (Boilerplate) Name of section that uses this recipe.
:param options: (Boilerplate) Options of section that uses this recipe.
"""

Nathan Hertz
committed
self.dirs = get_dirs()
self.names = get_names(self.dirs)

Nathan Hertz
committed
self.outputs = get_outputs(self.names)

Nathan Hertz
committed
self.options = options

Nathan Hertz
committed
# TODO: Keep track of path in setup_dict

Nathan Hertz
committed
def install(self):
"""
Install method that runs when recipe has components it needs to install.
:return: Paths to files, as strings, created by the recipe.
"""

Nathan Hertz
committed
for i, d in enumerate(self.dirs):

Nathan Hertz
committed
if d != '':
setup_data = parse_setup(d)

Nathan Hertz
committed
data_dict = data_to_dict(setup_data)
metadata = generate_metadata(data_dict, d)

Nathan Hertz
committed
write_metafile(metadata, self.outputs[i])
# Pass created file into options.created()

Nathan Hertz
committed
self.options.created(self.outputs[i])

Nathan Hertz
committed
return self.options.created()
# No special procedure for updating vs. installing

Nathan Hertz
committed
update = install