Source code for layered_config_tree.utilities

"""
=========
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)