"""
=========
Utilities
=========
This module contains utility functions and classes for the layered_config_tree
package.
"""
from collections.abc import Hashable
from pathlib import Path
from typing import Any
import yaml
from layered_config_tree import DuplicatedConfigurationError
[docs]
def load_yaml(data: str | Path) -> dict[str, Any]:
"""Load a YAML filepath or string into a dictionary.
Parameters
----------
data
The YAML content to load. This can be a file path to a YAML file or a string
containing YAML-formatted text.
Returns
-------
A dictionary representation of the loaded YAML content.
Raises
------
ValueError
If the loaded YAML content is not a dictionary.
Notes
-----
If `data` is a Path object or a string that ends with ".yaml" or ".yml", it is
treated as a filepath and this function loads the file. Otherwise, `data` is a
string that does _not_ end in ".yaml" or ".yml" and it is treated as YAML-formatted
text which is loaded directly into a dictionary.
"""
if (isinstance(data, str) and data.endswith((".yaml", ".yml"))) or isinstance(data, Path):
# 'data' is a filepath to a yaml file (rather than a yaml string)
with open(data) as f:
data = f.read()
data_dict: dict[str, Any] = yaml.load(data, Loader=SafeLoader)
if not isinstance(data_dict, dict):
raise ValueError(
f"Loaded yaml file {data_dict} should be a dictionary but is type {type(data_dict)}"
)
return data_dict
[docs]
class SafeLoader(yaml.SafeLoader):
"""A yaml.SafeLoader that restricts duplicate keys."""
[docs]
def construct_mapping(
self, node: yaml.nodes.MappingNode, deep: bool = False
) -> dict[Hashable, Any]:
"""Construct the standard mapping after checking for duplicates.
Raises
------
DuplicatedConfigurationError
If duplicate keys within the same level are detected in the YAML file
being loaded.
Notes
-----
A key is considered a duplicate only if it is the same as another key
*at the same level in the YAML*.
This raises upon the *first* duplicate key found; other duplicates may exist
(in which case a new error will be raised upon re-loading of the YAML file
once the duplicate is resolved).
"""
mapping = []
for key_node, _value_node in node.value:
key = self.construct_object(key_node, deep=deep)
if key in mapping:
raise DuplicatedConfigurationError(
f"Duplicate key detected at same level of YAML: {key}. Resolve duplicates and try again.",
name=f"duplicated_{key}",
layer=None,
source=None,
value=None,
)
mapping.append(key)
return super().construct_mapping(node, deep)