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

1"""

2 Method to dump classes to other formats.

3"""

4

5 import json

6 import typing

7

8 import tomli_w

9 import yaml

10

11 from .helpers import camel_to_snake, instance_of_custom_class, is_custom_class

12 from .loaders.register import register_dumper

13

14 if typing.TYPE_CHECKING: # pragma: no cover

15 from .binary_config import BinaryConfig

16

17 PUBLIC = 0 # class.variable

18 PROTECTED = 1 # class._variable

19 PRIVATE = 2 # class.__variable

20

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

22

23

24@register_dumper("dict")

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

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

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

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

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

104 """

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

106 """

107 return inst._pack()