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

48 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-09 20:07 +0100

1""" 

2File loaders that work regardless of Python version. 

3""" 

4 

5import configparser 

6import json as json_lib 

7import typing 

8from collections import defaultdict 

9from pathlib import Path 

10from typing import BinaryIO 

11 

12import tomli 

13import yaml as yaml_lib 

14from dotenv import dotenv_values 

15 

16from ._types import T_config, as_tconfig 

17from .register import register_loader 

18 

19 

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) 

27 

28 

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) 

35 

36 

37@register_loader 

38def 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") 

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

47 """ 

48 Load a toml file. 

49 """ 

50 return dotenv_values(fullpath) 

51 

52 

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

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

55 

56 

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

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

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

60 return value 

61 

62 

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

64 

65 

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) 

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)