From bfa1793dee350e112483605651b88148ae095ca8 Mon Sep 17 00:00:00 2001 From: nhertz <nhertz@nrao.edu> Date: Wed, 2 Sep 2020 17:08:59 -0600 Subject: [PATCH] Added type hinting and PEP8-ified --- build/recipes/build_pkgs/build_pkgs.py | 40 +++--- build/recipes/build_pkgs/setup.py | 6 +- build/recipes/build_pkgs/test/conftest.py | 12 +- .../build_pkgs/test/test_build_pkgs.py | 8 +- build/recipes/setup_to_meta/setup.py | 6 +- build/recipes/setup_to_meta/setup_to_meta.py | 117 ++++++++++-------- build/recipes/setup_to_meta/test/conftest.py | 21 +++- .../setup_to_meta/test/test_setup_to_meta.py | 13 +- 8 files changed, 128 insertions(+), 95 deletions(-) diff --git a/build/recipes/build_pkgs/build_pkgs.py b/build/recipes/build_pkgs/build_pkgs.py index 304d2bad2..0cafdde39 100644 --- a/build/recipes/build_pkgs/build_pkgs.py +++ b/build/recipes/build_pkgs/build_pkgs.py @@ -1,19 +1,23 @@ -import subprocess import logging +import subprocess +from typing import List, Dict, Any, Callable, Optional + +from zc.buildout.buildout import Buildout, Options -logger = logging.getLogger("buildout/build_pkgs") +logger: logging.Logger = logging.getLogger("buildout/build_pkgs") -def get_dirs(): + +def get_dirs() -> List[str]: """ Finds all subdirectories containing setup.py files. :return: List of directories as strings. """ logger.debug("Getting list of directories containing setup.py files...") - find = subprocess.run([ + find: subprocess.CompletedProcess = subprocess.run([ 'find', '.', '-name', 'setup.py', '-not', '-path', './build/recipes/*' ], stdout=subprocess.PIPE) - dirs = find.stdout.decode('utf-8').split('\n') - dirs_cpy = dirs + dirs: List[str] = find.stdout.decode('utf-8').split('\n') + dirs_cpy: List[str] = dirs for i, d in enumerate(dirs_cpy): dirs[i] = d.replace('/setup.py', '') @@ -21,17 +25,18 @@ def get_dirs(): logger.debug("Done getting directories.") return dirs -def get_names(dirs): + +def get_names(dirs: List[str]) -> List[str]: """ 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. """ logger.debug("Generating list of subproject names...") - names = [] + names: List[str] = [] for d in dirs: if d != '': - name = d.split('/')[-1] + name: str = d.split('/')[-1] if name == "archive": # Case with ./services/archive having special dir structure @@ -41,8 +46,9 @@ def get_names(dirs): logger.debug("Done generating.") return names + class Recipe: - def __init__(self, buildout, name, options): + def __init__(self, buildout: Optional[Buildout], name: str, options: Options): """ Initializes fields needed for recipe. :param buildout: (Boilerplate) Dictionary of options from buildout section @@ -50,11 +56,11 @@ class Recipe: :param name: (Boilerplate) Name of section that uses this recipe. :param options: (Boilerplate) Options of section that uses this recipe. """ - self.name = name - self.options = options - self.pkg_list = get_names(get_dirs()) + self.name: str = name + self.options: Options = options + self.pkg_list: List[str] = get_names(get_dirs()) - def install(self): + def install(self) -> Any: """ Install method that runs when recipe has components it needs to install. :return: Paths to files, as strings, created by the recipe. @@ -63,9 +69,9 @@ class Recipe: logger.warning("WARNING: You've requested all packages to be built. This will take a long time.") logger.warning("If only one or a few packages have changed, consider specifying them " "in a comma-separated list.") - pkgs = self.pkg_list + pkgs: List[str] = self.pkg_list else: - pkgs = self.options['name'].split(',') + pkgs: List[str] = self.options['name'].split(',') for p in pkgs: if p not in self.pkg_list or p == '': @@ -79,4 +85,4 @@ class Recipe: return self.options.created() - update = install \ No newline at end of file + update: Callable = install diff --git a/build/recipes/build_pkgs/setup.py b/build/recipes/build_pkgs/setup.py index db69c236a..04e432da6 100644 --- a/build/recipes/build_pkgs/setup.py +++ b/build/recipes/build_pkgs/setup.py @@ -3,6 +3,6 @@ from setuptools import setup setup( name='build_pkgs', version='0.1', - py_modules = ['build_pkgs'], - entry_points = {"zc.buildout": ["default=build_pkgs:Recipe"]}, -) \ No newline at end of file + py_modules=['build_pkgs'], + entry_points={"zc.buildout": ["default=build_pkgs:Recipe"]}, +) diff --git a/build/recipes/build_pkgs/test/conftest.py b/build/recipes/build_pkgs/test/conftest.py index e98638c7a..5d7d7756f 100644 --- a/build/recipes/build_pkgs/test/conftest.py +++ b/build/recipes/build_pkgs/test/conftest.py @@ -1,8 +1,10 @@ import pytest import zc.buildout.testing +from .. import build_pkgs + @pytest.fixture(scope='module') -def recipe(): +def recipe() -> build_pkgs.Recipe: """ pytest fixture that initializes zc.buildout objects for use in testing. Initializes Buildout, Options, and Recipe objects. @@ -10,7 +12,7 @@ def recipe(): :return: Initialized recipe object for build_pkgs """ from .. import build_pkgs - buildout = zc.buildout.testing.Buildout() - options = buildout.Options(buildout, 'build_pkgs', {'recipe': 'build_pkgs', 'name': 'null'}) - recipe = build_pkgs.Recipe(buildout=buildout, name=None, options=options) - return recipe \ No newline at end of file + buildout: zc.buildout.testing.Buildout = zc.buildout.testing.Buildout() + options: buildout.Options = buildout.Options(buildout, 'build_pkgs', {'recipe': 'build_pkgs', 'name': 'null'}) + recipe: build_pkgs.Recipe = build_pkgs.Recipe(buildout=buildout, name=None, options=options) + return recipe diff --git a/build/recipes/build_pkgs/test/test_build_pkgs.py b/build/recipes/build_pkgs/test/test_build_pkgs.py index c0dfae66c..10758442d 100644 --- a/build/recipes/build_pkgs/test/test_build_pkgs.py +++ b/build/recipes/build_pkgs/test/test_build_pkgs.py @@ -1,13 +1,16 @@ import os +from typing import List + from .. import build_pkgs + class TestBuildPkgs: def test_get_names(self): """ Test that build_pkgs correctly gets the package name from :return: """ - d = 'apps/cli/executables/null' + d: str = 'apps/cli/executables/null' assert build_pkgs.get_names([d]) == ['null'] def test_get_dirs(self): @@ -21,10 +24,9 @@ class TestBuildPkgs: """ Test that the package specified in the recipe has been built correctly. """ - created = recipe.install() + created: List[str] = recipe.install() for path in created: - print(path) if len(path) > 0: assert path is not None, "conda build failed to build package" assert os.path.exists(path) diff --git a/build/recipes/setup_to_meta/setup.py b/build/recipes/setup_to_meta/setup.py index 24ade2276..8a2ebea13 100644 --- a/build/recipes/setup_to_meta/setup.py +++ b/build/recipes/setup_to_meta/setup.py @@ -3,6 +3,6 @@ from setuptools import setup setup( name='setup_to_meta', version='0.1', - py_modules = ['setup_to_meta'], - entry_points = {"zc.buildout": ["default=setup_to_meta:Recipe"]}, -) \ No newline at end of file + py_modules=['setup_to_meta'], + entry_points={"zc.buildout": ["default=setup_to_meta:Recipe"]}, +) diff --git a/build/recipes/setup_to_meta/setup_to_meta.py b/build/recipes/setup_to_meta/setup_to_meta.py index f7e32134d..b8ed06744 100644 --- a/build/recipes/setup_to_meta/setup_to_meta.py +++ b/build/recipes/setup_to_meta/setup_to_meta.py @@ -1,15 +1,20 @@ - -import subprocess -import logging -import json import os +import json +import logging +import subprocess +from typing import List, Any, Dict, Callable, Optional + +from zc.buildout.buildout import Buildout, Options -PYTHON_VERSION = '3.8' -logger = logging.getLogger("buildout/setup_to_meta") +PYTHON_VERSION: str = '3.8' +logger: logging.Logger = logging.getLogger("buildout/setup_to_meta") -def write_metafile(metadata, filepath): + +def write_metafile(metadata: str, filepath: str): """ Writes given metadata to file with given path. + :param metadata: String containing conda recipe metadata to be written + :param filepath: String containing the path to conda recipe file (meta.yaml) """ logger.debug(f"Writing meta.yaml file at {filepath}...") try: @@ -26,17 +31,16 @@ class MetadataGenerator: """ Uses given info extracted from setup.py file to fill out metadata template. """ - - def __init__(self, setup, path): - self.setup = setup - self.path = path + def __init__(self, setup: Dict[str, str], path: str): + self.setup: Dict[str, str] = setup + self.path: str = path - def fmt_ep(self): + def fmt_ep(self) -> str: """ Format entry points section of metadata. :return: Formatted string if entry points exists; else empty string. """ - ep_string = '' + ep_string: str = '' if 'entry_points' in self.setup.keys() and 'console_scripts' in self.setup['entry_points']: ep_string += 'entry_points:\n' for ep in self.setup['entry_points']['console_scripts']: @@ -44,17 +48,17 @@ class MetadataGenerator: ep_string += ' ' return ep_string - def fmt_reqs(self): + def fmt_reqs(self) -> str: """ Format requirements section of metadata. :return: Formatted string if requirements exists; else empty string. """ - reqs_string = '' - reqs_list = '' + reqs_string: str = '' + reqs_list: str = '' reqs_string += 'requirements:\n' - build_reqs = ' build:\n' - run_reqs = ' run:\n' - host_reqs = ' host:\n' + build_reqs: str = ' build:\n' + run_reqs: str = ' run:\n' + host_reqs: str = ' host:\n' reqs_list += ' - python={}\n'.format(PYTHON_VERSION) if 'install_requires' in self.setup.keys(): @@ -67,14 +71,14 @@ class MetadataGenerator: '\n' return reqs_string - def fmt_test(self): + def fmt_test(self) -> str: """ Format test section of metadata. NOTE: May need further tweaking to be smarter based on individual project needs. For now, it's pretty dumb. :return: Formatted string if tests_require exists; else empty string. """ - test_string = '' + test_string: str = '' if 'tests_require' in self.setup.keys(): test_string += ( 'test:\n' @@ -93,7 +97,7 @@ class MetadataGenerator: ) return test_string - def generate(self): + def generate(self) -> str: logger.debug(f"Generating meta.yaml file from {self.path}...") # Filter numpy etc. out of the requirements try: @@ -101,17 +105,17 @@ class MetadataGenerator: except KeyError: pass - 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'] + name: str = self.setup['name'] + version: str = self.setup['version'] + entry_points: str = self.fmt_ep() + pth: str = self.path.replace("./", "") + requirements: str = self.fmt_reqs() + test: str = self.fmt_test() + lic: str = self.setup['license'] + summary: str = self.setup['description'] with open('build/tools/metafile_template.txt', 'r') as f: - metadata = f.read() + metadata: str = f.read() logger.debug("Done generating.") return metadata.format( @@ -126,7 +130,7 @@ class MetadataGenerator: ) -def parse_setup(d): +def parse_setup(d: str) -> Dict[str, str]: """ 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. @@ -136,36 +140,37 @@ def parse_setup(d): logger.debug(f"Parsing setup.py at {d}...") subprocess.run(['cp', 'build/tools/parse_setup.py', d]) os.chdir(d) - proc = subprocess.run(['python3', 'parse_setup.py'], stdout=subprocess.PIPE) + proc: subprocess.CompletedProcess = subprocess.run(['python3', 'parse_setup.py'], stdout=subprocess.PIPE) os.chdir(root) subprocess.run(['rm', '{}/parse_setup.py'.format(d)]) logger.debug("Done parsing.") return json.loads(proc.stdout) -def get_outputs(names): + +def get_outputs(names: List[str]) -> List[str]: """ Generate list of metadata files that will be created. :param dirs: List of dirs of all subprojects with a setup.py file. :return: List of paths to output files as strings. """ - outputs = [] + outputs: List[str] = [] for name in names: outputs.append("build/metadata/{}/meta.yaml".format(name)) return outputs -def get_dirs(): +def get_dirs() -> List[str]: """ Finds all subdirectories containing setup.py files. :return: List of directories as strings. """ logger.debug("Finding list of directories containing setup.py files...") - find = subprocess.run([ + find: subprocess.CompletedProcess = subprocess.run([ 'find', '.', '-name', 'setup.py', '-not', '-path', './build/recipes/*' ], stdout=subprocess.PIPE) - dirs = find.stdout.decode('utf-8').split('\n') - dirs_cpy = dirs + dirs: List[str] = find.stdout.decode('utf-8').split('\n') + dirs_cpy: List[str] = dirs for i, d in enumerate(dirs_cpy): dirs[i] = d.replace('/setup.py', '') @@ -173,14 +178,15 @@ def get_dirs(): logger.debug("Done finding directories.") return dirs -def get_names(dirs): + +def get_names(dirs: List[str]) -> List[str]: """ 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. """ logger.debug("Getting list of names...") - names = [] + names: List[str] = [] for d in dirs: if d != '': name = d.split('/')[-1] @@ -193,7 +199,8 @@ def get_names(dirs): logger.debug("Done getting list of names.") return names -def del_substrings(s, substrings): + +def del_substrings(s: str, substrings: List[str]): """ Function for deleting multiple substrings from a string. :param s: String to be modified. @@ -201,11 +208,13 @@ def del_substrings(s, substrings): :return: Modified string. """ for replace in substrings: - s = s.replace(replace, '') + s: str = s.replace(replace, '') return s -root = os.getcwd() + +root: str = os.getcwd() + class Recipe: """ @@ -213,7 +222,7 @@ class Recipe: For more detailed information, see the link. http://www.buildout.org/en/latest/topics/writing-recipes.html """ - def __init__(self, buildout, name, options): + def __init__(self, buildout: Optional[Buildout], name: Optional[str], options: Options): """ Initializes fields needed for recipe. :param buildout: (Boilerplate) Dictionary of options from buildout section @@ -221,25 +230,25 @@ class Recipe: :param name: (Boilerplate) Name of section that uses this recipe. :param options: (Boilerplate) Options of section that uses this recipe. """ - self.dirs = get_dirs() - self.names = get_names(self.dirs) - self.outputs = get_outputs(self.names) - self.options = options + self.dirs: List[str] = get_dirs() + self.names: List[str] = get_names(self.dirs) + self.outputs: List[str] = get_outputs(self.names) + self.options: Options = options - def install(self): + def install(self) -> Any: """ Install method that runs when recipe has components it needs to install. :return: Paths to files, as strings, created by the recipe. """ for i, d in enumerate(self.dirs): if d != '': - setup_data = parse_setup(d) - metadata = MetadataGenerator(setup_data, d).generate() + setup_data: Dict[str, str] = parse_setup(d) + metadata: str = MetadataGenerator(setup_data, d).generate() write_metafile(metadata, self.outputs[i]) - # Pass created file into options.created() + # Buildout-specific operation: pass created file into options.created() self.options.created(self.outputs[i]) return self.options.created() # No special procedure for updating vs. installing - update = install \ No newline at end of file + update: Callable = install diff --git a/build/recipes/setup_to_meta/test/conftest.py b/build/recipes/setup_to_meta/test/conftest.py index 2f6dfb21d..c9f4b3a91 100644 --- a/build/recipes/setup_to_meta/test/conftest.py +++ b/build/recipes/setup_to_meta/test/conftest.py @@ -1,8 +1,11 @@ import pytest import zc.buildout.testing +from .. import setup_to_meta + + @pytest.fixture(scope='module') -def recipe(): +def recipe() -> setup_to_meta.Recipe: """ pytest fixture that initializes zc.buildout objects for use in testing. Initializes Buildout, Options, and Recipe objects. @@ -10,7 +13,15 @@ def recipe(): :return: Initialized recipe object for setup_to_meta """ from .. import setup_to_meta - buildout = zc.buildout.testing.Buildout() - options = buildout.Options(buildout, 'gen_metadata', {'recipe': 'setup_to_meta'}) - recipe = setup_to_meta.Recipe(buildout=buildout, name=None, options=options) - return recipe \ No newline at end of file + buildout: zc.buildout.testing.Buildout = zc.buildout.testing.Buildout() + options: buildout.Options = buildout.Options( + buildout, + 'gen_metadata', + {'recipe': 'setup_to_meta'} + ) + recipe: setup_to_meta.Recipe = setup_to_meta.Recipe( + buildout=buildout, + name=None, + options=options + ) + return recipe diff --git a/build/recipes/setup_to_meta/test/test_setup_to_meta.py b/build/recipes/setup_to_meta/test/test_setup_to_meta.py index 7b0162ae6..4b17197ca 100644 --- a/build/recipes/setup_to_meta/test/test_setup_to_meta.py +++ b/build/recipes/setup_to_meta/test/test_setup_to_meta.py @@ -1,18 +1,21 @@ +from typing import Dict, List + from .. import setup_to_meta + class TestSetupToMeta: def test_del_substrings(self): """ Tests that del_substrings function properly deletes substrings from a given string """ - replaced = setup_to_meta.del_substrings('heallob, woarlcd', ['a', 'b', 'c']) + replaced: str = setup_to_meta.del_substrings('heallob, woarlcd', ['a', 'b', 'c']) assert replaced == 'hello, world' def test_get_names(self): """ Tests that setup_to_meta correctly gets the package name from the package path """ - d = 'apps/cli/executables/null' + d: str = 'apps/cli/executables/null' assert setup_to_meta.get_names([d]) == ['null'] def test_get_dirs(self): @@ -33,8 +36,8 @@ class TestSetupToMeta: """ Tests that parse_setup correctly parses a setup.py file into a dictionary. """ - setup_data = setup_to_meta.parse_setup('apps/cli/executables/null') - keys = ['name', 'version', 'description', 'license'] + setup_data: Dict[str, str] = setup_to_meta.parse_setup('apps/cli/executables/null') + keys: List[str] = ['name', 'version', 'description', 'license'] for key in keys: assert key in setup_data @@ -46,7 +49,7 @@ class TestSetupToMeta: occur in a correct recipe. :param recipe: Fixture that initializes recipe class in setup_to_meta.py """ - created = recipe.install() + created: List[str] = recipe.install() for path in created: with open(path, 'r') as f: -- GitLab