ConfigUpdater 4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- configupdater/__init__.py +18 -0
- configupdater/block.py +207 -0
- configupdater/builder.py +131 -0
- configupdater/configupdater.py +217 -0
- configupdater/container.py +88 -0
- configupdater/document.py +417 -0
- configupdater/option.py +263 -0
- configupdater/parser.py +576 -0
- configupdater/py.typed +0 -0
- configupdater/section.py +369 -0
- configupdater-4.0.dist-info/METADATA +349 -0
- configupdater-4.0.dist-info/RECORD +14 -0
- configupdater-4.0.dist-info/WHEEL +4 -0
- configupdater-4.0.dist-info/licenses/LICENSE.txt +289 -0
configupdater/section.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Sections are intermediate containers in **ConfigUpdater**'s data model for
|
|
2
|
+
configuration files.
|
|
3
|
+
|
|
4
|
+
They are at the same time :class:`containers <Container>` that hold :mod:`options
|
|
5
|
+
<Option>` and :class:`blocks <Block>` nested inside the top level configuration
|
|
6
|
+
:class:`~configupdater.document.Document`.
|
|
7
|
+
|
|
8
|
+
Note:
|
|
9
|
+
Please remember that :meth:`Section.get` method is implemented to mirror the
|
|
10
|
+
:meth:`ConfigParser API <configparser.ConfigParser.set>` and do not correspond to
|
|
11
|
+
the more usual :meth:`~collections.abc.Mapping.get` method of *dict-like* objects.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable, Iterator, MutableMapping
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Self, TypeVar, cast, overload
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .document import Document
|
|
21
|
+
|
|
22
|
+
from .block import Block, Comment, Space
|
|
23
|
+
from .builder import BlockBuilder
|
|
24
|
+
from .container import Container
|
|
25
|
+
from .option import Option
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
Content = Option | Comment | Space
|
|
30
|
+
Value = Option | str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Section(Block, Container[Content], MutableMapping[str, Option]):
|
|
34
|
+
"""Section block holding options"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, name: str, container: Document | None = None, raw_comment: str = ""
|
|
38
|
+
):
|
|
39
|
+
self._container: Document | None = container
|
|
40
|
+
self._name = name
|
|
41
|
+
self._raw_comment = raw_comment
|
|
42
|
+
self._structure: list[Content] = []
|
|
43
|
+
self._updated = False
|
|
44
|
+
super().__init__(container=container)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def document(self) -> Document:
|
|
48
|
+
return cast("Document", self.container)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def add_before_skip_comments(self) -> BlockBuilder:
|
|
52
|
+
"""Insert before this section and its preceding comment blocks.
|
|
53
|
+
|
|
54
|
+
Only consecutive comments in the document are skipped. A space or
|
|
55
|
+
another section stops the search; comments inside a previous section
|
|
56
|
+
are left in place. The existing :attr:`add_before` behavior is unchanged.
|
|
57
|
+
"""
|
|
58
|
+
idx = self.container_idx
|
|
59
|
+
structure = self.container.structure
|
|
60
|
+
while idx > 0 and isinstance(structure[idx - 1], Comment):
|
|
61
|
+
idx -= 1
|
|
62
|
+
return self._builder(idx)
|
|
63
|
+
|
|
64
|
+
def add_option(self, entry: Option) -> Self:
|
|
65
|
+
"""Add an Option object to the section
|
|
66
|
+
|
|
67
|
+
Used during initial parsing mainly
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
entry (Option): key value pair as Option object
|
|
71
|
+
"""
|
|
72
|
+
entry.attach(self)
|
|
73
|
+
self._structure.append(entry)
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def add_comment(self, line: str) -> Self:
|
|
77
|
+
"""Add a Comment object to the section
|
|
78
|
+
|
|
79
|
+
Used during initial parsing mainly
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
line (str): one line in the comment
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(self.last_block, Comment):
|
|
85
|
+
comment: Comment = self.last_block
|
|
86
|
+
else:
|
|
87
|
+
comment = Comment(container=self)
|
|
88
|
+
self._structure.append(comment)
|
|
89
|
+
|
|
90
|
+
comment.add_line(line)
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def add_space(self, line: str) -> Self:
|
|
94
|
+
"""Add a Space object to the section
|
|
95
|
+
|
|
96
|
+
Used during initial parsing mainly
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
line (str): one line that defines the space, maybe whitespaces
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(self.last_block, Space):
|
|
102
|
+
space = self.last_block
|
|
103
|
+
else:
|
|
104
|
+
space = Space(container=self)
|
|
105
|
+
self._structure.append(space)
|
|
106
|
+
|
|
107
|
+
space.add_line(line)
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
# The following private helpers expect a key that was already transformed with
|
|
111
|
+
# `optionxform`. Public methods transform the given key exactly once and then
|
|
112
|
+
# use them, so a key is never transformed twice.
|
|
113
|
+
def _get_option_idx(self, key: str) -> int:
|
|
114
|
+
return next(
|
|
115
|
+
i
|
|
116
|
+
for i, entry in enumerate(self._structure)
|
|
117
|
+
if isinstance(entry, Option) and entry.key == key
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _find_option(self, key: str) -> Option | None:
|
|
121
|
+
return next((o for o in self.iter_options() if o.key == key), None)
|
|
122
|
+
|
|
123
|
+
def __str__(self) -> str:
|
|
124
|
+
if not self.updated:
|
|
125
|
+
s = super().__str__()
|
|
126
|
+
if self._structure and not s.endswith("\n"):
|
|
127
|
+
s += "\n"
|
|
128
|
+
else:
|
|
129
|
+
s = f"[{self._name}]{self.raw_comment}\n"
|
|
130
|
+
for entry in self._structure:
|
|
131
|
+
s += str(entry)
|
|
132
|
+
return s
|
|
133
|
+
|
|
134
|
+
def __repr__(self) -> str:
|
|
135
|
+
return f"<Section: {self.name!r} {super()._repr_blocks()}>"
|
|
136
|
+
|
|
137
|
+
def _instantiate_copy(self) -> Self:
|
|
138
|
+
"""Will be called by :meth:`Block.__deepcopy__`"""
|
|
139
|
+
clone = self.__class__(self._name, container=None)
|
|
140
|
+
# ^ A fresh copy should always be made detached from any container
|
|
141
|
+
clone._raw_comment = self._raw_comment
|
|
142
|
+
return clone
|
|
143
|
+
|
|
144
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
|
|
145
|
+
clone = Block.__deepcopy__(self, memo) # specific due to multi-inheritance
|
|
146
|
+
return clone._copy_structure(self._structure, memo)
|
|
147
|
+
|
|
148
|
+
def __getitem__(self, key: str) -> Option:
|
|
149
|
+
key = self.document.optionxform(key)
|
|
150
|
+
option = self._find_option(key)
|
|
151
|
+
if option is None:
|
|
152
|
+
raise KeyError(f"No option `{key}` found", {"key": key})
|
|
153
|
+
return option
|
|
154
|
+
|
|
155
|
+
def __setitem__(self, key: str, value: Value | None = None) -> None:
|
|
156
|
+
"""Set the value of an option.
|
|
157
|
+
|
|
158
|
+
Please notice that this method uses
|
|
159
|
+
:meth:`~configupdater.document.Document.optionxform` to verify if the given
|
|
160
|
+
option already exists inside the section object. A new option keeps the
|
|
161
|
+
given ``key`` as :attr:`~configupdater.Option.raw_key`.
|
|
162
|
+
"""
|
|
163
|
+
# First we check for inconsistencies
|
|
164
|
+
given_key = self.document.optionxform(key)
|
|
165
|
+
if isinstance(value, Option):
|
|
166
|
+
value_key = self.document.optionxform(value.raw_key)
|
|
167
|
+
# ^ Calculate value_key according to the optionxform of the current
|
|
168
|
+
# document, in the case the option is imported from a document with a
|
|
169
|
+
# different optionxform
|
|
170
|
+
option = value
|
|
171
|
+
if value_key != given_key:
|
|
172
|
+
msg = f"Set key `{given_key}` does not equal option key `{value_key}`"
|
|
173
|
+
raise ValueError(msg)
|
|
174
|
+
else:
|
|
175
|
+
option = self.create_option(key, value)
|
|
176
|
+
|
|
177
|
+
curr_option = self._find_option(given_key)
|
|
178
|
+
if curr_option is not None: # Replace an existing option
|
|
179
|
+
if isinstance(value, Option):
|
|
180
|
+
idx = curr_option.container_idx
|
|
181
|
+
curr_option.detach()
|
|
182
|
+
value.attach(self)
|
|
183
|
+
self._structure.insert(idx, value)
|
|
184
|
+
else:
|
|
185
|
+
curr_option.value = value
|
|
186
|
+
else: # Append a new option
|
|
187
|
+
option.attach(self)
|
|
188
|
+
self._structure.append(option)
|
|
189
|
+
|
|
190
|
+
def __delitem__(self, key: str) -> None:
|
|
191
|
+
key = self.document.optionxform(key)
|
|
192
|
+
try:
|
|
193
|
+
idx = self._get_option_idx(key=key)
|
|
194
|
+
del self._structure[idx]
|
|
195
|
+
except StopIteration as ex:
|
|
196
|
+
raise KeyError(f"No option `{key}` found", {"key": key}) from ex
|
|
197
|
+
|
|
198
|
+
# MutableMapping[str, Option] accepts key: object. As in ConfigParser, the key is
|
|
199
|
+
# passed to optionxform, which usually expects a string.
|
|
200
|
+
def __contains__(self, key: object) -> bool:
|
|
201
|
+
"""Returns whether the given option exists.
|
|
202
|
+
|
|
203
|
+
Like ``section[key]``, this applies
|
|
204
|
+
:meth:`~configupdater.ConfigUpdater.optionxform` to ``key``.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
key (str): name of option
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
bool: whether the option exists
|
|
211
|
+
"""
|
|
212
|
+
return self._find_option(self.document.optionxform(cast(str, key))) is not None
|
|
213
|
+
|
|
214
|
+
def __eq__(self, other: object) -> bool:
|
|
215
|
+
if isinstance(other, self.__class__):
|
|
216
|
+
return self.name == other.name and self._structure == other._structure
|
|
217
|
+
else:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def __iter__(self) -> Iterator[str]:
|
|
221
|
+
return (b.key for b in self.iter_blocks() if isinstance(b, Option))
|
|
222
|
+
|
|
223
|
+
def iter_options(self) -> Iterator[Option]:
|
|
224
|
+
"""Iterate only over option blocks"""
|
|
225
|
+
return (entry for entry in self.iter_blocks() if isinstance(entry, Option))
|
|
226
|
+
|
|
227
|
+
def option_blocks(self) -> list[Option]:
|
|
228
|
+
"""Returns option blocks
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
list: list of :class:`~configupdater.option.Option` blocks
|
|
232
|
+
"""
|
|
233
|
+
return list(self.iter_options())
|
|
234
|
+
|
|
235
|
+
def options(self) -> list[str]:
|
|
236
|
+
"""Returns option names
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
list: list of option names as strings
|
|
240
|
+
"""
|
|
241
|
+
return [option.key for option in self.iter_options()]
|
|
242
|
+
|
|
243
|
+
has_option = __contains__
|
|
244
|
+
|
|
245
|
+
def to_dict(self) -> dict[str, str | None]:
|
|
246
|
+
"""Transform to dictionary
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
dict: dictionary with same content
|
|
250
|
+
"""
|
|
251
|
+
return {opt.key: opt.value for opt in self.iter_options()}
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def name(self) -> str:
|
|
255
|
+
"""Name of the section"""
|
|
256
|
+
return self._name
|
|
257
|
+
|
|
258
|
+
@name.setter
|
|
259
|
+
def name(self, value: str) -> None:
|
|
260
|
+
self._name = str(value)
|
|
261
|
+
self._updated = True
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def raw_comment(self) -> str:
|
|
265
|
+
"""Raw comment (includes comment mark) inline with the section header"""
|
|
266
|
+
return self._raw_comment
|
|
267
|
+
|
|
268
|
+
@raw_comment.setter
|
|
269
|
+
def raw_comment(self, value: str) -> None:
|
|
270
|
+
"""Add/replace a single comment inline with the section header.
|
|
271
|
+
The given value should be a raw comment, i.e. it needs to contain the
|
|
272
|
+
comment mark.
|
|
273
|
+
"""
|
|
274
|
+
self._raw_comment = value
|
|
275
|
+
self._updated = True
|
|
276
|
+
|
|
277
|
+
def set(self, option: str, value: str | Iterable[str] | None = None) -> Self:
|
|
278
|
+
"""Set an option for chaining.
|
|
279
|
+
|
|
280
|
+
A new option keeps the given ``option`` name as
|
|
281
|
+
:attr:`~configupdater.Option.raw_key`, like ``section[option] = value``.
|
|
282
|
+
Unlike :meth:`configparser.ConfigParser.set`, which writes the key transformed
|
|
283
|
+
by ``optionxform`` (lowercase by default), the file gets the given spelling.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
option: option name
|
|
287
|
+
value: value, default None
|
|
288
|
+
"""
|
|
289
|
+
opt = self._find_option(self.document.optionxform(option))
|
|
290
|
+
if opt is None:
|
|
291
|
+
opt = self.create_option(option)
|
|
292
|
+
self.add_option(opt)
|
|
293
|
+
if not isinstance(value, Iterable) or isinstance(value, str):
|
|
294
|
+
opt.value = value
|
|
295
|
+
else:
|
|
296
|
+
opt.set_values(value)
|
|
297
|
+
return self
|
|
298
|
+
|
|
299
|
+
def create_option(
|
|
300
|
+
self,
|
|
301
|
+
key: str,
|
|
302
|
+
value: str | None = None,
|
|
303
|
+
*,
|
|
304
|
+
delimiter: str | None = None,
|
|
305
|
+
space_around_delimiters: bool | None = None,
|
|
306
|
+
) -> Option:
|
|
307
|
+
"""Creates an option with kwargs that respect syntax options given to
|
|
308
|
+
the parent ConfigUpdater object (e.g. ``space_around_delimiters``).
|
|
309
|
+
|
|
310
|
+
``delimiter`` and ``space_around_delimiters`` override the syntax options
|
|
311
|
+
unless they are ``None``. With ``space_around_delimiters=None`` the option
|
|
312
|
+
keeps following the syntax options of the ConfigUpdater it belongs to.
|
|
313
|
+
|
|
314
|
+
Warning:
|
|
315
|
+
This is a low level API, not intended for public use.
|
|
316
|
+
Prefer :meth:`set` or :meth:`__setitem__`.
|
|
317
|
+
"""
|
|
318
|
+
syntax_opts = getattr(self._container, "syntax_options", {})
|
|
319
|
+
if delimiter is None:
|
|
320
|
+
delimiter = next(iter(syntax_opts.get("delimiters", [])), None)
|
|
321
|
+
kwargs_: dict[str, Any] = {
|
|
322
|
+
"value": value,
|
|
323
|
+
"container": self,
|
|
324
|
+
"space_around_delimiters": space_around_delimiters,
|
|
325
|
+
"delimiter": delimiter,
|
|
326
|
+
}
|
|
327
|
+
kwargs = {k: v for k, v in kwargs_.items() if v is not None}
|
|
328
|
+
return Option(key, **kwargs)
|
|
329
|
+
|
|
330
|
+
@overload
|
|
331
|
+
def get(self, key: str) -> Option | None: ...
|
|
332
|
+
|
|
333
|
+
@overload
|
|
334
|
+
def get(self, key: str, default: T) -> Option | T: ...
|
|
335
|
+
|
|
336
|
+
def get(self, key: str, default: T | None = None) -> Option | T | None:
|
|
337
|
+
"""This method works similarly to :meth:`dict.get`, and allows you
|
|
338
|
+
to retrieve an option object by its key.
|
|
339
|
+
|
|
340
|
+
Like ``section[key]``, this applies
|
|
341
|
+
:meth:`~configupdater.ConfigUpdater.optionxform` to ``key``.
|
|
342
|
+
"""
|
|
343
|
+
option = self._find_option(self.document.optionxform(key))
|
|
344
|
+
return default if option is None else option
|
|
345
|
+
|
|
346
|
+
# The following is a pragmatic violation of Liskov substitution principle
|
|
347
|
+
# For some reason MutableMapping.items return a Set-like object
|
|
348
|
+
# but we want to preserve ordering
|
|
349
|
+
def items(self) -> list[tuple[str, Option]]: # type: ignore[override]
|
|
350
|
+
"""Return a list of (name, option) tuples for each option in
|
|
351
|
+
this section.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
list: list of (name, :class:`Option`) tuples
|
|
355
|
+
"""
|
|
356
|
+
return [(opt.key, opt) for opt in self.option_blocks()]
|
|
357
|
+
|
|
358
|
+
def insert_at(self, idx: int) -> BlockBuilder:
|
|
359
|
+
"""Returns a builder inserting a new block at the given index
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
idx (int): index where to insert
|
|
363
|
+
"""
|
|
364
|
+
return BlockBuilder(self, idx)
|
|
365
|
+
|
|
366
|
+
def clear(self) -> None:
|
|
367
|
+
for block in self._structure:
|
|
368
|
+
block.detach()
|
|
369
|
+
self._structure.clear()
|