Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tomlkit/container.py: 64%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import copy
5from typing import Any
6from typing import Iterator
8from tomlkit._compat import decode
9from tomlkit._types import _CustomDict
10from tomlkit._utils import merge_dicts
11from tomlkit.exceptions import KeyAlreadyPresent
12from tomlkit.exceptions import NonExistentKey
13from tomlkit.exceptions import TOMLKitError
14from tomlkit.items import AoT
15from tomlkit.items import Comment
16from tomlkit.items import Item
17from tomlkit.items import Key
18from tomlkit.items import Null
19from tomlkit.items import SingleKey
20from tomlkit.items import Table
21from tomlkit.items import Trivia
22from tomlkit.items import Whitespace
23from tomlkit.items import item as _item
26_NOT_SET = object()
29class Container(_CustomDict):
30 """
31 A container for items within a TOMLDocument.
33 This class implements the `dict` interface with copy/deepcopy protocol.
34 """
36 def __init__(self, parsed: bool = False) -> None:
37 self._map: dict[SingleKey, int | tuple[int, ...]] = {}
38 self._body: list[tuple[Key | None, Item]] = []
39 self._parsed = parsed
40 self._table_keys = []
42 @property
43 def body(self) -> list[tuple[Key | None, Item]]:
44 return self._body
46 def unwrap(self) -> dict[str, Any]:
47 """Returns as pure python object (ppo)"""
48 unwrapped = {}
49 for k, v in self.items():
50 if k is None:
51 continue
53 if isinstance(k, Key):
54 k = k.key
56 if hasattr(v, "unwrap"):
57 v = v.unwrap()
59 if k in unwrapped:
60 merge_dicts(unwrapped[k], v)
61 else:
62 unwrapped[k] = v
64 return unwrapped
66 @property
67 def value(self) -> dict[str, Any]:
68 """The wrapped dict value"""
69 d = {}
70 for k, v in self._body:
71 if k is None:
72 continue
74 k = k.key
75 v = v.value
77 if isinstance(v, Container):
78 v = v.value
80 if k in d:
81 merge_dicts(d[k], v)
82 else:
83 d[k] = v
85 return d
87 def parsing(self, parsing: bool) -> None:
88 self._parsed = parsing
90 for _, v in self._body:
91 if isinstance(v, Table):
92 v.value.parsing(parsing)
93 elif isinstance(v, AoT):
94 for t in v.body:
95 t.value.parsing(parsing)
97 def add(self, key: Key | Item | str, item: Item | None = None) -> Container:
98 """
99 Adds an item to the current Container.
101 :Example:
103 >>> # add a key-value pair
104 >>> doc.add('key', 'value')
105 >>> # add a comment or whitespace or newline
106 >>> doc.add(comment('# comment'))
107 """
108 if item is None:
109 if not isinstance(key, (Comment, Whitespace)):
110 raise ValueError(
111 "Non comment/whitespace items must have an associated key"
112 )
114 key, item = None, key
116 return self.append(key, item)
118 def _handle_dotted_key(self, key: Key, value: Item) -> None:
119 if isinstance(value, (Table, AoT)):
120 raise TOMLKitError("Can't add a table to a dotted key")
121 name, *mid, last = key
122 name._dotted = True
123 table = current = Table(Container(True), Trivia(), False, is_super_table=True)
124 for _name in mid:
125 _name._dotted = True
126 new_table = Table(Container(True), Trivia(), False, is_super_table=True)
127 current.append(_name, new_table)
128 current = new_table
130 last.sep = key.sep
131 current.append(last, value)
133 self.append(name, table)
134 return
136 def _get_last_index_before_table(self) -> int:
137 last_index = -1
138 for i, (k, v) in enumerate(self._body):
139 if isinstance(v, Null):
140 continue # Null elements are inserted after deletion
142 if isinstance(v, Whitespace) and not v.is_fixed():
143 continue
145 if isinstance(v, (Table, AoT)) and not k.is_dotted():
146 break
147 last_index = i
148 return last_index + 1
150 def _validate_out_of_order_table(self, key: SingleKey | None = None) -> None:
151 if key is None:
152 for k in self._map:
153 assert k is not None
154 self._validate_out_of_order_table(k)
155 return
156 if key not in self._map or not isinstance(self._map[key], tuple):
157 return
158 OutOfOrderTableProxy.validate(self, self._map[key])
160 def append(
161 self, key: Key | str | None, item: Item, validate: bool = True
162 ) -> Container:
163 """Similar to :meth:`add` but both key and value must be given."""
164 if not isinstance(key, Key) and key is not None:
165 key = SingleKey(key)
167 if not isinstance(item, Item):
168 item = _item(item)
170 if key is not None and key.is_multi():
171 self._handle_dotted_key(key, item)
172 return self
174 if isinstance(item, (AoT, Table)) and item.name is None:
175 item.name = key.key
177 prev = self._previous_item()
178 prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev)
179 if isinstance(item, Table):
180 if not self._parsed:
181 item.invalidate_display_name()
182 if (
183 self._body
184 and not (self._parsed or item.trivia.indent or prev_ws)
185 and not key.is_dotted()
186 ):
187 item.trivia.indent = "\n"
189 if isinstance(item, AoT) and self._body and not self._parsed:
190 item.invalidate_display_name()
191 if item and not ("\n" in item[0].trivia.indent or prev_ws):
192 item[0].trivia.indent = "\n" + item[0].trivia.indent
194 if key is not None and key in self:
195 current_idx = self._map[key]
196 if isinstance(current_idx, tuple):
197 current_body_element = self._body[current_idx[-1]]
198 else:
199 current_body_element = self._body[current_idx]
201 current = current_body_element[1]
203 if isinstance(item, Table):
204 if not isinstance(current, (Table, AoT)):
205 raise KeyAlreadyPresent(key)
207 if item.is_aot_element():
208 # New AoT element found later on
209 # Adding it to the current AoT
210 if not isinstance(current, AoT):
211 current = AoT([current, item], parsed=self._parsed)
213 self._replace(key, key, current)
214 else:
215 current.append(item)
217 return self
218 elif current.is_aot():
219 if not item.is_aot_element():
220 # Tried to define a table after an AoT with the same name.
221 raise KeyAlreadyPresent(key)
223 current.append(item)
225 return self
226 elif current.is_super_table():
227 if item.is_super_table():
228 # We need to merge both super tables
229 if (
230 key.is_dotted()
231 or current_body_element[0].is_dotted()
232 or self._table_keys[-1] != current_body_element[0]
233 ):
234 if key.is_dotted() and not self._parsed:
235 idx = self._get_last_index_before_table()
236 else:
237 idx = len(self._body)
239 if idx < len(self._body):
240 self._insert_at(idx, key, item)
241 else:
242 self._raw_append(key, item)
244 if validate:
245 self._validate_out_of_order_table(key)
247 return self
249 # Create a new element to replace the old one
250 current = copy.deepcopy(current)
251 for k, v in item.value.body:
252 current.append(k, v)
253 self._body[
254 (
255 current_idx[-1]
256 if isinstance(current_idx, tuple)
257 else current_idx
258 )
259 ] = (current_body_element[0], current)
261 return self
262 elif current_body_element[0].is_dotted():
263 raise TOMLKitError("Redefinition of an existing table")
264 elif not item.is_super_table():
265 raise KeyAlreadyPresent(key)
266 elif isinstance(item, AoT):
267 if not isinstance(current, AoT):
268 # Tried to define an AoT after a table with the same name.
269 raise KeyAlreadyPresent(key)
271 for table in item.body:
272 current.append(table)
274 return self
275 else:
276 raise KeyAlreadyPresent(key)
278 is_table = isinstance(item, (Table, AoT))
279 if (
280 key is not None
281 and self._body
282 and not self._parsed
283 and (not is_table or key.is_dotted())
284 ):
285 # If there is already at least one table in the current container
286 # and the given item is not a table, we need to find the last
287 # item that is not a table and insert after it
288 # If no such item exists, insert at the top of the table
289 last_index = self._get_last_index_before_table()
291 if last_index < len(self._body):
292 after_item = self._body[last_index][1]
293 if not (
294 isinstance(after_item, Whitespace)
295 or "\n" in after_item.trivia.indent
296 ):
297 after_item.trivia.indent = "\n" + after_item.trivia.indent
298 return self._insert_at(last_index, key, item)
299 else:
300 previous_item = self._body[-1][1]
301 if not (
302 isinstance(previous_item, Whitespace)
303 or ends_with_whitespace(previous_item)
304 or "\n" in previous_item.trivia.trail
305 ):
306 previous_item.trivia.trail += "\n"
308 self._raw_append(key, item)
309 return self
311 def _raw_append(self, key: Key | None, item: Item) -> None:
312 if key in self._map:
313 current_idx = self._map[key]
314 if not isinstance(current_idx, tuple):
315 current_idx = (current_idx,)
317 current = self._body[current_idx[-1]][1]
318 if key is not None and not isinstance(current, Table):
319 raise KeyAlreadyPresent(key)
321 self._map[key] = (*current_idx, len(self._body))
322 elif key is not None:
323 self._map[key] = len(self._body)
325 self._body.append((key, item))
326 if item.is_table():
327 self._table_keys.append(key)
329 if key is not None:
330 dict.__setitem__(self, key.key, item.value)
332 def _remove_at(self, idx: int) -> None:
333 key = self._body[idx][0]
334 index = self._map.get(key)
335 if index is None:
336 raise NonExistentKey(key)
337 self._body[idx] = (None, Null())
339 if isinstance(index, tuple):
340 index = list(index)
341 index.remove(idx)
342 if len(index) == 1:
343 index = index.pop()
344 else:
345 index = tuple(index)
346 self._map[key] = index
347 else:
348 dict.__delitem__(self, key.key)
349 self._map.pop(key)
351 def remove(self, key: Key | str) -> Container:
352 """Remove a key from the container."""
353 if not isinstance(key, Key):
354 key = SingleKey(key)
356 idx = self._map.pop(key, None)
357 if idx is None:
358 raise NonExistentKey(key)
360 if isinstance(idx, tuple):
361 for i in idx:
362 self._body[i] = (None, Null())
363 else:
364 self._body[idx] = (None, Null())
366 dict.__delitem__(self, key.key)
368 return self
370 def _insert_after(
371 self, key: Key | str, other_key: Key | str, item: Any
372 ) -> Container:
373 if key is None:
374 raise ValueError("Key cannot be null in insert_after()")
376 if key not in self:
377 raise NonExistentKey(key)
379 if not isinstance(key, Key):
380 key = SingleKey(key)
382 if not isinstance(other_key, Key):
383 other_key = SingleKey(other_key)
385 item = _item(item)
387 idx = self._map[key]
388 # Insert after the max index if there are many.
389 if isinstance(idx, tuple):
390 idx = max(idx)
391 current_item = self._body[idx][1]
392 if "\n" not in current_item.trivia.trail:
393 current_item.trivia.trail += "\n"
395 # Increment indices after the current index
396 for k, v in self._map.items():
397 if isinstance(v, tuple):
398 new_indices = []
399 for v_ in v:
400 if v_ > idx:
401 v_ = v_ + 1
403 new_indices.append(v_)
405 self._map[k] = tuple(new_indices)
406 elif v > idx:
407 self._map[k] = v + 1
409 self._map[other_key] = idx + 1
410 self._body.insert(idx + 1, (other_key, item))
412 if key is not None:
413 dict.__setitem__(self, other_key.key, item.value)
415 return self
417 def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container:
418 if idx > len(self._body) - 1:
419 raise ValueError(f"Unable to insert at position {idx}")
421 if not isinstance(key, Key):
422 key = SingleKey(key)
424 item = _item(item)
426 if idx > 0:
427 previous_item = self._body[idx - 1][1]
428 if not (
429 isinstance(previous_item, Whitespace)
430 or ends_with_whitespace(previous_item)
431 or isinstance(item, (AoT, Table))
432 or "\n" in previous_item.trivia.trail
433 ):
434 previous_item.trivia.trail += "\n"
436 # Increment indices after the current index
437 for k, v in self._map.items():
438 if isinstance(v, tuple):
439 new_indices = []
440 for v_ in v:
441 if v_ >= idx:
442 v_ = v_ + 1
444 new_indices.append(v_)
446 self._map[k] = tuple(new_indices)
447 elif v >= idx:
448 self._map[k] = v + 1
450 if key in self._map:
451 current_idx = self._map[key]
452 if not isinstance(current_idx, tuple):
453 current_idx = (current_idx,)
454 self._map[key] = (*current_idx, idx)
455 else:
456 self._map[key] = idx
457 self._body.insert(idx, (key, item))
459 dict.__setitem__(self, key.key, item.value)
461 return self
463 def item(self, key: Key | str) -> Item:
464 """Get an item for the given key."""
465 if not isinstance(key, Key):
466 key = SingleKey(key)
468 idx = self._map.get(key)
469 if idx is None:
470 raise NonExistentKey(key)
472 if isinstance(idx, tuple):
473 # The item we are getting is an out of order table
474 # so we need a proxy to retrieve the proper objects
475 # from the parent container
476 return OutOfOrderTableProxy(self, idx)
478 return self._body[idx][1]
480 def last_item(self) -> Item | None:
481 """Get the last item."""
482 if self._body:
483 return self._body[-1][1]
485 def as_string(self) -> str:
486 """Render as TOML string."""
487 s = ""
488 for k, v in self._body:
489 if k is not None:
490 if isinstance(v, Table):
491 if (
492 s.strip(" ")
493 and not s.strip(" ").endswith("\n")
494 and "\n" not in v.trivia.indent
495 ):
496 s += "\n"
497 s += self._render_table(k, v)
498 elif isinstance(v, AoT):
499 if (
500 s.strip(" ")
501 and not s.strip(" ").endswith("\n")
502 and "\n" not in v.trivia.indent
503 ):
504 s += "\n"
505 s += self._render_aot(k, v)
506 else:
507 s += self._render_simple_item(k, v)
508 else:
509 s += self._render_simple_item(k, v)
511 return s
513 def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
514 cur = ""
516 if table.display_name is not None:
517 _key = table.display_name
518 else:
519 _key = key.as_string()
521 if prefix is not None:
522 _key = prefix + "." + _key
524 if (
525 not table.is_super_table()
526 or (
527 any(
528 not isinstance(v, (Table, AoT, Whitespace, Null))
529 for _, v in table.value.body
530 )
531 and not key.is_dotted()
532 )
533 or (
534 any(k.is_dotted() for k, v in table.value.body if isinstance(v, Table))
535 and not key.is_dotted()
536 )
537 ):
538 open_, close = "[", "]"
539 if table.is_aot_element():
540 open_, close = "[[", "]]"
542 newline_in_table_trivia = (
543 "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else ""
544 )
545 cur += (
546 f"{table.trivia.indent}"
547 f"{open_}"
548 f"{decode(_key)}"
549 f"{close}"
550 f"{table.trivia.comment_ws}"
551 f"{decode(table.trivia.comment)}"
552 f"{table.trivia.trail}"
553 f"{newline_in_table_trivia}"
554 )
555 elif table.trivia.indent == "\n":
556 cur += table.trivia.indent
558 for k, v in table.value.body:
559 if isinstance(v, Table):
560 if (
561 cur.strip(" ")
562 and not cur.strip(" ").endswith("\n")
563 and "\n" not in v.trivia.indent
564 ):
565 cur += "\n"
566 if v.is_super_table():
567 if k.is_dotted() and not key.is_dotted():
568 # Dotted key inside table
569 cur += self._render_table(k, v)
570 else:
571 cur += self._render_table(k, v, prefix=_key)
572 else:
573 cur += self._render_table(k, v, prefix=_key)
574 elif isinstance(v, AoT):
575 if (
576 cur.strip(" ")
577 and not cur.strip(" ").endswith("\n")
578 and "\n" not in v.trivia.indent
579 ):
580 cur += "\n"
581 cur += self._render_aot(k, v, prefix=_key)
582 else:
583 cur += self._render_simple_item(
584 k, v, prefix=_key if key.is_dotted() else None
585 )
587 return cur
589 def _render_aot(self, key, aot, prefix=None):
590 _key = key.as_string()
591 if prefix is not None:
592 _key = prefix + "." + _key
594 cur = ""
595 _key = decode(_key)
596 for table in aot.body:
597 cur += self._render_aot_table(table, prefix=_key)
599 return cur
601 def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
602 cur = ""
603 _key = prefix or ""
604 open_, close = "[[", "]]"
606 cur += (
607 f"{table.trivia.indent}"
608 f"{open_}"
609 f"{decode(_key)}"
610 f"{close}"
611 f"{table.trivia.comment_ws}"
612 f"{decode(table.trivia.comment)}"
613 f"{table.trivia.trail}"
614 )
616 for k, v in table.value.body:
617 if isinstance(v, Table):
618 if v.is_super_table():
619 if k.is_dotted():
620 # Dotted key inside table
621 cur += self._render_table(k, v)
622 else:
623 cur += self._render_table(k, v, prefix=_key)
624 else:
625 cur += self._render_table(k, v, prefix=_key)
626 elif isinstance(v, AoT):
627 cur += self._render_aot(k, v, prefix=_key)
628 else:
629 cur += self._render_simple_item(k, v)
631 return cur
633 def _render_simple_item(self, key, item, prefix=None):
634 if key is None:
635 return item.as_string()
637 _key = key.as_string()
638 if prefix is not None:
639 _key = prefix + "." + _key
641 return (
642 f"{item.trivia.indent}"
643 f"{decode(_key)}"
644 f"{key.sep}"
645 f"{decode(item.as_string())}"
646 f"{item.trivia.comment_ws}"
647 f"{decode(item.trivia.comment)}"
648 f"{item.trivia.trail}"
649 )
651 def __len__(self) -> int:
652 return dict.__len__(self)
654 def __iter__(self) -> Iterator[str]:
655 return iter(dict.keys(self))
657 # Dictionary methods
658 def __getitem__(self, key: Key | str) -> Item | Container:
659 item = self.item(key)
660 if isinstance(item, Item) and item.is_boolean():
661 return item.value
663 return item
665 def __setitem__(self, key: Key | str, value: Any) -> None:
666 if key is not None and key in self:
667 old_key = next(filter(lambda k: k == key, self._map))
668 self._replace(old_key, key, value)
669 else:
670 self.append(key, value)
672 def __delitem__(self, key: Key | str) -> None:
673 self.remove(key)
675 def setdefault(self, key: Key | str, default: Any) -> Any:
676 super().setdefault(key, default=default)
677 return self[key]
679 def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
680 if not isinstance(key, Key):
681 key = SingleKey(key)
683 idx = self._map.get(key)
684 if idx is None:
685 raise NonExistentKey(key)
687 self._replace_at(idx, new_key, value)
689 def _replace_at(
690 self, idx: int | tuple[int], new_key: Key | str, value: Item
691 ) -> None:
692 value = _item(value)
694 if isinstance(idx, tuple):
695 for i in idx[1:]:
696 self._body[i] = (None, Null())
698 idx = idx[0]
700 k, v = self._body[idx]
701 if not isinstance(new_key, Key):
702 if (
703 isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
704 or new_key != k.key
705 ):
706 new_key = SingleKey(new_key)
707 else: # Inherit the sep of the old key
708 new_key = k
710 del self._map[k]
711 self._map[new_key] = idx
712 if new_key != k:
713 dict.__delitem__(self, k)
715 if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)):
716 # new tables should appear after all non-table values
717 self.remove(k)
718 for i in range(idx, len(self._body)):
719 if isinstance(self._body[i][1], (AoT, Table)):
720 self._insert_at(i, new_key, value)
721 idx = i
722 break
723 else:
724 idx = -1
725 self.append(new_key, value)
726 else:
727 # Copying trivia
728 if not isinstance(value, (Whitespace, AoT)):
729 value.trivia.indent = v.trivia.indent
730 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws
731 value.trivia.comment = value.trivia.comment or v.trivia.comment
732 value.trivia.trail = v.trivia.trail
733 self._body[idx] = (new_key, value)
735 if hasattr(value, "invalidate_display_name"):
736 value.invalidate_display_name() # type: ignore[attr-defined]
738 if isinstance(value, Table):
739 # Insert a cosmetic new line for tables if:
740 # - it does not have it yet OR is not followed by one
741 # - it is not the last item, or
742 # - The table being replaced has a newline
743 last, _ = self._previous_item_with_index()
744 idx = last if idx < 0 else idx
745 has_ws = ends_with_whitespace(value)
746 replace_has_ws = (
747 isinstance(v, Table)
748 and v.value.body
749 and isinstance(v.value.body[-1][1], Whitespace)
750 )
751 next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace)
752 if (idx < last or replace_has_ws) and not (next_ws or has_ws):
753 value.append(None, Whitespace("\n"))
755 dict.__setitem__(self, new_key.key, value.value)
757 def __str__(self) -> str:
758 return str(self.value)
760 def __repr__(self) -> str:
761 return repr(self.value)
763 def __eq__(self, other: dict) -> bool:
764 if not isinstance(other, dict):
765 return NotImplemented
767 return self.value == other
769 def _getstate(self, protocol):
770 return (self._parsed,)
772 def __reduce__(self):
773 return self.__reduce_ex__(2)
775 def __reduce_ex__(self, protocol):
776 return (
777 self.__class__,
778 self._getstate(protocol),
779 (self._map, self._body, self._parsed, self._table_keys),
780 )
782 def __setstate__(self, state):
783 self._map = state[0]
784 self._body = state[1]
785 self._parsed = state[2]
786 self._table_keys = state[3]
788 for key, item in self._body:
789 if key is not None:
790 dict.__setitem__(self, key.key, item.value)
792 def copy(self) -> Container:
793 return copy.copy(self)
795 def __copy__(self) -> Container:
796 c = self.__class__(self._parsed)
797 for k, v in dict.items(self):
798 dict.__setitem__(c, k, v)
800 c._body += self.body
801 c._map.update(self._map)
803 return c
805 def _previous_item_with_index(
806 self, idx: int | None = None, ignore=(Null,)
807 ) -> tuple[int, Item] | None:
808 """Find the immediate previous item before index ``idx``"""
809 if idx is None or idx > len(self._body):
810 idx = len(self._body)
811 for i in range(idx - 1, -1, -1):
812 v = self._body[i][-1]
813 if not isinstance(v, ignore):
814 return i, v
815 return None
817 def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None:
818 """Find the immediate previous item before index ``idx``.
819 If ``idx`` is not given, the last item is returned.
820 """
821 prev = self._previous_item_with_index(idx, ignore)
822 return prev[-1] if prev else None
825class OutOfOrderTableProxy(_CustomDict):
826 @staticmethod
827 def validate(container: Container, indices: tuple[int, ...]) -> None:
828 """Validate out of order tables in the given container"""
829 # Append all items to a temp container to see if there is any error
830 temp_container = Container(True)
831 for i in indices:
832 _, item = container._body[i]
834 if isinstance(item, Table):
835 for k, v in item.value.body:
836 temp_container.append(k, v, validate=False)
838 temp_container._validate_out_of_order_table()
840 def __init__(self, container: Container, indices: tuple[int, ...]) -> None:
841 self._container = container
842 self._internal_container = Container(True)
843 self._tables: list[Table] = []
844 self._tables_map: dict[Key, list[int]] = {}
846 for i in indices:
847 _, item = self._container._body[i]
849 if isinstance(item, Table):
850 self._tables.append(item)
851 table_idx = len(self._tables) - 1
852 for k, v in item.value.body:
853 self._internal_container._raw_append(k, v)
854 indices = self._tables_map.setdefault(k, [])
855 if table_idx not in indices:
856 indices.append(table_idx)
857 if k is not None:
858 dict.__setitem__(self, k.key, v)
860 self._internal_container._validate_out_of_order_table()
862 def unwrap(self) -> str:
863 return self._internal_container.unwrap()
865 @property
866 def value(self):
867 return self._internal_container.value
869 def __getitem__(self, key: Key | str) -> Any:
870 if key not in self._internal_container:
871 raise NonExistentKey(key)
873 return self._internal_container[key]
875 def __setitem__(self, key: Key | str, value: Any) -> None:
876 from .items import item
878 def _is_table_or_aot(it: Any) -> bool:
879 return isinstance(item(it), (Table, AoT))
881 if key in self._tables_map:
882 # Overwrite the first table and remove others
883 indices = self._tables_map[key]
884 while len(indices) > 1:
885 table = self._tables[indices.pop()]
886 self._remove_table(table)
887 old_value = self._tables[indices[0]][key]
888 if _is_table_or_aot(old_value) and not _is_table_or_aot(value):
889 # Remove the entry from the map and set value again.
890 del self._tables[indices[0]][key]
891 del self._tables_map[key]
892 self[key] = value
893 return
894 self._tables[indices[0]][key] = value
895 elif self._tables:
896 if not _is_table_or_aot(value): # if the value is a plain value
897 for table in self._tables:
898 # find the first table that allows plain values
899 if any(not _is_table_or_aot(v) for _, v in table.items()):
900 table[key] = value
901 break
902 else:
903 self._tables[0][key] = value
904 else:
905 self._tables[0][key] = value
906 else:
907 self._container[key] = value
909 self._internal_container[key] = value
910 if key is not None:
911 dict.__setitem__(self, key, value)
913 def _remove_table(self, table: Table) -> None:
914 """Remove table from the parent container"""
915 self._tables.remove(table)
916 for idx, item in enumerate(self._container._body):
917 if item[1] is table:
918 self._container._remove_at(idx)
919 break
921 def __delitem__(self, key: Key | str) -> None:
922 if key not in self._tables_map:
923 raise NonExistentKey(key)
925 for i in reversed(self._tables_map[key]):
926 table = self._tables[i]
927 del table[key]
928 if not table and len(self._tables) > 1:
929 self._remove_table(table)
931 del self._tables_map[key]
932 del self._internal_container[key]
933 if key is not None:
934 dict.__delitem__(self, key)
936 def __iter__(self) -> Iterator[str]:
937 return iter(dict.keys(self))
939 def __len__(self) -> int:
940 return dict.__len__(self)
942 def setdefault(self, key: Key | str, default: Any) -> Any:
943 super().setdefault(key, default=default)
944 return self[key]
947def ends_with_whitespace(it: Any) -> bool:
948 """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object
949 ending with a ``Whitespace``.
950 """
951 return (
952 isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace)
953 ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))