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

1"""

2 File loaders that work regardless of Python version.

3"""

4

5 import configparser

6 import json as json_lib

7 import typing

8 from collections import defaultdict

9 from pathlib import Path

10 from typing import BinaryIO

11

12 import tomli

13 import yaml as yaml_lib

14 from dotenv import dotenv_values

15

16 from ._types import T_config, as_tconfig

17 from .register import register_loader

18

19

20@register_loader

21 def 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)

27

28

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

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

31 """

32 Load a YAML file.

33 """

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

35

36

37@register_loader

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

39 """

40 Load a toml file.

41 """

42 return tomli.load(f)

43

44

45@register_loader(".env")

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

47 """

48 Load a toml file.

49 """

50 return dotenv_values(fullpath)

51

52

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

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

55

56

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

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

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

60 return value

61

62

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

64

65

66@register_loader

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

68 """

69 Load an ini file.

70 """

71 config = configparser.ConfigParser()

72 config.read(fullpath)

73

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]

83

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

89

90 return dict(final_data)