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

17

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

19 """

20 Load a JSON file.

21 """

22 data = json_lib.load(f)

23 return as_tconfig(data)

24

25

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

27 """

28 Load a YAML file.

29 """

30 data = yaml_lib.load(f, yaml_lib.SafeLoader)

31 return as_tconfig(data)

32

33

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

35 """

36 Load a toml file.

37 """

38 data = tomli.load(f)

39 return as_tconfig(data)

40

41

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

43 """

44 Load a toml file.

45 """

46 data = dotenv_values(fullpath)

47 return as_tconfig(data)

48

49

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

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

52

53

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

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

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

57 return value

58

59

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

61

62

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

64 """

65 Load an ini file.

66 """

67 config = configparser.ConfigParser()

68 config.read(fullpath)

69

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

71 for section in config.sections():

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

73 section = _convert_key(section)

74 if "." in section:

75 _section = _current = {} # type: ignore

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

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

78 _current = _current[part]

79

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

81 _current |= data

82 final_data |= _section

83 else:

84 final_data[section] = data

85

86 return as_tconfig(dict(final_data))