Coverage for src/configuraptor/loaders/loaders_shared.py: 100%

1"""

2 File loaders that work regardless of Python version.

3"""

4 import configparser

5 import json as json_lib

6 import typing

7 from collections import defaultdict

8 from pathlib import Path

9 from typing import BinaryIO

10

11 import tomli

12 import yaml as yaml_lib

13 from dotenv import dotenv_values

14

15 from ._types import T_config, as_tconfig

16 from .register import register_loader

17

18

19@register_loader

20 def json(f: BinaryIO, _: typing.Optional[Path]) -> T_config:

21 """

22 Load a JSON file.

23 """

24 data = json_lib.load(f)

25 return as_tconfig(data)

26

27

28@register_loader(".yaml", ".yml")

29 def yaml(f: BinaryIO, _: typing.Optional[Path]) -> typing.Any:

30 """

31 Load a YAML file.

32 """

33 return yaml_lib.load(f, yaml_lib.SafeLoader)

34

35

36@register_loader

37 def toml(f: BinaryIO, _: typing.Optional[Path]) -> typing.Any:

38 """

39 Load a toml file.

40 """

41 return tomli.load(f)

42

43

44@register_loader(".env")

45 def dotenv(_: typing.Optional[BinaryIO], fullpath: Path) -> typing.Any:

46 """

47 Load a toml file.

48 """

49 return dotenv_values(fullpath)

50

51

52 def _convert_key(key: str) -> str:

53 return key.replace(" ", "_").replace("-", "_")

54

55

56 def _convert_value(value: str) -> str:

57 if value.startswith('"') and value.endswith('"'):

58 value = value.removeprefix('"').removesuffix('"')

59 return value

60

61

62 RecursiveDict = dict[str, typing.Union[str, "RecursiveDict"]]

63

64

65@register_loader

66 def ini(_: typing.Optional[BinaryIO], fullpath: Path) -> typing.Any:

67 """

68 Load an ini file.

69 """

70 config = configparser.ConfigParser()

71 config.read(fullpath)

72

73 final_data: defaultdict[str, RecursiveDict] = defaultdict(dict)

74 for section in config.sections():

75 data: RecursiveDict = {_convert_key(k): _convert_value(v) for k, v in dict(config[section]).items()}

76 section = _convert_key(section)

77 if "." in section:

78 _section = _current = {} # type: ignore

79 for part in section.split("."):

80 _current[part] = _current.get(part) or {}

81 _current = _current[part]

82

83 # nested structure is set up, now load the right data into it:

84 _current |= data

85 final_data |= _section

86 else:

87 final_data[section] = data

88

89 return dict(final_data)