Coverage for src/configuraptor/dump.py: 100%
49 statements
« prev ^ index » next coverage.py v7.2.7, created at 2026-05-01 17:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2026-05-01 17:14 +0200
1"""
2Method to dump classes to other formats.
3"""
5import json
6import typing
8import tomli_w
9import yaml
11from .helpers import camel_to_snake, instance_of_custom_class, is_custom_class
12from .loaders.register import register_dumper
14if typing.TYPE_CHECKING: # pragma: no cover
15 from .binary_config import BinaryConfig
17PUBLIC = 0 # class.variable
18PROTECTED = 1 # class._variable
19PRIVATE = 2 # class.__variable
21T_Scope = typing.Literal[0, 1, 2] | bool
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] = {}
33 if not hasattr(inst, "__dict__"):
34 # weird type - skip
35 return {}
37 internals_prefix = f"_{inst.__class__.__name__}__"
38 for key, value in inst.__dict__.items():
39 if exclude_internals == PROTECTED and key.startswith(internals_prefix):
40 # skip _ and __ on level 2
41 continue
42 elif exclude_internals == PRIVATE and key.startswith("_"):
43 # skip __ on level 1
44 continue
45 # else: skip nothing
47 cls = value.__class__
48 if is_custom_class(cls):
49 value = asdict(value, _level + 1, exclude_internals=exclude_internals)
50 elif isinstance(value, list):
51 value = [
52 asdict(_, _level + 1, exclude_internals=exclude_internals) if instance_of_custom_class(_) else _
53 for _ in value
54 ]
55 elif isinstance(value, dict):
56 value = {
57 k: asdict(v, _level + 1, exclude_internals=exclude_internals) if instance_of_custom_class(v) else v
58 for k, v in value.items()
59 }
61 data[key] = value
63 if _level == 0 and with_top_level_key:
64 # top-level: add an extra key indicating the class' name
65 cls_name = camel_to_snake(inst.__class__.__name__)
66 return {cls_name: data}
68 return data
71@register_dumper("toml")
72def astoml(inst: typing.Any, multiline_strings: bool = False, **kw: typing.Any) -> str:
73 """
74 Dump a config instance to toml (recursively).
75 """
76 data = asdict(
77 inst,
78 with_top_level_key=kw.pop("with_top_level_key", True),
79 exclude_internals=kw.pop("exclude_internals", False),
80 )
81 return tomli_w.dumps(data, multiline_strings=multiline_strings)
84@register_dumper("json")
85def asjson(inst: typing.Any, **kw: typing.Any) -> str:
86 """
87 Dump a config instance to json (recursively).
88 """
89 data = asdict(
90 inst,
91 with_top_level_key=kw.pop("with_top_level_key", True),
92 exclude_internals=kw.pop("exclude_internals", False),
93 )
94 return json.dumps(data, **kw)
97@register_dumper("yaml")
98def asyaml(inst: typing.Any, **kw: typing.Any) -> str:
99 """
100 Dump a config instance to yaml (recursively).
101 """
102 data = asdict(
103 inst,
104 with_top_level_key=kw.pop("with_top_level_key", True),
105 exclude_internals=kw.pop("exclude_internals", False),
106 )
107 output = yaml.dump(data, encoding=None, **kw)
108 # output is already a str but mypy doesn't know that
109 return typing.cast(str, output)
112@register_dumper("bytes")
113def asbytes(inst: "BinaryConfig", **_: typing.Any) -> bytes:
114 """
115 Dumper for binary config to 'pack' into a bytestring.
116 """
117 return inst._pack()