Passed
Push — main ( ed7d21...87238c )
by Douglas
01:43
created

ParseTools.parse_properties()   B

Complexity

Conditions 6

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 32
rs 8.6166
c 0
b 0
f 0
cc 6
nop 2
1
from collections.abc import Generator, Mapping, MutableMapping, Sequence
2
from typing import Any, Self
3
4
from tomlkit.items import AoT
5
6
from pocketutils import AlreadyUsedError, ParsingError
7
8
9
class ParseTools:
10
    @classmethod
11
    def dicts_to_toml_aot(cls: type[Self], dicts: Sequence[Mapping[str, Any]]) -> AoT:
12
        """
13
        Make a tomlkit Document consisting of an array of tables ("AOT").
14
15
        Args:
16
            dicts: A sequence of dictionaries
17
18
        Returns:
19
            A tomlkit`AoT<https://github.com/sdispater/tomlkit/blob/master/tomlkit/items.py>`_
20
            (i.e. `[[array]]`)
21
        """
22
        import tomlkit
23
24
        aot = tomlkit.aot()
25
        for ser in dicts:
26
            tab = tomlkit.table()
27
            aot.append(tab)
28
            for k, v in ser.items():
29
                tab.add(k, v)
30
            tab.add(tomlkit.nl())
31
        return aot
32
33
    @classmethod
34
    def dots_to_dict(cls: type[Self], items: Mapping[str, Any]) -> Mapping[str, Any]:
35
        """
36
        Make sub-dictionaries from substrings in `items` delimited by `.`.
37
        Used for TOML.
38
39
        Example:
40
            `Utils.dots_to_dict({"genus.species": "fruit bat"}) == {"genus": {"species": "fruit bat"}}`
41
42
        See Also:
43
            :meth:`dict_to_dots`
44
        """
45
        dct = {}
46
        cls._un_leaf(dct, items)
47
        return dct
48
49
    @classmethod
50
    def dict_to_dots(cls: type[Self], items: Mapping[str, Any]) -> Mapping[str, Any]:
51
        """
52
        Performs the inverse of :meth:`dots_to_dict`.
53
54
        Example:
55
            `Utils.dict_to_dots({"genus": {"species": "fruit bat"}}) == {"genus.species": "fruit bat"}`
56
        """
57
        return dict(cls._re_leaf("", items))
58
59
    @classmethod
60
    def _un_leaf(cls: type[Self], to: MutableMapping[str, Any], items: Mapping[str, Any]) -> None:
61
        for k, v in items.items():
62
            if "." not in k:
63
                to[k] = v
64
            else:
65
                k0, k1 = k.split(".", 1)
66
                if k0 not in to:
67
                    to[k0] = {}
68
                cls._un_leaf(to[k0], {k1: v})
69
70
    @classmethod
71
    def _re_leaf(cls: type[Self], at: str, items: Mapping[str, Any]) -> Generator[tuple[str, Any], None, None]:
72
        for k, v in items.items():
73
            me = at + "." + k if len(at) > 0 else k
74
            if hasattr(v, "items") and hasattr(v, "keys") and hasattr(v, "values"):
75
                yield from cls._re_leaf(me, v)
76
            else:
77
                yield me, v
78
79
    @classmethod
80
    def read_lines(cls: type[Self], data: str, *, ignore_comments: bool = False) -> Sequence[str]:
81
        """
82
        Returns a list of lines in the file.
83
        Optionally skips lines starting with `#` or that only contain whitespace.
84
        """
85
        lines = []
86
        for line in data.splitlines():
87
            line = line.strip()
88
            if not ignore_comments or not line.startswith("#") and len(line.strip()) != 0:
89
                lines.append(line)
90
        return lines
91
92
    @classmethod
93
    def parse_properties(cls: type[Self], data: str) -> Mapping[str, str]:
94
        """
95
        Reads a .properties file.
96
        A list of lines with key=value pairs (with an equals sign).
97
        Lines beginning with # are ignored.
98
        Each line must contain exactly 1 equals sign.
99
100
        .. caution::
101
            The escaping is not compliant with the standard
102
103
        Args:
104
            data: Data
105
106
        Returns:
107
            A dict mapping keys to values, both with surrounding whitespace stripped
108
        """
109
        dct = {}
110
        for i, line in enumerate(data.splitlines()):
111
            line = line.strip()
112
            if len(line) == 0 or line.startswith("#"):
113
                continue
114
            if line.count("=") != 1:
115
                msg = f"Bad line {i}: {line}"
116
                raise ParsingError(msg)
117
            k, v = line.split("=")
118
            k, v = k.strip(), v.strip()
119
            if k in dct:
120
                msg = f"Duplicate property {k} (line {i})"
121
                raise AlreadyUsedError(msg, key=k)
122
            dct[k] = v
123
        return dct
124
125
    @classmethod
126
    def write_properties(cls: type[Self], properties: Mapping[Any, Any]) -> str:
127
        """
128
        Writes lines of a .properties file.
129
130
        .. caution::
131
            The escaping is not compliant with the standard
132
        """
133
        bad_keys = []
134
        bad_values = []
135
        lines = []
136
        for k, v in properties.items():
137
            if "=" in k or "\n" in k:
138
                bad_keys.append(k)
139
            if "=" in v or "\n" in v:
140
                bad_values.append(k)
141
            lines.append(
142
                str(k).replace("=", "--").replace("\n", "\\n")
143
                + "="
144
                + str(v).replace("=", "--").replace("\n", "\\n")
145
                + "\n",
146
            )
147
        if len(bad_keys) > 0:
148
            msg = f"These keys containing '=' or \\n were escaped: {', '.join(bad_keys)}"
149
            raise ValueError(
150
                msg,
151
            )
152
        if len(bad_values) > 0:
153
            msg = f"These keys containing '=' or \\n were escaped: {', '.join(bad_values)}"
154
            raise ValueError(
155
                msg,
156
            )
157
        return "\n".join(lines)
158
159
160
__all__ = ["ParseTools"]
161