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

48 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-09-18 12:33 +0200

1""" 

2File loaders that work regardless of Python version. 

3""" 

4import configparser 

5import json as json_lib 

6import typing 

7from collections import defaultdict 

8from pathlib import Path 

9from typing import BinaryIO 

10 

11import tomli 

12import yaml as yaml_lib 

13from dotenv import dotenv_values 

14 

15from ._types import T_config, as_tconfig 

16from .register import register_loader 

17 

18 

19@register_loader 

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

29def 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 

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

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

46 """ 

47 Load a toml file. 

48 """ 

49 return dotenv_values(fullpath) 

50 

51 

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

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

54 

55 

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

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

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

59 return value 

60 

61 

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

63 

64 

65@register_loader 

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