Coverage for src/configuraptor/dump.py: 100%

47 statements  

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

1""" 

2Method to dump classes to other formats. 

3""" 

4 

5import json 

6import typing 

7 

8import tomli_w 

9import yaml 

10 

11from .helpers import camel_to_snake, instance_of_custom_class, is_custom_class 

12from .loaders.register import register_dumper 

13 

14if typing.TYPE_CHECKING: # pragma: no cover 

15 from .binary_config import BinaryConfig 

16 

17PUBLIC = 0 # class.variable 

18PROTECTED = 1 # class._variable 

19PRIVATE = 2 # class.__variable 

20 

21T_Scope = typing.Literal[0, 1, 2] | bool 

22 

23 

24@register_dumper("dict") 

25def asdict( 

26 inst: typing.Any, _level: int = 0, /, with_top_level_key: bool = True, exclude_internals: T_Scope = 0 

27) -> dict[str, typing.Any]: 

28 """ 

29 Dump a config instance to a dictionary (recursively). 

30 """ 

31 data: dict[str, typing.Any] = {} 

32 

33 internals_prefix = f"_{inst.__class__.__name__}__" 

34 for key, value in inst.__dict__.items(): 

35 if exclude_internals == PROTECTED and key.startswith(internals_prefix): 

36 # skip _ and __ on level 2 

37 continue 

38 elif exclude_internals == PRIVATE and key.startswith("_"): 

39 # skip __ on level 1 

40 continue 

41 # else: skip nothing 

42 

43 cls = value.__class__ 

44 if is_custom_class(cls): 

45 value = asdict(value, _level + 1) 

46 elif isinstance(value, list): 

47 value = [asdict(_, _level + 1) if instance_of_custom_class(_) else _ for _ in value] 

48 elif isinstance(value, dict): 

49 value = {k: asdict(v, _level + 1) if instance_of_custom_class(v) else v for k, v in value.items()} 

50 

51 data[key] = value 

52 

53 if _level == 0 and with_top_level_key: 

54 # top-level: add an extra key indicating the class' name 

55 cls_name = camel_to_snake(inst.__class__.__name__) 

56 return {cls_name: data} 

57 

58 return data 

59 

60 

61@register_dumper("toml") 

62def astoml(inst: typing.Any, multiline_strings: bool = False, **kw: typing.Any) -> str: 

63 """ 

64 Dump a config instance to toml (recursively). 

65 """ 

66 data = asdict( 

67 inst, 

68 with_top_level_key=kw.pop("with_top_level_key", True), 

69 exclude_internals=kw.pop("exclude_internals", False), 

70 ) 

71 return tomli_w.dumps(data, multiline_strings=multiline_strings) 

72 

73 

74@register_dumper("json") 

75def asjson(inst: typing.Any, **kw: typing.Any) -> str: 

76 """ 

77 Dump a config instance to json (recursively). 

78 """ 

79 data = asdict( 

80 inst, 

81 with_top_level_key=kw.pop("with_top_level_key", True), 

82 exclude_internals=kw.pop("exclude_internals", False), 

83 ) 

84 return json.dumps(data, **kw) 

85 

86 

87@register_dumper("yaml") 

88def asyaml(inst: typing.Any, **kw: typing.Any) -> str: 

89 """ 

90 Dump a config instance to yaml (recursively). 

91 """ 

92 data = asdict( 

93 inst, 

94 with_top_level_key=kw.pop("with_top_level_key", True), 

95 exclude_internals=kw.pop("exclude_internals", False), 

96 ) 

97 output = yaml.dump(data, encoding=None, **kw) 

98 # output is already a str but mypy doesn't know that 

99 return typing.cast(str, output) 

100 

101 

102@register_dumper("bytes") 

103def asbytes(inst: "BinaryConfig", **_: typing.Any) -> bytes: 

104 """ 

105 Dumper for binary config to 'pack' into a bytestring. 

106 """ 

107 return inst._pack()