From 414796edbe4dc936a99202c11bd5768a0655f3f5 Mon Sep 17 00:00:00 2001 From: James Nesbitt Date: Tue, 16 Mar 2021 13:00:36 -0400 Subject: [PATCH] refactored formatting - pattern now uses double braces {{ }} for pattern identification - patterns now use "plugin::" to pass arguments to a formatter plugin - plugins now receive "plugin::target"" target strings instead of full data - single pass instead of a pass per plugin (centralized patterns) - unlimited default ? options - "special::None" , [], {} "null" options Signed-off-by: James Nesbitt --- configerus/__init__.py | 1 - configerus/config.py | 20 +- configerus/contrib/deep/README.md | 0 configerus/contrib/deep/__init__.py | 21 -- configerus/contrib/deep/format.py | 41 ---- configerus/contrib/files/__init__.py | 2 +- configerus/contrib/files/format.py | 97 ++++------ configerus/contrib/get/format.py | 84 +++----- configerus/format.py | 277 +++++++++++++++++++++++++++ configerus/plugin.py | 4 +- configerus/test/test_5_templating.py | 122 ++++++++---- setup.cfg | 4 +- 12 files changed, 445 insertions(+), 228 deletions(-) delete mode 100644 configerus/contrib/deep/README.md delete mode 100644 configerus/contrib/deep/__init__.py delete mode 100644 configerus/contrib/deep/format.py create mode 100644 configerus/format.py diff --git a/configerus/__init__.py b/configerus/__init__.py index f9f89da..fb91b20 100644 --- a/configerus/__init__.py +++ b/configerus/__init__.py @@ -6,7 +6,6 @@ CONFIGERUS_BOOSTRAP_DEFAULT = [ - 'deep', 'get', 'files' ] diff --git a/configerus/config.py b/configerus/config.py index 2474c35..ae0858a 100644 --- a/configerus/config.py +++ b/configerus/config.py @@ -1,13 +1,14 @@ import logging from importlib import metadata import copy -from typing import Any +from typing import Any, Dict, List from .plugin import Factory, Type from .instances import PluginInstances from .shared import tree_merge from .loaded import Loaded from .validator import ValidationError +from .format import Formatter logger = logging.getLogger('configerus:config') @@ -271,6 +272,9 @@ def add_formatter(self, plugin_id: str, instance_id: str = '', priority: int = P def format(self, data, default_label: Any, validator: str = ""): """ Format some data using the config object formatters + Parameters: + ----------- + data (Any): primitive data that should be formatted. The data will be passed to the formatter plugins in descending priority order. @@ -286,12 +290,20 @@ def format(self, data, default_label: Any, validator: str = ""): if empty/None then no validation is performed. + Raises: + ------- + + Will throw a ValueError if your data contains a format tag that cannot + be interpreted. + Will raise a KeyError if your data contains a format tag with a bad key + for action, or for a value as interpreted by the plugin. """ - for formatter in self.plugins.get_plugins(type=Type.FORMATTER, exception_if_missing=False): - data = formatter.format(data, default_label) + + formatter = Formatter(self) + data = formatter.format(data=data, default_label=default_label) if validator: - self.validate(self.loaded[label].data, validator) + self.validate(data, validator) return data diff --git a/configerus/contrib/deep/README.md b/configerus/contrib/deep/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/configerus/contrib/deep/__init__.py b/configerus/contrib/deep/__init__.py deleted file mode 100644 index d945c47..0000000 --- a/configerus/contrib/deep/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ - -from configerus.config import Config -from configerus.plugin import FormatFactory - -from .format import ConfigFormatDeepPlugin - -PLUGIN_ID_FORMAT_DEEP = 'deep' -PLUGIN_PRIORITY_FORMAT_DEEP = 90 - -""" Format plugin_id for the configerus deep format plugin """ - - -@FormatFactory(plugin_id=PLUGIN_ID_FORMAT_DEEP) -def plugin_factory_format_deep(config: Config, instance_id: str = ''): - """ create an format plugin which recursiveley formats data """ - return ConfigFormatDeepPlugin(config, instance_id) - - -def configerus_bootstrap(config: Config): - """ Bootstrap a config object by adding our formatter """ - config.add_formatter(PLUGIN_ID_FORMAT_DEEP, priority=PLUGIN_PRIORITY_FORMAT_DEEP) diff --git a/configerus/contrib/deep/format.py b/configerus/contrib/deep/format.py deleted file mode 100644 index 11b35d1..0000000 --- a/configerus/contrib/deep/format.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import List, Dict, Any - -from configerus.config import Config - - -class ConfigFormatDeepPlugin(): - """ """ - - def __init__(self, config: Config, instance_id: str): - """ """ - self.config = config - self.instance_id = instance_id - - def format(self, target, default_source: str): - """ Deep formatter for formatting that reruns formatting across iterables - - this formatter just reruns formatting on elements of primitive iterables. - - """ - # try to iterate across the target - # There is probably a more `python` approach that would cover more - # iterables, as long as we can re-assign - if isinstance(target, Dict): - return self.format_dict(target, default_source) - elif isinstance(target, List): - return self.format_list(target, default_source) - - # anything we can't iterate through we just return - return target - - def format_list(self, target: List[Any], default_source: str): - """ Search a List for strings to format @see format_string """ - for index, value in enumerate(target): - target[index] = self.config.format(value, default_source) - return target - - def format_dict(self, target: Dict[str, Any], default_source: str): - """ Search a Dict for strings to format @see format_string """ - for key, value in target.items(): - target[key] = self.config.format(value, default_source) - return target diff --git a/configerus/contrib/files/__init__.py b/configerus/contrib/files/__init__.py index f671035..60d1870 100644 --- a/configerus/contrib/files/__init__.py +++ b/configerus/contrib/files/__init__.py @@ -19,7 +19,7 @@ def plugin_factory_configsource_path(config: Config, instance_id: str = ''): return ConfigSourcePathPlugin(config, instance_id) -PLUGIN_ID_FORMAT_FILE = 'configerus.plugin.formatter.file' +PLUGIN_ID_FORMAT_FILE = 'file' """ Format plugin_id for the configerus filepath format plugin """ diff --git a/configerus/contrib/files/format.py b/configerus/contrib/files/format.py index 3bb5196..7440de5 100644 --- a/configerus/contrib/files/format.py +++ b/configerus/contrib/files/format.py @@ -7,14 +7,14 @@ from configerus.config import Config -FILES_FORMAT_MATCH_PATTERN = r'(\[(file\:)(?P(\~?\/?\w+\/)*\w*(\.\w+)?)\])' +FILES_FORMAT_MATCH_PATTERN = r'(?P(\~?\/?\w+\/)*\w*(\.\w+)?)' """ A regex pattern to identify files that should be embedded """ logger = logging.getLogger('configerus.contrib.files:format') class ConfigFormatFilePlugin: - """ """ + """ Format a key by returning the contents of a file """ def __init__(self, config: Config, instance_id: str): """ """ @@ -23,51 +23,18 @@ def __init__(self, config: Config, instance_id: str): self.pattern = re.compile(FILES_FORMAT_MATCH_PATTERN) - def format(self, target, default_source: str): + def format(self, key, default_label: str): """ Format a string by substituting config values Parameters ---------- - target: a string that should be formatted. If not a string then no - formatting is performed + key: a string that should gies instructions to the formatter on how to + create a format replacements - default_source : if format/replace patterns don't have a source defined + default_label : if format/replace patterns don't have a source defined then this is used as a source. - """ - if not isinstance(target, str): - return target - - # if the entire target is the match, then replace whatever type we get - # out of the config .get() call - match = self.pattern.fullmatch(target) - if match: - return self._get(match, return_only_string=False) - - # search through the target replacing any found matches with - start = 0 - match = self.pattern.search(target, start) - while match: - rep = str(self._get(match)) - target = target[:match.start()] + rep + target[match.end()] - start = start + len(rep) - match = self.pattern.search(target, start) - - return target - - def _get(self, match, return_only_string: bool = True): - """ find a file match and return the file contents - - Parameters - ---------- - - match (re Match) a regex match which we use to determine file path - - return_only_string (bool) optional indictor that the function is - expecting only a string return. Knowing this means that we can - skip unmarshalling file contents which could be expensive. - Raises ------ @@ -84,40 +51,42 @@ def _get(self, match, return_only_string: bool = True): unmarshalled json/yml file or string contents of the file """ + + match = self.pattern.fullmatch(key.strip()) + if not match: + raise KeyError("Could not interpret Format action target '{}'".format(key)) + file = match.group('file') - extension = '' + """ path to the file to return as a replacement """ + extension = '' + """ file extension, used to make decisions about parsing/unmarshalling """ file_split = os.path.splitext(file) if len(file_split) > 0: extension = file_split[1].lower() try: - with open(file) as file_o: - if not return_only_string: - # try to parse/unmarshall a file instead of just returing it - - if extension == ".json": - try: - data = json.load(file_o) - except json.decoder.JSONDecodeError as e: - raise ValueError( - "Failed to parse one of the config files '{}': {}".format( - os.path.join( - self.path, file), e)) - return data - - elif extension == ".yml" or extension == ".yaml": - try: - data = yaml.load(file_o, Loader=yaml.FullLoader) - except yaml.YAMLError as e: - raise ValueError( - "Failed to parse one of the config files '{}': {}".format( - os.path.join( - self.path, file), e)) - return data + with open(file) as fo: + if extension == ".json": + try: + return json.load(fo) + except json.decoder.JSONDecodeError as e: + raise ValueError( + "Failed to parse one of the config files '{}': {}".format( + os.path.join( + self.path, file), e)) + + elif extension == ".yml" or extension == ".yaml": + try: + return yaml.load(fo, Loader=yaml.FullLoader) + except yaml.YAMLError as e: + raise ValueError( + "Failed to parse one of the config files '{}': {}".format( + os.path.join( + self.path, file), e)) # return file contents as a string (above parsing didn't happen) - return file_o.read() + return fo.read() except FileNotFoundError as e: raise KeyError("Could not embed file as config as file could not be found: {}".format(file)) diff --git a/configerus/contrib/get/format.py b/configerus/contrib/get/format.py index 2ea8412..ee27172 100644 --- a/configerus/contrib/get/format.py +++ b/configerus/contrib/get/format.py @@ -5,27 +5,12 @@ logger = logging.getLogger('configerus.contrib.get:formatter') -CONFIG_DEFAULT_MATCH_PATTERN = r'\{((?P\w+):)?(?P[-\w.]+)(\?(?P[^\}]+))?\}' -""" Default regex pattern used to string template, which needs to identify -keys that can be used for replacement. Needs to have a named group for -key, and can have named group for source and default - -The above regex pattern resolves for three named groups: source, key and default: - -the {first?check} value : key=first, default=check -a {common.dot.notation} test : key=commond.dot.notation -a {label:dot.notation.test} here : source=label, key=dot.notation.test - -so: -1. all replacements are wrapped in "{}" -2. an optional "source:" group tells config to look in a specifig label -3. a "?default" group allows a default (untemplated) value to be used if the - key cannot be found -""" +CONFIG_DEFAULT_MATCH_PATTERN = r'((?P