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.
@@ -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()