Skip to content

Commit

Permalink
Merge pull request #22 from james-nesbitt/format-v2
Browse files Browse the repository at this point in the history
refactored formatting
  • Loading branch information
james-nesbitt authored Mar 18, 2021
2 parents ee61079 + 414796e commit f7a63fe
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 228 deletions.
1 change: 0 additions & 1 deletion configerus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@


CONFIGERUS_BOOSTRAP_DEFAULT = [
'deep',
'get',
'files'
]
Expand Down
20 changes: 16 additions & 4 deletions configerus/config.py
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down
Empty file removed configerus/contrib/deep/README.md
Empty file.
21 changes: 0 additions & 21 deletions configerus/contrib/deep/__init__.py

This file was deleted.

41 changes: 0 additions & 41 deletions configerus/contrib/deep/format.py

This file was deleted.

2 changes: 1 addition & 1 deletion configerus/contrib/files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """


Expand Down
97 changes: 33 additions & 64 deletions configerus/contrib/files/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

from configerus.config import Config

FILES_FORMAT_MATCH_PATTERN = r'(\[(file\:)(?P<file>(\~?\/?\w+\/)*\w*(\.\w+)?)\])'
FILES_FORMAT_MATCH_PATTERN = r'(?P<file>(\~?\/?\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):
""" """
Expand All @@ -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
------
Expand All @@ -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))
84 changes: 28 additions & 56 deletions configerus/contrib/get/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,12 @@

logger = logging.getLogger('configerus.contrib.get:formatter')

CONFIG_DEFAULT_MATCH_PATTERN = r'\{((?P<source>\w+):)?(?P<key>[-\w.]+)(\?(?P<default>[^\}]+))?\}'
""" 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<label>\w+)\:)?(?P<key>[-\w.]+)'
""" Default regex pattern used detect format targets for config retrieval """


class ConfigFormatGetPlugin():
""" """
""" return a format replacement by retriving data from config """

def __init__(self, config: Config, instance_id: str):
""" """
Expand All @@ -34,51 +19,38 @@ def __init__(self, config: Config, instance_id: str):

self.pattern = re.compile(CONFIG_DEFAULT_MATCH_PATTERN)

def format(self, target, default_source: str):
""" Format a string by substituting config values
def format(self, key, default_label: str):
""" Format a key by returning config values
target: a string that should be formatted. If not a string then no
formatting is performed
Parameters:
-----------
default_source : if format/replace patterns don't have a source defined
then this is used as a source.
key: a string that should gies instructions to the formatter on how to
create a format replacements
"""
default_label : if format/replace patterns don't have a label defined
then this is used as a label.
if not isinstance(target, str):
return target
Raises:
-------
# 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, default_source)
KeyError if the source or the key could not be found.
# search through the target replacing any found matches with
start = 0
match = self.pattern.search(target, start)
while match:
rep = str(self._get(match, default_source))
target = target[:match.start()] + rep + (target[match.end():] if len(target) > match.end() else '')
start = start + len(rep)
match = self.pattern.search(target, start)
"""

return target
match = self.pattern.fullmatch(key.strip())
if not match:
raise KeyError("Could not interpret Format action key '{}'".format(key))

def _get(self, match, default_source: str):
""" from an re.match get data from config """
label = match.group('label')
key = match.group('key')
source = match.group('source')
default = match.group('default')

if source is None:
source = default_source
source_config = self.config.load(source)
try:
return source_config.get(key, exception_if_missing=True)
except KeyError as e:
if default is not None:
return default
else:
# if a template string wasn't found then exception
raise e

if label is None:
label = default_label

if label is None:
label = default_label

loaded = self.config.load(label)
return loaded.get(key, exception_if_missing=True)
Loading

0 comments on commit f7a63fe

Please sign in to comment.