diff --git a/acre/core.py b/acre/core.py index 661d52e..1a22152 100644 --- a/acre/core.py +++ b/acre/core.py @@ -10,7 +10,7 @@ PLATFORM = platform.system().lower() logging.basicConfig() -log = logging.getLogger() +log = logging.getLogger(__name__) class CycleError(ValueError): @@ -44,14 +44,17 @@ def compute(env, # Collect dependencies dependencies = [] for key, value in env.items(): - dependent_keys = re.findall("{(.+?)}", value) - for dependency in dependent_keys: - # Ignore direct references to itself because - # we don't format with itself anyway - if dependency == key: - continue + try: + dependent_keys = re.findall("{(.+?)}", value) + for dependency in dependent_keys: + # Ignore direct references to itself because + # we don't format with itself anyway + if dependency == key: + continue - dependencies.append((key, dependency)) + dependencies.append((key, dependency)) + except Exception: + dependencies.append((key, value)) result = lib.topological_sort(dependencies) @@ -66,6 +69,8 @@ def compute(env, # Format dynamic values for key in reversed(result.sorted): if key in env: + if not isinstance(env[key], str): + continue data = env.copy() data.pop(key) # format without itself env[key] = lib.partial_format(env[key], data=data) @@ -73,6 +78,8 @@ def compute(env, # Format cyclic values for key in result.cyclic: if key in env: + if not isinstance(env[key], str): + continue data = env.copy() data.pop(key) # format without itself env[key] = lib.partial_format(env[key], data=data) @@ -81,8 +88,12 @@ def compute(env, if dynamic_keys: formatted = {} for key, value in env.items(): - new_key = lib.partial_format(key, data=env) + if not isinstance(value, str): + new_key = key + formatted[key] = value + continue + new_key = lib.partial_format(key, data=env) if new_key in formatted: if not allow_key_clash: raise DynamicKeyClashError("Key clashes on: {0} " @@ -96,6 +107,8 @@ def compute(env, if cleanup: separator = os.pathsep for key, value in env.items(): + if not isinstance(value, str): + continue paths = value.split(separator) # Keep unique path entries: {A};{A};{B} -> {A};{B} @@ -138,7 +151,7 @@ def parse(env, platform_name=None): # Allow to have lists as values in the tool data if isinstance(value, (list, tuple)): - value = ";".join(value) + value = os.pathsep.join(value) result[variable] = value @@ -151,12 +164,14 @@ def append(env, env_b): # todo: this function name might also be confusing with "merge" env = env.copy() for variable, value in env_b.items(): - for path in value.split(";"): - if not path: - continue - - lib.append_path(env, variable, path) - + try: + for path in value.split(os.pathsep): + if not path: + continue + lib.append_path(env, variable, path) + except Exception: + if not isinstance(value, str): + env[variable] = value return env @@ -190,7 +205,7 @@ def get_tools(tools, platform_name=None): '"TOOL_ENV" environment variable not found. ' 'Please create it and point it to a folder with your .json ' 'config files.' - ) + ) # Collect the tool files to load tool_paths = [] @@ -219,12 +234,13 @@ def get_tools(tools, platform_name=None): continue tool_env = parse(tool_env, platform_name=platform_name) + environment = append(environment, tool_env) return environment -def merge(env, current_env): +def merge(env, current_env, missing=None): """Merge the tools environment with the 'current_env'. This finalizes the join with a current environment by formatting the @@ -236,16 +252,20 @@ def merge(env, current_env): env (dict): The dynamic environment current_env (dict): The "current environment" to merge the dynamic environment into. + missing (str): Argument passed to 'partial_format' during merging. + `None` should keep missing keys unchanged. Returns: dict: The resulting environment after the merge. """ - result = current_env.copy() for key, value in env.items(): - value = lib.partial_format(value, data=current_env, missing="") - result[key] = value + if not isinstance(value, str): + value = str(value) - return result + value = lib.partial_format(value, data=current_env, missing=missing) + result[key] = str(value) + + return result diff --git a/acre/lib.py b/acre/lib.py index 09b3062..ca312da 100644 --- a/acre/lib.py +++ b/acre/lib.py @@ -1,4 +1,5 @@ import os +import re import string from collections import defaultdict, namedtuple @@ -37,12 +38,24 @@ class FormatDict(dict): Missing keys are replaced with the return value of __missing__. """ + def __missing__(self, key): return missing.format(key=key) formatter = string.Formatter() mapping = FormatDict(**data) - return formatter.vformat(s, (), mapping) + try: + f = formatter.vformat(s, (), mapping) + except Exception: + r_token = re.compile(r"({.*?})") + matches = re.findall(r_token, s) + f = s + for m in matches: + try: + f = re.sub(m, m.format(**data), f) + except (KeyError, ValueError): + continue + return f def topological_sort(dependency_pairs): @@ -68,10 +81,12 @@ def topological_sort(dependency_pairs): return Results(ordered, cyclic) -def append_path(self, key, path): - """Append *path* to *key* in *self*.""" - try: - if path not in self[key]: - self[key] = os.pathsep.join([self[key], str(path)]) - except KeyError: - self[key] = str(path) +def append_path(env, key, path): + """Append *path* to *key* in *env*.""" + + orig_value = env.get(key) + if not orig_value: + env[key] = str(path) + + elif path not in orig_value.split(os.pathsep): + env[key] = os.pathsep.join([orig_value, str(path)]) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8ef3f1a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +version = "1.0.0" +name = "acre" +description = "Lightweight cross-platform environment management Python package that makes it trivial to launch applications in their own configurable working environment." +readme = "README.md" +authors = [ + { name = "Roy Nieterau", email ="roy_nieterau@hotmail.com" }, +] +license = { file = "LICENSE" } +requires-python = ">=2.7" +keywords = ["environment", "pipeline"] + +classifiers = [ + "Topic :: Software Development", + "Topic :: Utilities", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +[project.urls] +homepage = "https://github.com/BigRoy/acre" +repository = "https://github.com/BigRoy/acre" +documentation = "https://github.com/BigRoy/acre" + +[build-system] +requires = ["setuptools >= 35.0.2", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6479c8b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,20 @@ +[metadata] +name = acre +version = 1.0.0 +description = Lightweight cross-platform environment management Python package that makes it trivial to launch applications in their own configurable working environment +long_description = file: README.md, LICENSE.md +keywords = environment, pipeline +license = GNU Lesser General Public License v3 (LGPLv3) +classifiers = + Topic :: Software Development + License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) + Topic :: Utilities + Intended Audience :: Developers + Topic :: Software Development :: Libraries :: Python Modules + +[options] +zip_safe = True +include_package_data = True +packages = find: +install_requires = + importlib; python_version == "2.7" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bac24a4 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import setuptools + +if __name__ == "__main__": + setuptools.setup() diff --git a/tests/test_dynamic_environments.py b/tests/test_dynamic_environments.py index d039d11..4558ecb 100644 --- a/tests/test_dynamic_environments.py +++ b/tests/test_dynamic_environments.py @@ -131,6 +131,22 @@ def test_compute_preserve_reference_to_self(self): "PYTHONPATH": "x;y/{PYTHONPATH}" }) + def test_compute_reference_formats(self): + """acre.compute() will correctly skip unresolved references.""" + data = { + "A": "a", + "B": "{A},b", + "C": "{C}", + "D": "{D[x]}", + } + data = acre.compute(data) + self.assertEqual(data, { + "A": "a", + "B": "a,b", + "C": "{C}", + "D": "{D[x]}" + }) + def test_append(self): """Append paths of two environments into one."""