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 return self._insert_at(last_index, key, item)
293 else:
294 previous_item = self._body[-1][1]
295 if not (
296 isinstance(previous_item, Whitespace)
297 or ends_with_whitespace(previous_item)
298 or "\n" in previous_item.trivia.trail
299 ):
300 previous_item.trivia.trail += "\n"
301
302 self._raw_append(key, item)
303 return self
304
305 def _raw_append(self, key: Key | None, item: Item) -> None:
306 if key in self._map:
307 current_idx = self._map[key]
308 if not isinstance(current_idx, tuple):
309 current_idx = (current_idx,)
310
311 current = self._body[current_idx[-1]][1]
312 if key is not None and not isinstance(current, Table):
313 raise KeyAlreadyPresent(key)
314
315 self._map[key] = (*current_idx, len(self._body))
316 elif key is not None:
317 self._map[key] = len(self._body)
318
319 self._body.append((key, item))
320 if item.is_table():
321 self._table_keys.append(key)
322
323 if key is not None:
324 dict.__setitem__(self, key.key, item.value)
325
326 def _remove_at(self, idx: int) -> None:
327 key = self._body[idx][0]
328 index = self._map.get(key)
329 if index is None:
330 raise NonExistentKey(key)
331 self._body[idx] = (None, Null())
332
333 if isinstance(index, tuple):
334 index = list(index)
335 index.remove(idx)
336 if len(index) == 1:
337 index = index.pop()
338 else:
339 index = tuple(index)
340 self._map[key] = index
341 else:
342 dict.__delitem__(self, key.key)
343 self._map.pop(key)
344
345 def remove(self, key: Key | str) -> Container:
346 """Remove a key from the container."""
347 if not isinstance(key, Key):
348 key = SingleKey(key)
349
350 idx = self._map.pop(key, None)
351 if idx is None:
352 raise NonExistentKey(key)
353
354 if isinstance(idx, tuple):
355 for i in idx:
356 self._body[i] = (None, Null())
357 else:
358 self._body[idx] = (None, Null())
359
360 dict.__delitem__(self, key.key)
361
362 return self
363
364 def _insert_after(
365 self, key: Key | str, other_key: Key | str, item: Any
366 ) -> Container:
367 if key is None:
368 raise ValueError("Key cannot be null in insert_after()")
369
370 if key not in self:
371 raise NonExistentKey(key)
372
373 if not isinstance(key, Key):
374 key = SingleKey(key)
375
376 if not isinstance(other_key, Key):
377 other_key = SingleKey(other_key)
378
379 item = _item(item)
380
381 idx = self._map[key]
382 # Insert after the max index if there are many.
383 if isinstance(idx, tuple):
384 idx = max(idx)
385 current_item = self._body[idx][1]
386 if "\n" not in current_item.trivia.trail:
387 current_item.trivia.trail += "\n"
388
389 # Increment indices after the current index
390 for k, v in self._map.items():
391 if isinstance(v, tuple):
392 new_indices = []
393 for v_ in v:
394 if v_ > idx:
395 v_ = v_ + 1
396
397 new_indices.append(v_)
398
399 self._map[k] = tuple(new_indices)
400 elif v > idx:
401 self._map[k] = v + 1
402
403 self._map[other_key] = idx + 1
404 self._body.insert(idx + 1, (other_key, item))
405
406 if key is not None:
407 dict.__setitem__(self, other_key.key, item.value)
408
409 return self
410
411 def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container:
412 if idx > len(self._body) - 1:
413 raise ValueError(f"Unable to insert at position {idx}")
414
415 if not isinstance(key, Key):
416 key = SingleKey(key)
417
418 item = _item(item)
419
420 if idx > 0:
421 previous_item = self._body[idx - 1][1]
422 if not (
423 isinstance(previous_item, Whitespace)
424 or ends_with_whitespace(previous_item)
425 or isinstance(item, (AoT, Table))
426 or "\n" in previous_item.trivia.trail
427 ):
428 previous_item.trivia.trail += "\n"
429
430 # Increment indices after the current index
431 for k, v in self._map.items():
432 if isinstance(v, tuple):
433 new_indices = []
434 for v_ in v:
435 if v_ >= idx:
436 v_ = v_ + 1
437
438 new_indices.append(v_)
439
440 self._map[k] = tuple(new_indices)
441 elif v >= idx:
442 self._map[k] = v + 1
443
444 if key in self._map:
445 current_idx = self._map[key]
446 if not isinstance(current_idx, tuple):
447 current_idx = (current_idx,)
448 self._map[key] = (*current_idx, idx)
449 else:
450 self._map[key] = idx
451 self._body.insert(idx, (key, item))
452
453 dict.__setitem__(self, key.key, item.value)
454
455 return self
456
457 def item(self, key: Key | str) -> Item:
458 """Get an item for the given key."""
459 if not isinstance(key, Key):
460 key = SingleKey(key)
461
462 idx = self._map.get(key)
463 if idx is None:
464 raise NonExistentKey(key)
465
466 if isinstance(idx, tuple):
467 # The item we are getting is an out of order table
468 # so we need a proxy to retrieve the proper objects
469 # from the parent container
470 return OutOfOrderTableProxy(self, idx)
471
472 return self._body[idx][1]
473
474 def last_item(self) -> Item | None:
475 """Get the last item."""
476 if self._body:
477 return self._body[-1][1]
478
479 def as_string(self) -> str:
480 """Render as TOML string."""
481 s = ""
482 for k, v in self._body:
483 if k is not None:
484 if isinstance(v, Table):
485 s += self._render_table(k, v)
486 elif isinstance(v, AoT):
487 s += self._render_aot(k, v)
488 else:
489 s += self._render_simple_item(k, v)
490 else:
491 s += self._render_simple_item(k, v)
492
493 return s
494
495 def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
496 cur = ""
497
498 if table.display_name is not None:
499 _key = table.display_name
500 else:
501 _key = key.as_string()
502
503 if prefix is not None:
504 _key = prefix + "." + _key
505
506 if not table.is_super_table() or (
507 any(
508 not isinstance(v, (Table, AoT, Whitespace, Null))
509 for _, v in table.value.body
510 )
511 and not key.is_dotted()
512 ):
513 open_, close = "[", "]"
514 if table.is_aot_element():
515 open_, close = "[[", "]]"
516
517 newline_in_table_trivia = (
518 "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else ""
519 )
520 cur += (
521 f"{table.trivia.indent}"
522 f"{open_}"
523 f"{decode(_key)}"
524 f"{close}"
525 f"{table.trivia.comment_ws}"
526 f"{decode(table.trivia.comment)}"
527 f"{table.trivia.trail}"
528 f"{newline_in_table_trivia}"
529 )
530 elif table.trivia.indent == "\n":
531 cur += table.trivia.indent
532
533 for k, v in table.value.body:
534 if isinstance(v, Table):
535 if v.is_super_table():
536 if k.is_dotted() and not key.is_dotted():
537 # Dotted key inside table
538 cur += self._render_table(k, v)
539 else:
540 cur += self._render_table(k, v, prefix=_key)
541 else:
542 cur += self._render_table(k, v, prefix=_key)
543 elif isinstance(v, AoT):
544 cur += self._render_aot(k, v, prefix=_key)
545 else:
546 cur += self._render_simple_item(
547 k, v, prefix=_key if key.is_dotted() else None
548 )
549
550 return cur
551
552 def _render_aot(self, key, aot, prefix=None):
553 _key = key.as_string()
554 if prefix is not None:
555 _key = prefix + "." + _key
556
557 cur = ""
558 _key = decode(_key)
559 for table in aot.body:
560 cur += self._render_aot_table(table, prefix=_key)
561
562 return cur
563
564 def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
565 cur = ""
566 _key = prefix or ""
567 open_, close = "[[", "]]"
568
569 cur += (
570 f"{table.trivia.indent}"
571 f"{open_}"
572 f"{decode(_key)}"
573 f"{close}"
574 f"{table.trivia.comment_ws}"
575 f"{decode(table.trivia.comment)}"
576 f"{table.trivia.trail}"
577 )
578
579 for k, v in table.value.body:
580 if isinstance(v, Table):
581 if v.is_super_table():
582 if k.is_dotted():
583 # Dotted key inside table
584 cur += self._render_table(k, v)
585 else:
586 cur += self._render_table(k, v, prefix=_key)
587 else:
588 cur += self._render_table(k, v, prefix=_key)
589 elif isinstance(v, AoT):
590 cur += self._render_aot(k, v, prefix=_key)
591 else:
592 cur += self._render_simple_item(k, v)
593
594 return cur
595
596 def _render_simple_item(self, key, item, prefix=None):
597 if key is None:
598 return item.as_string()
599
600 _key = key.as_string()
601 if prefix is not None:
602 _key = prefix + "." + _key
603
604 return (
605 f"{item.trivia.indent}"
606 f"{decode(_key)}"
607 f"{key.sep}"
608 f"{decode(item.as_string())}"
609 f"{item.trivia.comment_ws}"
610 f"{decode(item.trivia.comment)}"
611 f"{item.trivia.trail}"
612 )
613
614 def __len__(self) -> int:
615 return dict.__len__(self)
616
617 def __iter__(self) -> Iterator[str]:
618 return iter(dict.keys(self))
619
620 # Dictionary methods
621 def __getitem__(self, key: Key | str) -> Item | Container:
622 item = self.item(key)
623 if isinstance(item, Item) and item.is_boolean():
624 return item.value
625
626 return item
627
628 def __setitem__(self, key: Key | str, value: Any) -> None:
629 if key is not None and key in self:
630 old_key = next(filter(lambda k: k == key, self._map))
631 self._replace(old_key, key, value)
632 else:
633 self.append(key, value)
634
635 def __delitem__(self, key: Key | str) -> None:
636 self.remove(key)
637
638 def setdefault(self, key: Key | str, default: Any) -> Any:
639 super().setdefault(key, default=default)
640 return self[key]
641
642 def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
643 if not isinstance(key, Key):
644 key = SingleKey(key)
645
646 idx = self._map.get(key)
647 if idx is None:
648 raise NonExistentKey(key)
649
650 self._replace_at(idx, new_key, value)
651
652 def _replace_at(
653 self, idx: int | tuple[int], new_key: Key | str, value: Item
654 ) -> None:
655 value = _item(value)
656
657 if isinstance(idx, tuple):
658 for i in idx[1:]:
659 self._body[i] = (None, Null())
660
661 idx = idx[0]
662
663 k, v = self._body[idx]
664 if not isinstance(new_key, Key):
665 if (
666 isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
667 or new_key != k.key
668 ):
669 new_key = SingleKey(new_key)
670 else: # Inherit the sep of the old key
671 new_key = k
672
673 del self._map[k]
674 self._map[new_key] = idx
675 if new_key != k:
676 dict.__delitem__(self, k)
677
678 if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)):
679 # new tables should appear after all non-table values
680 self.remove(k)
681 for i in range(idx, len(self._body)):
682 if isinstance(self._body[i][1], (AoT, Table)):
683 self._insert_at(i, new_key, value)
684 idx = i
685 break
686 else:
687 idx = -1
688 self.append(new_key, value)
689 else:
690 # Copying trivia
691 if not isinstance(value, (Whitespace, AoT)):
692 value.trivia.indent = v.trivia.indent
693 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws
694 value.trivia.comment = value.trivia.comment or v.trivia.comment
695 value.trivia.trail = v.trivia.trail
696 self._body[idx] = (new_key, value)
697
698 if hasattr(value, "invalidate_display_name"):
699 value.invalidate_display_name() # type: ignore[attr-defined]
700
701 if isinstance(value, Table):
702 # Insert a cosmetic new line for tables if:
703 # - it does not have it yet OR is not followed by one
704 # - it is not the last item, or
705 # - The table being replaced has a newline
706 last, _ = self._previous_item_with_index()
707 idx = last if idx < 0 else idx
708 has_ws = ends_with_whitespace(value)
709 replace_has_ws = (
710 isinstance(v, Table)
711 and v.value.body
712 and isinstance(v.value.body[-1][1], Whitespace)
713 )
714 next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace)
715 if (idx < last or replace_has_ws) and not (next_ws or has_ws):
716 value.append(None, Whitespace("\n"))
717
718 dict.__setitem__(self, new_key.key, value.value)
719
720 def __str__(self) -> str:
721 return str(self.value)
722
723 def __repr__(self) -> str:
724 return repr(self.value)
725
726 def __eq__(self, other: dict) -> bool:
727 if not isinstance(other, dict):
728 return NotImplemented
729
730 return self.value == other
731
732 def _getstate(self, protocol):
733 return (self._parsed,)
734
735 def __reduce__(self):
736 return self.__reduce_ex__(2)
737
738 def __reduce_ex__(self, protocol):
739 return (
740 self.__class__,
741 self._getstate(protocol),
742 (self._map, self._body, self._parsed, self._table_keys),
743 )
744
745 def __setstate__(self, state):
746 self._map = state[0]
747 self._body = state[1]
748 self._parsed = state[2]
749 self._table_keys = state[3]
750
751 for key, item in self._body:
752 if key is not None:
753 dict.__setitem__(self, key.key, item.value)
754
755 def copy(self) -> Container:
756 return copy.copy(self)
757
758 def __copy__(self) -> Container:
759 c = self.__class__(self._parsed)
760 for k, v in dict.items(self):
761 dict.__setitem__(c, k, v)
762
763 c._body += self.body
764 c._map.update(self._map)
765
766 return c
767
768 def _previous_item_with_index(
769 self, idx: int | None = None, ignore=(Null,)
770 ) -> tuple[int, Item] | None:
771 """Find the immediate previous item before index ``idx``"""
772 if idx is None or idx > len(self._body):
773 idx = len(self._body)
774 for i in range(idx - 1, -1, -1):
775 v = self._body[i][-1]
776 if not isinstance(v, ignore):
777 return i, v
778 return None
779
780 def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None:
781 """Find the immediate previous item before index ``idx``.
782 If ``idx`` is not given, the last item is returned.
783 """
784 prev = self._previous_item_with_index(idx, ignore)
785 return prev[-1] if prev else None
786
787
788class OutOfOrderTableProxy(_CustomDict):
789 @staticmethod
790 def validate(container: Container, indices: tuple[int, ...]) -> None:
791 """Validate out of order tables in the given container"""
792 # Append all items to a temp container to see if there is any error
793 temp_container = Container(True)
794 for i in indices:
795 _, item = container._body[i]
796
797 if isinstance(item, Table):
798 for k, v in item.value.body:
799 temp_container.append(k, v, validate=False)
800
801 temp_container._validate_out_of_order_table()
802
803 def __init__(self, container: Container, indices: tuple[int, ...]) -> None:
804 self._container = container
805 self._internal_container = Container(True)
806 self._tables = []
807 self._tables_map = {}
808
809 for i in indices:
810 _, item = self._container._body[i]
811
812 if isinstance(item, Table):
813 self._tables.append(item)
814 table_idx = len(self._tables) - 1
815 for k, v in item.value.body:
816 self._internal_container._raw_append(k, v)
817 self._tables_map.setdefault(k, []).append(table_idx)
818 if k is not None:
819 dict.__setitem__(self, k.key, v)
820
821 self._internal_container._validate_out_of_order_table()
822
823 def unwrap(self) -> str:
824 return self._internal_container.unwrap()
825
826 @property
827 def value(self):
828 return self._internal_container.value
829
830 def __getitem__(self, key: Key | str) -> Any:
831 if key not in self._internal_container:
832 raise NonExistentKey(key)
833
834 return self._internal_container[key]
835
836 def __setitem__(self, key: Key | str, item: Any) -> None:
837 if key in self._tables_map:
838 # Overwrite the first table and remove others
839 indices = self._tables_map[key]
840 while len(indices) > 1:
841 table = self._tables[indices.pop()]
842 self._remove_table(table)
843 self._tables[indices[0]][key] = item
844 elif self._tables:
845 table = self._tables[0]
846 table[key] = item
847 else:
848 self._container[key] = item
849
850 self._internal_container[key] = item
851 if key is not None:
852 dict.__setitem__(self, key, item)
853
854 def _remove_table(self, table: Table) -> None:
855 """Remove table from the parent container"""
856 self._tables.remove(table)
857 for idx, item in enumerate(self._container._body):
858 if item[1] is table:
859 self._container._remove_at(idx)
860 break
861
862 def __delitem__(self, key: Key | str) -> None:
863 if key not in self._tables_map:
864 raise NonExistentKey(key)
865
866 for i in reversed(self._tables_map[key]):
867 table = self._tables[i]
868 del table[key]
869 if not table and len(self._tables) > 1:
870 self._remove_table(table)
871
872 del self._tables_map[key]
873 del self._internal_container[key]
874 if key is not None:
875 dict.__delitem__(self, key)
876
877 def __iter__(self) -> Iterator[str]:
878 return iter(dict.keys(self))
879
880 def __len__(self) -> int:
881 return dict.__len__(self)
882
883 def setdefault(self, key: Key | str, default: Any) -> Any:
884 super().setdefault(key, default=default)
885 return self[key]
886
887
888def ends_with_whitespace(it: Any) -> bool:
889 """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object
890 ending with a ``Whitespace``.
891 """
892 return (
893 isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace)
894 ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))