Source code for merge_utils.naming
"""Utilities for expanding name templates using metadata."""
import os
import sys
import logging
from merge_utils import config, io_utils, config_keys
logger = logging.getLogger(__name__)
CONFIG_KEYS = {
"CFG": "",
"TIMESTAMP": "job.timestamp",
"NAME": "output.name",
"DUNESW_VERSION": "method.environment.dunesw_version",
"DUNESW_QUALIFIER": "method.environment.dunesw_qualifier",
}
FUNC_KEYS = {
"UUID": config.uuid,
"PKG": io_utils.pkg_dir
}
KEY_BLACKLIST = {
"$DUNESW_VERSION": "DUNESW_VERSION",
"$DUNE_VERSION": "DUNESW_VERSION",
"$DUNE_QUALIFIER": "DUNESW_QUALIFIER",
"DUNE_VERSION": "DUNESW_VERSION",
"DUNE_QUALIFIER": "DUNESW_QUALIFIER"
}
[docs]
def read_list(data: list, idx: any) -> tuple:
"""
Try to extract a value from a list.
:param data: list of data
:param idx: index or slice string
:return: extracted value and list of errors
"""
if isinstance(idx, int):
if idx >= 0 and idx < len(data):
return data[idx], []
return None, ["index out of range"]
elif isinstance(idx, str):
try:
idx = [int(i) if i else None for i in idx.split(':')]
if len(idx) == 1:
return data[idx[0]]
if len(idx) == 2:
return data[slice(idx[0], idx[1])]
if len(idx) == 3:
return data[slice(idx[0], idx[1], idx[2])]
except ValueError:
pass
return None, ["invalid slice"]
return None, ["invalid index"]
[docs]
def read_dict(data: dict, key: any) -> tuple:
"""
Try to extract a value from a dictionary.
:param data: dictionary of data
:param key: dictionary key
:return: extracted value
"""
if isinstance(key, str):
# Remove quotes from string keys
if key.startswith("'") and key.endswith("'"):
key = key[1:-1]
elif key.startswith('"') and key.endswith('"'):
key = key[1:-1]
val = data.get(key)
if val is None:
return None, ["invalid index"]
return val, []
[docs]
class Formatter:
"""Wrapper class to access metadata dictionary."""
def __init__(self, metadata: dict = None):
"""
Initialize the Formatter with a metadata dictionary.
"""
self.metadata = metadata
self.errors = []
self.metadata_err = False
self.defer_uuid = False
[docs]
def reset(self):
"""Reset the formatter state."""
self.errors = []
self.metadata_err = False
self.defer_uuid = False
[docs]
def get(self, key, idx=None):
"""
Get a metadata value by key.
:param key: metadata key
:param idx: optional index or slice
:return: metadata value
"""
if self.metadata is None:
self.metadata_err = True
self.errors.insert(0, "Metadata keys cannot be inserted into this string")
return None
val = self.metadata.get(key)
if val is None:
self.errors.append(f"Metadata key '{key}' not found")
elif idx is not None:
errors = []
if isinstance(val, list):
val, errors = read_list(val, idx)
elif isinstance(val, dict):
val, errors = read_dict(val, idx)
elif hasattr(val, '__getitem__'):
try:
val = val[idx]
except (KeyError, IndexError, TypeError):
errors = ["invalid index"]
val = None
else:
self.errors.append(f"Metadata key '{key}' is not subscriptable")
val = None
self.errors.extend([f"Metadata key '{key}[{idx}]' has {err}" for err in errors])
return val
[docs]
def format_key(self, key: str, val: any, spec: str) -> str:
"""
Format a metadata key with a given specification.
:param key: key name
:param val: key value
:param spec: format specification
:return: formatted string
"""
if val is None:
if spec:
return f"{{{key}:{spec}}}"
return f"{{{key}}}"
if isinstance(val, str):
# Apply specific abbreviations
abbr = str(config.naming.abbreviations.get(key, {}).get(val, ''))
if abbr:
logger.debug("Abbreviating key '%s' value '%s' to '%s'", key, val, abbr)
val = abbr
# Remove known extensions
for ext in config.naming.extensions:
if val.endswith(f".{ext}"):
logger.debug("Stripping extension '.%s' from '%s'", ext, val)
val = val[:-(len(ext)+1)]
break
# Apply formatting
try:
output = format(val, spec)
except (ValueError, TypeError):
output = f"{{{key}:{spec}}}"
self.errors.append(f"Invalid format spec '{output}' for value '{val}'")
return output
# Apply general substitutions
for old, new in config.naming.substitutions.items():
output = output.replace(str(old), str(new))
return output
[docs]
class MetaFormatter:
"""Class to format metadata keys."""
def __init__(self, head, key, idx=None, valid=True):
self._head = head
self._key = key
self._idx = idx
self._valid = valid
def __getattr__(self, name):
if self._idx is not None:
new_name = f"{self._key}[{self._idx}].{name}"
return Formatter.MetaFormatter(self._head, new_name, valid=False)
return Formatter.MetaFormatter(self._head, f"{self._key}.{name}", valid=self._valid)
def __getitem__(self, name):
if self._idx is not None:
new_name = f"{self._key}[{self._idx}][{name}]"
return Formatter.MetaFormatter(self._head, new_name, valid=False)
return Formatter.MetaFormatter(self._head, self._key, name, valid=self._valid)
def __format__(self, spec):
key = self._key if self._idx is None else f"{self._key}[{self._idx}]"
if not self._valid:
self._head.errors.append(f"Metadata key '{key}' failed parsing")
return self._head.format_key(key, None, spec)
val = self._head.get(self._key, self._idx)
out = self._head.format_key(key, val, spec)
logger.debug("Key '%s' with value '%s' formatted to '%s'", key, val, out)
return out
[docs]
class CfgFormatter:
"""Class to format configuration keys."""
def __init__(self, head, key, val):
self._head = head
self._key = key
self._val = val
def __getattr__(self, name):
new_key = f"{self._key}.{name}"
new_val = None
if self._val is not None:
try:
new_val = getattr(self._val, name)
except AttributeError:
self._head.errors.append(f"Config key '{new_key}' not found")
return Formatter.CfgFormatter(self._head, new_key, new_val)
def __getitem__(self, name):
new_key = f"{self._key}[{name}]"
new_val = None
if self._val is not None:
try:
new_val = self._val[name]
except (KeyError, IndexError, TypeError):
self._head.errors.append(f"Config key '{new_key}' not found")
return Formatter.CfgFormatter(self._head, new_key, new_val)
def __format__(self, spec):
if isinstance(self._val, config_keys.ConfigString):
logger.debug("Recursively formatting config key '%s'", self._key)
Formatter(self._head.metadata).format(self._val, self._head.defer_uuid)
val = self._val
if val is not None:
val = val._value # pylint: disable=protected-access
if val is None:
self._head.errors.append(f"Config key '{self._key}' not set")
return self._head.format_key(self._key, val, spec)
[docs]
class ValFormatter:
"""Class to format generic values."""
def __init__(self, head, key, value, no_format=False):
self._head = head
self._key = key
self._val = value
self._no_format = no_format
def __format__(self, format_spec):
if self._no_format and format_spec:
if format_spec:
self._head.errors.append(f"Formatting not allowed for key '{self._key}'")
return self._head.format_key(self._key, None, format_spec)
return str(self._val)
return self._head.format_key(self._key, self._val, format_spec)
def __getitem__(self, name):
if name in KEY_BLACKLIST:
new_name = KEY_BLACKLIST[name]
if new_name:
logger.warning("Name substitution '%s' is not allowed, use '%s'", name, new_name)
name = new_name
else:
self.errors.append(f"Name substitution with '{name}' is not allowed")
return Formatter.CfgFormatter(self, name, None)
if self.defer_uuid and name == "UUID":
return Formatter.ValFormatter(self, name, "{UUID}", no_format=True)
if name in FUNC_KEYS:
return Formatter.ValFormatter(self, name, FUNC_KEYS[name]())
if name in CONFIG_KEYS:
obj = config.get_key(CONFIG_KEYS[name])
return Formatter.CfgFormatter(self, name, obj)
if name.startswith('$'):
val = os.getenv(name[1:], None)
if val is None:
self.errors.append(f"Environment var '{name[1:]}' not found")
return Formatter.ValFormatter(self, name, val)
return Formatter.MetaFormatter(self, name)
[docs]
def format(self, template: config_keys.ConfigString, defer_uuid: bool = False):
"""
Format a config string using the metadata dictionary.
:param template: config string template
"""
name = template._name # pylint: disable=protected-access
# Validate input
if not isinstance(template, config_keys.ConfigKey):
logger.critical("The name formatter expects a config key, got '%s'", type(template))
sys.exit(1)
if not isinstance(template, config_keys.ConfigString):
logger.critical("Config key '%s' is not a string and cannot be formatted", name)
sys.exit(1)
config_keys.string_keys.discard(name) # Remove from unformatted keys
# Skip keys witout any {...} fields
val = template.value
if val is None or '{' not in val or '}' not in val:
return
logger.debug("Formatting config key '%s'", name)
# Perform formatting
self.reset()
self.defer_uuid = defer_uuid
result = str(template).format_map(self)
if self.errors:
io_utils.log_list(
f"Config key '{name}' could not be formatted:\n (got '{result}')",
self.errors, logging.CRITICAL)
sys.exit(1)
# Expand paths if needed
if template._type == 'path': # pylint: disable=protected-access
result = io_utils.expand_path(result)
template._set(result) # pylint: disable=protected-access
logger.debug("Config key '%s' formatted to '%s'", name, result)
[docs]
def eval(self, condition: str) -> bool:
"""
Evaluate a condition using the metadata dictionary.
:param condition: condition string to evaluate
:return: evaluated value
"""
self.reset()
logger.debug("Evaluating condition '%s'", condition)
expr = str(condition).format_map(self)
if self.errors:
io_utils.log_list(
f"Error evaluating condition expression '{condition}':",
self.errors, logging.ERROR)
return False
try:
val = eval(expr) #pylint: disable=eval-used
except Exception as exc:
logger.error("Error evaluating condition expression '%s':\n %s", expr, exc)
return False
logger.debug("Condition expression '%s' evaluated to '%s'", expr, val)
return val