Coverage for src/configuraptor/loaders/loaders_shared.py: 100%
48 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-09 20:20 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2025-01-09 20:20 +0100
1"""
2File loaders that work regardless of Python version.
3"""
5import configparser
6import json as json_lib
7import typing
8from collections import defaultdict
9from pathlib import Path
10from typing import BinaryIO
12import tomli
13import yaml as yaml_lib
14from dotenv import dotenv_values
16from ._types import T_config, as_tconfig
17from .register import register_loader
20@register_loader
21def json(f: BinaryIO, _: typing.Optional[Path]) -> T_config:
22 """
23 Load a JSON file.
24 """
25 data = json_lib.load(f)
26 return as_tconfig(data)
29@register_loader(".yaml", ".yml")
30def yaml(f: BinaryIO, _: typing.Optional[Path]) -> typing.Any:
31 """
32 Load a YAML file.
33 """
34 return yaml_lib.load(f, yaml_lib.SafeLoader)
37@register_loader
38def toml(f: BinaryIO, _: typing.Optional[Path]) -> typing.Any:
39 """
40 Load a toml file.
41 """
42 return tomli.load(f)
45@register_loader(".env")
46def dotenv(_: typing.Optional[BinaryIO], fullpath: Path) -> typing.Any:
47 """
48 Load a toml file.
49 """
50 return dotenv_values(fullpath)
53def _convert_key(key: str) -> str:
54 return key.replace(" ", "_").replace("-", "_")
57def _convert_value(value: str) -> str:
58 if value.startswith('"') and value.endswith('"'):
59 value = value.removeprefix('"').removesuffix('"')
60 return value
63RecursiveDict = dict[str, typing.Union[str, "RecursiveDict"]]
66@register_loader
67def ini(_: typing.Optional[BinaryIO], fullpath: Path) -> typing.Any:
68 """
69 Load an ini file.
70 """
71 config = configparser.ConfigParser()
72 config.read(fullpath)
74 final_data: defaultdict[str, RecursiveDict] = defaultdict(dict)
75 for section in config.sections():
76 data: RecursiveDict = {_convert_key(k): _convert_value(v) for k, v in dict(config[section]).items()}
77 section = _convert_key(section)
78 if "." in section:
79 _section = _current = {} # type: ignore
80 for part in section.split("."):
81 _current[part] = _current.get(part) or {}
82 _current = _current[part]
84 # nested structure is set up, now load the right data into it:
85 _current |= data
86 final_data |= _section
87 else:
88 final_data[section] = data
90 return dict(final_data)