From f4506a16aa332e8f7de4c4d84f994650feb2c1e4 Mon Sep 17 00:00:00 2001
From: Daniel K Lyons <dlyons@nrao.edu>
Date: Wed, 12 Aug 2020 16:18:28 -0600
Subject: [PATCH] Refactor and remove numpy dependencies

 - Make communication between parse_setup and setup_to_meta JSON
 - Refactor generate_metadata() into class
 - Filter out numpy, because it is implied by Conda
---
 src/setup_to_meta/setup_to_meta.py | 115 ++++++++++++-----------------
 tools/parse_setup.py               |  26 +++----
 2 files changed, 60 insertions(+), 81 deletions(-)

diff --git a/src/setup_to_meta/setup_to_meta.py b/src/setup_to_meta/setup_to_meta.py
index 9fc2826e6..af65485ab 100644
--- a/src/setup_to_meta/setup_to_meta.py
+++ b/src/setup_to_meta/setup_to_meta.py
@@ -1,3 +1,5 @@
+import json
+
 import setuptools, importlib, subprocess, os, re
 
 PYTHON_VERSION = '3.8'
@@ -14,46 +16,51 @@ def write_metafile(metadata, filepath):
     with open(filepath, 'w') as f:
         f.write(metadata)
 
-def generate_metadata(setup_dict, path):
+
+class MetadataGenerator:
     """
     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():
+
+    def __init__(self, setup, path):
+        self.setup = setup
+        self.path = path
+    
+    def fmt_ep(self):
         """
         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():
+        if 'entry_points' in self.setup.keys() and 'console_scripts' in self.setup['entry_points']:
             ep_string += 'entry_points:\n'
-            for ep in setup_dict['entry_points']:
+            for ep in self.setup['entry_points']['console_scripts']:
                 ep_string += '    - {}\n'.format(ep)
             ep_string += '  '
         return ep_string
-    def fmt_reqs():
+
+    def fmt_reqs(self):
         """
         Format requirements section of metadata.
         :return: Formatted string if requirements exists; else empty string.
         """
         reqs_string = ''
         reqs_list = ''
-        if 'install_requires' in setup_dict.keys():
+        if 'install_requires' in self.setup.keys():
             reqs_string += 'requirements:\n'
             build_reqs = '  build:\n'
             run_reqs = '  run:\n'
             host_reqs = '  host:\n'
             reqs_list += '    - python={}\n'.format(PYTHON_VERSION)
-            for req in setup_dict['install_requires'].split(', '):
+            for req in self.setup['install_requires']:
                 reqs_list += '    - {}\n'.format(req)
             reqs_string += build_reqs + reqs_list + \
                            run_reqs + reqs_list + \
                            host_reqs + reqs_list + \
                            '\n'
         return reqs_string
-    def fmt_test():
+
+    def fmt_test(self):
         """
         Format test section of metadata.
         NOTE: May need further tweaking to be smarter based on individual project
@@ -61,7 +68,7 @@ def generate_metadata(setup_dict, path):
         :return: Formatted string if tests_require exists; else empty string.
         """
         test_string = ''
-        if 'tests_require' in setup_dict.keys():
+        if 'tests_require' in self.setup.keys():
             test_string += (
                 'test:\n'
                 '  source_files:\n'
@@ -69,7 +76,7 @@ def generate_metadata(setup_dict, path):
                 '  requires:\n'
             )
 
-            for req in setup_dict['tests_require'].split(', '):
+            for req in self.setup['tests_require']:
                 test_string += '    - {}\n'.format(req)
 
             test_string += (
@@ -79,55 +86,33 @@ def generate_metadata(setup_dict, path):
             )
         return test_string
 
-    name = setup_dict['name']
-    version = setup_dict['version']
-    entry_points = fmt_ep()
-    pth = path.replace("./", "")
-    requirements = fmt_reqs()
-    test = fmt_test()
-    lic = 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 = "../../" + pth,
-        requirements = requirements,
-        test = test,
-        license = lic,
-        summary = summary
-    )
-
-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.
-    """
-    data_dict = {}
-    r_entry = r"([a-z-_]+): "
-    r_id = r"[a-zA-Z0-9-_]"
-    r_ep = r"{{(?:[a-zA-Z0-9-_.]+):\s*(?P<values>(?:{0}+\s*=\s*[a-zA-Z0-9-_.]+:{0}+(?:,\s*)?)*)}}".format(r_id)
-
-    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 generate(self):
+        # Filter numpy etc. out of the requirements
+        self.setup['install_requires'] = [req for req in self.setup['install_requires'] if req != 'numpy']
+    
+        name = self.setup['name']
+        version = self.setup['version']
+        entry_points = self.fmt_ep()
+        pth = self.path.replace("./", "")
+        requirements = self.fmt_reqs()
+        test = self.fmt_test()
+        lic = self.setup['license']
+        summary = self.setup['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 = "../../" + pth,
+            requirements = requirements,
+            test = test,
+            license = lic,
+            summary = summary
+        )
+
 
 def parse_setup(d):
     """
@@ -139,11 +124,10 @@ def parse_setup(d):
     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
+    return json.loads(proc.stdout)
 
 def get_outputs(names):
     """
@@ -233,8 +217,7 @@ class Recipe:
         for i, d in enumerate(self.dirs):
             if d != '':
                 setup_data = parse_setup(d)
-                data_dict = data_to_dict(setup_data)
-                metadata = generate_metadata(data_dict, d)
+                metadata = MetadataGenerator(setup_data, d).generate()
                 write_metafile(metadata, self.outputs[i])
                 # Pass created file into options.created()
                 self.options.created(self.outputs[i])
diff --git a/tools/parse_setup.py b/tools/parse_setup.py
index b9e5f8e4b..8acd807b9 100644
--- a/tools/parse_setup.py
+++ b/tools/parse_setup.py
@@ -1,16 +1,6 @@
+import sys
 import setuptools
-
-def get_data(field, data):
-    """
-    Get data from field if it exists.
-    :return: None if not found. Datum of field if found.
-    """
-    datum = None
-    try:
-        datum = data[field]
-    except KeyError as e:
-        pass
-    return datum
+import json
 
 data = {}
 def my_setup(*args, **kwargs):
@@ -28,17 +18,23 @@ def my_setup(*args, **kwargs):
     ]
 
     for field in fields:
-        data[field] = get_data(field, kwargs)
+        data[field] = kwargs.get(field)
 
 def main():
     # Author of these shenanigans: Daniel Lyons (but you already knew that)
+
+    global data
     # Monkey-patch over the setuptools setup() function to do our function instead
     setuptools.setup = my_setup
+
     # Load the setup.py file
     import setup
+
+    # Remove None-flavored values
+    data = {k: v for k, v in data.items() if v is not None}
+
     # Instead of exiting, we now have populated our global variable, without doing any parsing
-    for field, datum in data.items():
-        print('{}: {}'.format(field, datum))
+    json.dump(data, sys.stdout)
 
 if __name__ == "__main__":
     main()
\ No newline at end of file
-- 
GitLab