1from __future__ import annotations
2
3import copy
4
5from typing import Any
6from typing import Iterator
7
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
24
25
26_NOT_SET = object()
27
28
29class Container(_CustomDict):
30 """
31 A container for items within a TOMLDocument.
32
33 This class implements the `dict` interface with copy/deepcopy protocol.
34 """
35
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 = []
41
42 @property
43 def body(self) -> list[tuple[Key | None, Item]]:
44 return self._body
45
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
52
53 if isinstance(k, Key):
54 k = k.key
55
56 if hasattr(v, "unwrap"):
57 v = v.unwrap()
58
59 if k in unwrapped:
60 merge_dicts(unwrapped[k], v)
61 else:
62 unwrapped[k] = v
63
64 return unwrapped
65
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
73
74 k = k.key
75 v = v.value
76
77 if isinstance(v, Container):
78 v = v.value
79
80 if k in d:
81 merge_dicts(d[k], v)
82 else:
83 d[k] = v
84
85 return d
86
87 def parsing(self, parsing: bool) -> None:
88 self._parsed = parsing
89
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)
96
97 def add(self, key: Key | Item | str, item: Item | None = None) -> Container:
98 """
99 Adds an item to the current Container.
100
101 :Example:
102
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 )
113
114 key, item = None, key
115
116 return self.append(key, item)
117
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
129
130 last.sep = key.sep
131 current.append(last, value)
132
133 self.append(name, table)
134 return
135
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
141
142 if isinstance(v, Whitespace) and not v.is_fixed():
143 continue
144
145 if isinstance(v, (Table, AoT)) and not k.is_dotted():
146 break
147 last_index = i
148 return last_index + 1
149
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])
159
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)
166
167 if not isinstance(item, Item):
168 item = _item(item)
169
170 if key is not None and key.is_multi():
171 self._handle_dotted_key(key, item)
172 return self
173
174 if isinstance(item, (AoT, Table)) and item.name is None:
175 item.name = key.key
176
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"
188
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
193
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]
200
201 current = current_body_element[1]
202
203 if isinstance(item, Table):
204 if not isinstance(current, (Table, AoT)):
205 raise KeyAlreadyPresent(key)
206
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)
212
213 self._replace(key, key, current)
214 else:
215 current.append(item)
216
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)
222
223 current.append(item)
224
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)
238
239 if idx < len(self._body):
240 self._insert_at(idx, key, item)
241 else:
242 self._raw_append(key, item)
243
244 if validate:
245 self._validate_out_of_order_table(key)
246
247 return self
248
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)
260
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)
270
271 for table in item.body:
272 current.append(table)
273
274 return self
275 else:
276 raise KeyAlreadyPresent(key)
277
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()
290
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"
307
308 self._raw_append(key, item)
309 return self
310
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,)
316
317 current = self._body[current_idx[-1]][1]
318 if key is not None and not isinstance(current, Table):
319 raise KeyAlreadyPresent(key)
320
321 self._map[key] = (*current_idx, len(self._body))
322 elif key is not None:
323 self._map[key] = len(self._body)
324
325 self._body.append((key, item))
326 if item.is_table():
327 self._table_keys.append(key)
328
329 if key is not None:
330 dict.__setitem__(self, key.key, item.value)
331
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())
338
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)
350
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)
355
356 idx = self._map.pop(key, None)
357 if idx is None:
358 raise NonExistentKey(key)
359
360 if isinstance(idx, tuple):
361 for i in idx:
362 self._body[i] = (None, Null())
363 else:
364 self._body[idx] = (None, Null())
365
366 dict.__delitem__(self, key.key)
367
368 return self
369
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()")
375
376 if key not in self:
377 raise NonExistentKey(key)
378
379 if not isinstance(key, Key):
380 key = SingleKey(key)
381
382 if not isinstance(other_key, Key):
383 other_key = SingleKey(other_key)
384
385 item = _item(item)
386
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"
394
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
402
403 new_indices.append(v_)
404
405 self._map[k] = tuple(new_indices)
406 elif v > idx:
407 self._map[k] = v + 1
408
409 self._map[other_key] = idx + 1
410 self._body.insert(idx + 1, (other_key, item))
411
412 if key is not None:
413 dict.__setitem__(self, other_key.key, item.value)
414
415 return self
416
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}")
420
421 if not isinstance(key, Key):
422 key = SingleKey(key)
423
424 item = _item(item)
425
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"
435
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
443
444 new_indices.append(v_)
445
446 self._map[k] = tuple(new_indices)
447 elif v >= idx:
448 self._map[k] = v + 1
449
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))
458
459 dict.__setitem__(self, key.key, item.value)
460
461 return self
462
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)
467
468 idx = self._map.get(key)
469 if idx is None:
470 raise NonExistentKey(key)
471
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)
477
478 return self._body[idx][1]
479
480 def last_item(self) -> Item | None:
481 """Get the last item."""
482 if self._body:
483 return self._body[-1][1]
484
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)
510
511 return s
512
513 def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
514 cur = ""
515
516 if table.display_name is not None:
517 _key = table.display_name
518 else:
519 _key = key.as_string()
520
521 if prefix is not None:
522 _key = prefix + "." + _key
523
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 = "[[", "]]"
541
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
557
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 )
586
587 return cur
588
589 def _render_aot(self, key, aot, prefix=None):
590 _key = key.as_string()
591 if prefix is not None:
592 _key = prefix + "." + _key
593
594 cur = ""
595 _key = decode(_key)
596 for table in aot.body:
597 cur += self._render_aot_table(table, prefix=_key)
598
599 return cur
600
601 def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
602 cur = ""
603 _key = prefix or ""
604 open_, close = "[[", "]]"
605
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 )
615
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)
630
631 return cur
632
633 def _render_simple_item(self, key, item, prefix=None):
634 if key is None:
635 return item.as_string()
636
637 _key = key.as_string()
638 if prefix is not None:
639 _key = prefix + "." + _key
640
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 )
650
651 def __len__(self) -> int:
652 return dict.__len__(self)
653
654 def __iter__(self) -> Iterator[str]:
655 return iter(dict.keys(self))
656
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
662
663 return item
664
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)
671
672 def __delitem__(self, key: Key | str) -> None:
673 self.remove(key)
674
675 def setdefault(self, key: Key | str, default: Any) -> Any:
676 super().setdefault(key, default=default)
677 return self[key]
678
679 def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
680 if not isinstance(key, Key):
681 key = SingleKey(key)
682
683 idx = self._map.get(key)
684 if idx is None:
685 raise NonExistentKey(key)
686
687 self._replace_at(idx, new_key, value)
688
689 def _replace_at(
690 self, idx: int | tuple[int], new_key: Key | str, value: Item
691 ) -> None:
692 value = _item(value)
693
694 if isinstance(idx, tuple):
695 for i in idx[1:]:
696 self._body[i] = (None, Null())
697
698 idx = idx[0]
699
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
709
710 del self._map[k]
711 self._map[new_key] = idx
712 if new_key != k:
713 dict.__delitem__(self, k)
714
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)
734
735 if hasattr(value, "invalidate_display_name"):
736 value.invalidate_display_name() # type: ignore[attr-defined]
737
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"))
754
755 dict.__setitem__(self, new_key.key, value.value)
756
757 def __str__(self) -> str:
758 return str(self.value)
759
760 def __repr__(self) -> str:
761 return repr(self.value)
762
763 def __eq__(self, other: dict) -> bool:
764 if not isinstance(other, dict):
765 return NotImplemented
766
767 return self.value == other
768
769 def _getstate(self, protocol):
770 return (self._parsed,)
771
772 def __reduce__(self):
773 return self.__reduce_ex__(2)
774
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 )
781
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]
787
788 for key, item in self._body:
789 if key is not None:
790 dict.__setitem__(self, key.key, item.value)
791
792 def copy(self) -> Container:
793 return copy.copy(self)
794
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)
799
800 c._body += self.body
801 c._map.update(self._map)
802
803 return c
804
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
816
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
823
824
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]
833
834 if isinstance(item, Table):
835 for k, v in item.value.body:
836 temp_container.append(k, v, validate=False)
837
838 temp_container._validate_out_of_order_table()
839
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]] = {}
845
846 for i in indices:
847 _, item = self._container._body[i]
848
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)
859
860 self._internal_container._validate_out_of_order_table()
861
862 def unwrap(self) -> str:
863 return self._internal_container.unwrap()
864
865 @property
866 def value(self):
867 return self._internal_container.value
868
869 def __getitem__(self, key: Key | str) -> Any:
870 if key not in self._internal_container:
871 raise NonExistentKey(key)
872
873 return self._internal_container[key]
874
875 def __setitem__(self, key: Key | str, value: Any) -> None:
876 from .items import item
877
878 def _is_table_or_aot(it: Any) -> bool:
879 return isinstance(item(it), (Table, AoT))
880
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
908
909 self._internal_container[key] = value
910 if key is not None:
911 dict.__setitem__(self, key, value)
912
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
920
921 def __delitem__(self, key: Key | str) -> None:
922 if key not in self._tables_map:
923 raise NonExistentKey(key)
924
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)
930
931 del self._tables_map[key]
932 del self._internal_container[key]
933 if key is not None:
934 dict.__delitem__(self, key)
935
936 def __iter__(self) -> Iterator[str]:
937 return iter(dict.keys(self))
938
939 def __len__(self) -> int:
940 return dict.__len__(self)
941
942 def setdefault(self, key: Key | str, default: Any) -> Any:
943 super().setdefault(key, default=default)
944 return self[key]
945
946
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))