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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""This module focus in the top level data layer API of ConfigUpdater, i.e.
|
|
2
|
+
how to access and modify the sections of the configurations.
|
|
3
|
+
|
|
4
|
+
Differently from :mod:`configparser`, the different aspects of the ConfigUpdater API are
|
|
5
|
+
split between several modules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Iterator, MutableMapping
|
|
11
|
+
from configparser import (
|
|
12
|
+
ConfigParser,
|
|
13
|
+
DuplicateSectionError,
|
|
14
|
+
NoOptionError,
|
|
15
|
+
NoSectionError,
|
|
16
|
+
)
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, Self, TypeVar, overload
|
|
19
|
+
|
|
20
|
+
from .block import Comment, Space
|
|
21
|
+
from .container import Container
|
|
22
|
+
from .option import Option
|
|
23
|
+
from .section import Section
|
|
24
|
+
|
|
25
|
+
# Used in parser getters to indicate the default behaviour when a specific
|
|
26
|
+
# option is not found it to raise an exception. Created to enable 'None' as
|
|
27
|
+
# a valid fallback value.
|
|
28
|
+
_UniqueValues = Enum("_UniqueValues", "_UNSET")
|
|
29
|
+
_UNSET = _UniqueValues._UNSET
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
ConfigContent = Section | Comment | Space
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Document(Container[ConfigContent], MutableMapping[str, Section]):
|
|
37
|
+
"""Access to the data manipulation API from **ConfigUpdater**.
|
|
38
|
+
|
|
39
|
+
A ``Document`` object tries to implement a familiar *dict-like* interface,
|
|
40
|
+
via :class:`MutableMapping`. However, it also tries to be as compatible as possible
|
|
41
|
+
with the stdlib's :class:`~configparser.ConfigParser`. This means that there are a
|
|
42
|
+
few methods that will work differently from what users familiar with *dict-like*
|
|
43
|
+
interfaces would expect. The most notable example is :meth:`get`.
|
|
44
|
+
|
|
45
|
+
A important difference between ConfigUpdater's ``Document`` model and
|
|
46
|
+
:class:`~configparser.ConfigParser` is the behaviour of the :class:`Section`
|
|
47
|
+
objects.
|
|
48
|
+
If we represent the type of a *dict-like* (or :class:`MutableMapping`) object by
|
|
49
|
+
``M[K, V]``, where ``K`` is the type of its keys and ``V`` is the type of the
|
|
50
|
+
associated values, ConfigUpdater's sections would be equivalent to
|
|
51
|
+
``M[str, Option]``, while ConfigParser's would be ``M[str, str]``.
|
|
52
|
+
|
|
53
|
+
This means that when you try to access a key inside a section in ConfigUpdater, you
|
|
54
|
+
are going to receive a :class:`Option` object, not its value.
|
|
55
|
+
To access the value of the option you need to call :class:`Option.value`.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def _get_section_idx(self, name: str) -> int:
|
|
59
|
+
return next(
|
|
60
|
+
i
|
|
61
|
+
for i, entry in enumerate(self._structure)
|
|
62
|
+
if isinstance(entry, Section) and entry.name == name
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def optionxform(self, optionstr: str) -> str:
|
|
66
|
+
"""Converts an option key for unification
|
|
67
|
+
|
|
68
|
+
By default it uses :meth:`str.lower`, which means that ConfigUpdater will
|
|
69
|
+
compare options in a case insensitive way.
|
|
70
|
+
|
|
71
|
+
This implementation mimics ConfigParser API, and can be configured as described
|
|
72
|
+
in :meth:`configparser.ConfigParser.optionxform`.
|
|
73
|
+
|
|
74
|
+
Warning:
|
|
75
|
+
As in ConfigParser, a custom ``optionxform`` must be idempotent, i.e.
|
|
76
|
+
``optionxform(optionxform(key)) == optionxform(key)``. The keys that
|
|
77
|
+
ConfigUpdater returns, e.g. when iterating over a section, are already
|
|
78
|
+
transformed and are transformed again when they are used for a lookup.
|
|
79
|
+
:meth:`str.lower`, :meth:`str.upper` and :class:`str` (to keep keys
|
|
80
|
+
case-sensitive) satisfy this, while something like
|
|
81
|
+
``lambda key: "prefix_" + key`` does not.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
optionstr (str): key name
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
str: unified option name
|
|
88
|
+
"""
|
|
89
|
+
return optionstr.lower()
|
|
90
|
+
|
|
91
|
+
def validate_format(self, **kwargs: Any) -> bool:
|
|
92
|
+
"""Call ConfigParser to validate config
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
kwargs: are passed to :class:`configparser.ConfigParser`
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
configparser.ParsingError: if syntax errors are found
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
``True`` when no error is found
|
|
102
|
+
"""
|
|
103
|
+
kwargs.pop("space_around_delimiters", None)
|
|
104
|
+
parser = ConfigParser(**kwargs)
|
|
105
|
+
parser.read_string(str(self))
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def iter_sections(self) -> Iterator[Section]:
|
|
109
|
+
"""Iterate only over section blocks"""
|
|
110
|
+
return (block for block in self._structure if isinstance(block, Section))
|
|
111
|
+
|
|
112
|
+
def section_blocks(self) -> list[Section]:
|
|
113
|
+
"""Returns all section blocks
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
list: list of :class:`Section` blocks
|
|
117
|
+
"""
|
|
118
|
+
return list(self.iter_sections())
|
|
119
|
+
|
|
120
|
+
def sections(self) -> list[str]:
|
|
121
|
+
"""Return a list of section names
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
list: list of section names
|
|
125
|
+
"""
|
|
126
|
+
return [section.name for section in self.iter_sections()]
|
|
127
|
+
|
|
128
|
+
def __iter__(self) -> Iterator[str]:
|
|
129
|
+
return (b.name for b in self.iter_blocks() if isinstance(b, Section))
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
return "".join(str(block) for block in self._structure)
|
|
133
|
+
|
|
134
|
+
def __getitem__(self, key: str) -> Section:
|
|
135
|
+
for section in self.section_blocks():
|
|
136
|
+
if section.name == key:
|
|
137
|
+
return section
|
|
138
|
+
|
|
139
|
+
raise KeyError(f"No section `{key}` found", {"key": key})
|
|
140
|
+
|
|
141
|
+
def __setitem__(self, key: str, value: Section) -> None:
|
|
142
|
+
if not isinstance(value, Section):
|
|
143
|
+
raise ValueError("Value must be of type Section!")
|
|
144
|
+
if key in self:
|
|
145
|
+
idx = self._get_section_idx(key)
|
|
146
|
+
self.__getitem__(key).detach()
|
|
147
|
+
value.attach(self)
|
|
148
|
+
self._structure.insert(idx, value)
|
|
149
|
+
else:
|
|
150
|
+
if value.name != key:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"Set key `{key}` does not equal option key `{value.name}`"
|
|
153
|
+
)
|
|
154
|
+
self.add_section(value)
|
|
155
|
+
|
|
156
|
+
def __delitem__(self, key: str) -> None:
|
|
157
|
+
if not self.remove_section(key):
|
|
158
|
+
raise KeyError(f"No section `{key}` found", {"key": key})
|
|
159
|
+
|
|
160
|
+
# MutableMapping[str, Section] accepts key: object, which is fine here since
|
|
161
|
+
# the key is only compared with the section names
|
|
162
|
+
def __contains__(self, key: object) -> bool:
|
|
163
|
+
"""Returns whether the given section exists.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
key (str): name of section
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
bool: whether the section exists
|
|
170
|
+
"""
|
|
171
|
+
return next((True for s in self.iter_sections() if s.name == key), False)
|
|
172
|
+
|
|
173
|
+
has_section = __contains__
|
|
174
|
+
|
|
175
|
+
def __eq__(self, other: object) -> bool:
|
|
176
|
+
if isinstance(other, self.__class__):
|
|
177
|
+
return self._structure == other._structure
|
|
178
|
+
else:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def clear(self) -> None:
|
|
182
|
+
for block in self._structure:
|
|
183
|
+
block.detach()
|
|
184
|
+
self._structure.clear()
|
|
185
|
+
|
|
186
|
+
def add_section(self, section: str | Section) -> None:
|
|
187
|
+
"""Create a new section in the configuration.
|
|
188
|
+
|
|
189
|
+
Raise DuplicateSectionError if a section by the specified name
|
|
190
|
+
already exists. Raise ValueError if name is DEFAULT.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
section (str or :class:`Section`): name or Section type
|
|
194
|
+
"""
|
|
195
|
+
if isinstance(section, str):
|
|
196
|
+
# create a new section
|
|
197
|
+
section_obj = Section(section)
|
|
198
|
+
elif isinstance(section, Section):
|
|
199
|
+
section_obj = section
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError("Parameter must be a string or Section type!")
|
|
202
|
+
|
|
203
|
+
if self.has_section(section_obj.name):
|
|
204
|
+
raise DuplicateSectionError(section_obj.name)
|
|
205
|
+
|
|
206
|
+
# Ensure a newline before the new section if the last block
|
|
207
|
+
# doesn't end with one (e.g. file without trailing newline)
|
|
208
|
+
if self._structure and not str(self._structure[-1]).endswith("\n"):
|
|
209
|
+
space = Space(container=self)
|
|
210
|
+
space.add_line("\n")
|
|
211
|
+
self._structure.append(space)
|
|
212
|
+
|
|
213
|
+
section_obj.attach(self)
|
|
214
|
+
self._structure.append(section_obj)
|
|
215
|
+
|
|
216
|
+
def options(self, section: str) -> list[str]:
|
|
217
|
+
"""Returns list of configuration options for the named section.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
section (str): name of section
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
list: list of option names
|
|
224
|
+
"""
|
|
225
|
+
if not self.has_section(section):
|
|
226
|
+
raise NoSectionError(section) from None
|
|
227
|
+
return self.__getitem__(section).options()
|
|
228
|
+
|
|
229
|
+
# The following is a pragmatic violation of Liskov substitution principle:
|
|
230
|
+
# As dicts, Mappings should have get(self, key: str, default: T) -> T
|
|
231
|
+
# but ConfigParser overwrites it and uses the function to offer a different
|
|
232
|
+
# functionality
|
|
233
|
+
@overload # type: ignore[override]
|
|
234
|
+
def get(self, section: str, option: str) -> Option: ...
|
|
235
|
+
|
|
236
|
+
@overload
|
|
237
|
+
def get(self, section: str, option: str, fallback: T) -> Option | T: ...
|
|
238
|
+
|
|
239
|
+
def get(
|
|
240
|
+
self, section: str, option: str, fallback: T | _UniqueValues = _UNSET
|
|
241
|
+
) -> Option | T:
|
|
242
|
+
"""Gets an option object for a given section or a fallback value.
|
|
243
|
+
|
|
244
|
+
Warning:
|
|
245
|
+
Please notice this method works differently from what is expected of
|
|
246
|
+
:meth:`MutableMapping.get` (or :meth:`dict.get`).
|
|
247
|
+
Similarly to :meth:`configparser.ConfigParser.get`, will take least 2
|
|
248
|
+
arguments, and the second argument does not correspond to a default value.
|
|
249
|
+
|
|
250
|
+
This happens because this function is not designed to return a
|
|
251
|
+
:obj:`Section` of the :obj:`ConfigUpdater` document, but instead a nested
|
|
252
|
+
:obj:`Option`.
|
|
253
|
+
|
|
254
|
+
See :meth:`get_section`, if instead, you want to retrieve a :obj:`Section`.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
section (str): section name
|
|
258
|
+
option (str): option name
|
|
259
|
+
fallback (T): if the key is not found and fallback is provided, the
|
|
260
|
+
``fallback`` value will be returned. ``None`` is a valid fallback value.
|
|
261
|
+
|
|
262
|
+
.. attention::
|
|
263
|
+
When ``option`` is not present, the ``fallback`` value itself is
|
|
264
|
+
returned. If you want instead to obtain a new ``Option`` object
|
|
265
|
+
with a default value associated with it, you can try the following::
|
|
266
|
+
|
|
267
|
+
configupdater.get("section", "option", fallback=Option("name", value))
|
|
268
|
+
|
|
269
|
+
... which roughly corresponds to::
|
|
270
|
+
|
|
271
|
+
configupdater["section"].get("option", Option("name", value))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
:class:`NoSectionError`: if ``section`` cannot be found
|
|
276
|
+
:class:`NoOptionError`: if the option cannot be found and no ``fallback``
|
|
277
|
+
was given
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
:class:`Option` object holding key/value pair when it exists. Otherwise,
|
|
281
|
+
the value passed via the ``fallback`` argument itself (type ``T``).
|
|
282
|
+
"""
|
|
283
|
+
section_obj = self.get_section(section, _UNSET)
|
|
284
|
+
if section_obj is _UNSET:
|
|
285
|
+
raise NoSectionError(section) from None
|
|
286
|
+
|
|
287
|
+
value = section_obj.get(option, fallback)
|
|
288
|
+
# ^ we checked section_obj against _UNSET, so we are sure about its type
|
|
289
|
+
|
|
290
|
+
if value is _UNSET:
|
|
291
|
+
raise NoOptionError(self.optionxform(option), section)
|
|
292
|
+
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
@overload
|
|
296
|
+
def get_section(self, name: str) -> Section | None: ...
|
|
297
|
+
|
|
298
|
+
@overload
|
|
299
|
+
def get_section(self, name: str, default: T) -> Section | T: ...
|
|
300
|
+
|
|
301
|
+
def get_section(self, name: str, default: T | None = None) -> Section | T | None:
|
|
302
|
+
"""This method works similarly to :meth:`dict.get`, and allows you
|
|
303
|
+
to retrieve an entire section by its name, or provide a ``default`` value in
|
|
304
|
+
case it cannot be found.
|
|
305
|
+
"""
|
|
306
|
+
return next((s for s in self.iter_sections() if s.name == name), default)
|
|
307
|
+
|
|
308
|
+
# The following is a pragmatic violation of Liskov substitution principle
|
|
309
|
+
# For some reason MutableMapping.items return a Set-like object
|
|
310
|
+
# but we want to preserve ordering
|
|
311
|
+
@overload # type: ignore[override]
|
|
312
|
+
def items(self) -> list[tuple[str, Section]]: ...
|
|
313
|
+
|
|
314
|
+
@overload
|
|
315
|
+
def items(self, section: str) -> list[tuple[str, Option]]: ...
|
|
316
|
+
|
|
317
|
+
def items(
|
|
318
|
+
self, section: str | _UniqueValues = _UNSET
|
|
319
|
+
) -> list[tuple[str, Section]] | list[tuple[str, Option]]:
|
|
320
|
+
"""Return a list of (name, value) tuples for options or sections.
|
|
321
|
+
|
|
322
|
+
If section is given, return a list of tuples with (name, value) for
|
|
323
|
+
each option in the section. Otherwise, return a list of tuples with
|
|
324
|
+
(section_name, section_type) for each section.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
section (str): optional section name, default UNSET
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
list: list of :class:`Section` or :class:`Option` objects
|
|
331
|
+
"""
|
|
332
|
+
if section is _UNSET:
|
|
333
|
+
return [(sect.name, sect) for sect in self.iter_sections()]
|
|
334
|
+
|
|
335
|
+
section_obj = self.__getitem__(section)
|
|
336
|
+
return [(opt.key, opt) for opt in section_obj.iter_options()]
|
|
337
|
+
|
|
338
|
+
def has_option(self, section: str, option: str) -> bool:
|
|
339
|
+
"""Checks for the existence of a given option in a given section.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
section (str): name of section
|
|
343
|
+
option (str): name of option
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
bool: whether the option exists in the given section
|
|
347
|
+
"""
|
|
348
|
+
return option in self.get_section(section, {})
|
|
349
|
+
|
|
350
|
+
def set(
|
|
351
|
+
self,
|
|
352
|
+
section: str,
|
|
353
|
+
option: str,
|
|
354
|
+
value: str | Iterable[str] | None = None,
|
|
355
|
+
) -> Self:
|
|
356
|
+
"""Set an option.
|
|
357
|
+
|
|
358
|
+
A new option keeps the given ``option`` name as
|
|
359
|
+
:attr:`~configupdater.Option.raw_key`, see :meth:`~configupdater.Section.set`.
|
|
360
|
+
Unlike :meth:`configparser.ConfigParser.set`, which writes the key transformed
|
|
361
|
+
by ``optionxform`` (lowercase by default), the file gets the given spelling.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
section: section name
|
|
365
|
+
option: option name
|
|
366
|
+
value: value, default None
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
section_obj = self.__getitem__(section)
|
|
370
|
+
except KeyError:
|
|
371
|
+
raise NoSectionError(section) from None
|
|
372
|
+
section_obj.set(option, value)
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
def remove_option(self, section: str, option: str) -> bool:
|
|
376
|
+
"""Remove an option.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
section (str): section name
|
|
380
|
+
option (str): option name
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
bool: whether the option was actually removed
|
|
384
|
+
"""
|
|
385
|
+
try:
|
|
386
|
+
section_obj = self.__getitem__(section)
|
|
387
|
+
except KeyError:
|
|
388
|
+
raise NoSectionError(section) from None
|
|
389
|
+
try:
|
|
390
|
+
del section_obj[option]
|
|
391
|
+
except KeyError:
|
|
392
|
+
return False
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
def remove_section(self, name: str) -> bool:
|
|
396
|
+
"""Remove a file section.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
name: name of the section
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
bool: whether the section was actually removed
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
idx = self._get_section_idx(name)
|
|
406
|
+
del self._structure[idx]
|
|
407
|
+
return True
|
|
408
|
+
except StopIteration:
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
def to_dict(self) -> dict[str, dict[str, str | None]]:
|
|
412
|
+
"""Transform to dictionary
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
dict: dictionary with same content
|
|
416
|
+
"""
|
|
417
|
+
return {sect.name: sect.to_dict() for sect in self.iter_sections()}
|
configupdater/option.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Options are the ultimate mean of configuration inside a configuration value.
|
|
2
|
+
|
|
3
|
+
They are always associated with a :attr:`~Option.key` (or the name of the configuration
|
|
4
|
+
parameter) and a :attr:`~Option.value`.
|
|
5
|
+
|
|
6
|
+
Options can also have multi-line values that are usually interpreted as a list of
|
|
7
|
+
values.
|
|
8
|
+
|
|
9
|
+
When editing configuration files with ConfigUpdater, a handy way of setting a multi-line
|
|
10
|
+
(or comma separated value) for an specific option is to use the
|
|
11
|
+
:meth:`~Option.set_values` method.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
from collections.abc import Iterable
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .document import Document
|
|
22
|
+
from .section import Section
|
|
23
|
+
|
|
24
|
+
from .block import AssignMultilineValueError, Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_multi_line(value: Any) -> bool:
|
|
28
|
+
"""Checks if a given value has multiple lines"""
|
|
29
|
+
if isinstance(value, str):
|
|
30
|
+
return "\n" in value
|
|
31
|
+
else:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NoneValueDisallowed(SyntaxWarning):
|
|
36
|
+
"""Warns that an option without a value is omitted from the output.
|
|
37
|
+
|
|
38
|
+
This happens when such an option is converted to a string while the
|
|
39
|
+
:class:`~configupdater.ConfigUpdater` does not use ``allow_no_value=True``.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def warn(cls, option: str) -> None:
|
|
44
|
+
msg = (
|
|
45
|
+
f"Cannot represent <{option} = None>, the option is omitted. "
|
|
46
|
+
"Please use ``allow_no_value=True`` with ``ConfigUpdater``."
|
|
47
|
+
)
|
|
48
|
+
warnings.warn(msg, cls, stacklevel=2)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Option(Block):
|
|
52
|
+
"""Option block holding a key/value pair.
|
|
53
|
+
|
|
54
|
+
``space_around_delimiters`` decides whether the delimiter is surrounded by spaces
|
|
55
|
+
when the option is rendered after a change. ``True`` and ``False`` take precedence
|
|
56
|
+
over the syntax options of the :class:`~configupdater.ConfigUpdater` holding the
|
|
57
|
+
option. ``None`` (the default) follows its ``space_around_delimiters`` and adds
|
|
58
|
+
spaces if the option does not belong to a :class:`~configupdater.ConfigUpdater`.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
key: str,
|
|
64
|
+
value: str | None = None,
|
|
65
|
+
container: Section | None = None,
|
|
66
|
+
delimiter: str = "=",
|
|
67
|
+
space_around_delimiters: bool | None = None,
|
|
68
|
+
line: str | None = None,
|
|
69
|
+
):
|
|
70
|
+
super().__init__(container=container)
|
|
71
|
+
self._key = key
|
|
72
|
+
self._values: list[str | None] = [] if value is None else [value]
|
|
73
|
+
self._value_is_none = value is None
|
|
74
|
+
self._delimiter = delimiter
|
|
75
|
+
self._value: str | None = None # will be filled after join_multiline_value
|
|
76
|
+
self._updated = False
|
|
77
|
+
self._multiline_value_joined = False
|
|
78
|
+
self._space_around_delimiters = space_around_delimiters
|
|
79
|
+
if line:
|
|
80
|
+
super().add_line(line)
|
|
81
|
+
if value is not None:
|
|
82
|
+
self._set_value(value)
|
|
83
|
+
|
|
84
|
+
def add_value(self, value: str | None) -> None:
|
|
85
|
+
"""PRIVATE: this function is not part of the public API of Option.
|
|
86
|
+
It is only used internally by other classes of the package during parsing.
|
|
87
|
+
"""
|
|
88
|
+
self._value_is_none = value is None
|
|
89
|
+
self._values.append(value)
|
|
90
|
+
|
|
91
|
+
def add_line(self, line: str) -> Self:
|
|
92
|
+
"""PRIVATE: this function is not part of the public API of Option.
|
|
93
|
+
It is only used internally by other classes of the package during parsing.
|
|
94
|
+
"""
|
|
95
|
+
super().add_line(line)
|
|
96
|
+
self.add_value(line.strip())
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def _join_multiline_value(self) -> None:
|
|
100
|
+
if not self._multiline_value_joined and not self._value_is_none:
|
|
101
|
+
# do what `_join_multiline_value` in ConfigParser would do
|
|
102
|
+
self._value = "\n".join(cast(list[str], self._values)).rstrip()
|
|
103
|
+
self._multiline_value_joined = True
|
|
104
|
+
|
|
105
|
+
def _get_delim(self, determine_suffix: bool = True) -> str:
|
|
106
|
+
document = self._document()
|
|
107
|
+
opts = getattr(document, "syntax_options", None) or {}
|
|
108
|
+
value = self._value
|
|
109
|
+
space = self._space_around_delimiters
|
|
110
|
+
if space is None:
|
|
111
|
+
space = opts.get("space_around_delimiters", True)
|
|
112
|
+
# no space is needed if we use multi-line arguments
|
|
113
|
+
suffix = "" if determine_suffix and str(value).startswith("\n") else " "
|
|
114
|
+
delim = f" {self._delimiter}{suffix}" if space else self._delimiter
|
|
115
|
+
return delim
|
|
116
|
+
|
|
117
|
+
def value_start_idx(self) -> int:
|
|
118
|
+
"""Index where the value of the option starts, good for indentation"""
|
|
119
|
+
delim = self._get_delim(determine_suffix=False)
|
|
120
|
+
return len(f"{self._key}{delim}")
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
if not self.updated:
|
|
124
|
+
return super().__str__()
|
|
125
|
+
|
|
126
|
+
document = self._document()
|
|
127
|
+
opts = getattr(document, "syntax_options", None) or {}
|
|
128
|
+
value = self._value
|
|
129
|
+
|
|
130
|
+
if value is None:
|
|
131
|
+
if document is None or opts.get("allow_no_value"):
|
|
132
|
+
return f"{self._key}\n"
|
|
133
|
+
NoneValueDisallowed.warn(self._key)
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
delim = self._get_delim()
|
|
137
|
+
return f"{self._key}{delim}{value}\n"
|
|
138
|
+
|
|
139
|
+
def __repr__(self) -> str:
|
|
140
|
+
return f"<Option: {self._key} = {self.value!r}>"
|
|
141
|
+
|
|
142
|
+
def _instantiate_copy(self) -> Self:
|
|
143
|
+
"""Will be called by :meth:`Block.__deepcopy__`"""
|
|
144
|
+
self._join_multiline_value()
|
|
145
|
+
return self.__class__(
|
|
146
|
+
self._key,
|
|
147
|
+
self._value,
|
|
148
|
+
container=None,
|
|
149
|
+
delimiter=self._delimiter,
|
|
150
|
+
space_around_delimiters=self._space_around_delimiters,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _document(self) -> Document | None:
|
|
154
|
+
if self._container is None:
|
|
155
|
+
return None
|
|
156
|
+
return cast("Section", self._container)._container
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def section(self) -> Section:
|
|
160
|
+
return cast("Section", self.container)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def key(self) -> str:
|
|
164
|
+
"""Key string associated with the option.
|
|
165
|
+
|
|
166
|
+
Please notice that the option key is normalized with
|
|
167
|
+
:meth:`~configupdater.document.Document.optionxform`.
|
|
168
|
+
|
|
169
|
+
When the option object is :obj:`detached <configupdater.block.Block.detach>`,
|
|
170
|
+
this method will raise a :obj:`NotAttachedError`.
|
|
171
|
+
"""
|
|
172
|
+
return self.section.document.optionxform(self._key)
|
|
173
|
+
|
|
174
|
+
@key.setter
|
|
175
|
+
def key(self, value: str) -> None:
|
|
176
|
+
self._join_multiline_value()
|
|
177
|
+
self._key = value
|
|
178
|
+
self._updated = True
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def raw_key(self) -> str:
|
|
182
|
+
"""Equivalent to :obj:`key`, but before applying :meth:`optionxform`."""
|
|
183
|
+
return self._key
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def value(self) -> str | None:
|
|
187
|
+
"""Value associated with the given option."""
|
|
188
|
+
self._join_multiline_value()
|
|
189
|
+
return self._value
|
|
190
|
+
|
|
191
|
+
@value.setter
|
|
192
|
+
def value(self, value: str | None) -> None:
|
|
193
|
+
if is_multi_line(value):
|
|
194
|
+
raise AssignMultilineValueError(self)
|
|
195
|
+
self._set_value(value)
|
|
196
|
+
|
|
197
|
+
def _set_value(self, value: str | None) -> None:
|
|
198
|
+
self._updated = True
|
|
199
|
+
self._multiline_value_joined = True
|
|
200
|
+
self._value = value
|
|
201
|
+
self._values = [value]
|
|
202
|
+
|
|
203
|
+
def as_list(self, separator: str = "\n") -> list[str]:
|
|
204
|
+
"""Returns the (multi-line/element) value as a list
|
|
205
|
+
|
|
206
|
+
Empty list if value is None, single-element list for a one-element
|
|
207
|
+
value and an element for each line in a multi-element value.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
separator (str): separator for values, default: line separator
|
|
211
|
+
"""
|
|
212
|
+
if self._value_is_none:
|
|
213
|
+
return []
|
|
214
|
+
else:
|
|
215
|
+
return [v.strip() for v in cast(str, self.value).strip().split(separator)]
|
|
216
|
+
|
|
217
|
+
def append(self, value: str, **kwargs: Any) -> Option:
|
|
218
|
+
"""Append a value to a mult-line value
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
value (str): value
|
|
222
|
+
kwargs: keyword arguments for `set_values`
|
|
223
|
+
"""
|
|
224
|
+
sep = kwargs.get("separator")
|
|
225
|
+
new_values = self.as_list() if sep is None else self.as_list(separator=sep)
|
|
226
|
+
|
|
227
|
+
new_values.append(value)
|
|
228
|
+
self.set_values(new_values, **kwargs)
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def set_values(
|
|
232
|
+
self,
|
|
233
|
+
values: Iterable[str],
|
|
234
|
+
separator: str = "\n",
|
|
235
|
+
indent: str | None = None,
|
|
236
|
+
prepend_newline: bool = True,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Sets the value to a given list of options, e.g. multi-line values
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
values (iterable): sequence of values
|
|
242
|
+
separator (str): separator for values, default: line separator
|
|
243
|
+
indent (optional str): indentation in case of line separator.
|
|
244
|
+
If prepend_newline is `True` 4 whitespaces by default, otherwise
|
|
245
|
+
determine automatically if `None`.
|
|
246
|
+
prepend_newline (bool): start with a new line or not, resp.
|
|
247
|
+
"""
|
|
248
|
+
values = list(values).copy()
|
|
249
|
+
self._updated = True
|
|
250
|
+
self._multiline_value_joined = True
|
|
251
|
+
self._values = cast(list[str | None], values)
|
|
252
|
+
|
|
253
|
+
if indent is None:
|
|
254
|
+
indent = 4 * " " if prepend_newline else self.value_start_idx() * " "
|
|
255
|
+
|
|
256
|
+
# The most common case of multiline values being preceded by a new line
|
|
257
|
+
if prepend_newline and "\n" in separator:
|
|
258
|
+
values = ["", *values]
|
|
259
|
+
separator = separator + indent
|
|
260
|
+
elif "\n" in separator:
|
|
261
|
+
separator = separator + indent
|
|
262
|
+
|
|
263
|
+
self._value = separator.join(values)
|