Coverage for src/configuraptor/loaders/loaders_shared.py: 100%
48 statements
« prev ^ index » next coverage.py v7.2.7, created at 2026-04-28 15:40 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2026-04-28 15:40 +0200
1"""
2File loaders that work regardless of Python version.
3"""
5import configparser
6import typing
7from collections import defaultdict
8from pathlib import Path
9from typing import BinaryIO
11import pyjson5 as json_lib
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(".json", ".json5")
21def json(f: BinaryIO, _: typing.Optional[Path]) -> T_config:
22 """
23 Load a JSON file.
24 """
25 data = json_lib.load(f) # type: ignore
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)