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
4import math
6from collections.abc import Iterator
7from typing import Any
9from tomlkit._compat import decode
10from tomlkit._types import _CustomDict
11from tomlkit._utils import merge_dicts
12from tomlkit.exceptions import KeyAlreadyPresent
13from tomlkit.exceptions import NonExistentKey
14from tomlkit.exceptions import TOMLKitError
15from tomlkit.items import AoT
16from tomlkit.items import Comment
17from tomlkit.items import Item
18from tomlkit.items import Key
19from tomlkit.items import Null
20from tomlkit.items import SingleKey
21from tomlkit.items import Table
22from tomlkit.items import Trivia
23from tomlkit.items import Whitespace
24from tomlkit.items import item as _item
27_NOT_SET = object()
30class Container(_CustomDict):
31 """
32 A container for items within a TOMLDocument.
34 This class implements the `dict` interface with copy/deepcopy protocol.
35 """
37 def __init__(self, parsed: bool = False) -> None:
38 self._map: dict[SingleKey, int | tuple[int, ...]] = {}
39 self._body: list[tuple[Key | None, Item]] = []
40 self._parsed = parsed
41 self._table_keys = []
43 @property
44 def body(self) -> list[tuple[Key | None, Item]]:
45 return self._body
47 def unwrap(self) -> dict[str, Any]:
48 """Returns as pure python object (ppo)"""
49 unwrapped = {}
50 for k, v in self.items():
51 if k is None:
52 continue
54 if isinstance(k, Key):
55 k = k.key
57 if hasattr(v, "unwrap"):
58 v = v.unwrap()
60 if k in unwrapped:
61 merge_dicts(unwrapped[k], v)
62 else:
63 unwrapped[k] = v
65 return unwrapped
67 @property
68 def value(self) -> dict[str, Any]:
69 """The wrapped dict value"""
70 d = {}
71 for k, v in self._body:
72 if k is None:
73 continue
75 k = k.key
76 v = v.value
78 if isinstance(v, Container):
79 v = v.value
81 if k in d:
82 merge_dicts(d[k], v)
83 else:
84 d[k] = v
86 return d
88 def parsing(self, parsing: bool) -> None:
89 self._parsed = parsing
91 for _, v in self._body:
92 if isinstance(v, Table):
93 v.value.parsing(parsing)
94 elif isinstance(v, AoT):
95 for t in v.body:
96 t.value.parsing(parsing)
98 def add(self, key: Key | Item | str, item: Item | None = None) -> Container:
99 """
100 Adds an item to the current Container.
102 :Example:
104 >>> # add a key-value pair
105 >>> doc.add('key', 'value')
106 >>> # add a comment or whitespace or newline
107 >>> doc.add(comment('# comment'))
108 """
109 if item is None:
110 if not isinstance(key, (Comment, Whitespace)):
111 raise ValueError(
112 "Non comment/whitespace items must have an associated key"
113 )
115 key, item = None, key
117 return self.append(key, item)
119 def _handle_dotted_key(self, key: Key, value: Item) -> None:
120 if isinstance(value, (Table, AoT)):
121 raise TOMLKitError("Can't add a table to a dotted key")
122 name, *mid, last = key
123 name._dotted = True
124 table = current = Table(Container(True), Trivia(), False, is_super_table=True)
125 for _name in mid:
126 _name._dotted = True
127 new_table = Table(Container(True), Trivia(), False, is_super_table=True)
128 current.append(_name, new_table)
129 current = new_table
131 last.sep = key.sep
132 current.append(last, value)
134 self.append(name, table)
135 return
137 def _get_last_index_before_table(self) -> int:
138 last_index = -1
139 for i, (k, v) in enumerate(self._body):
140 if isinstance(v, Null):
141 continue # Null elements are inserted after deletion
143 if isinstance(v, Whitespace) and not v.is_fixed():
144 continue
146 if isinstance(v, (Table, AoT)) and not k.is_dotted():
147 break
148 last_index = i
149 return last_index + 1
151 def _validate_out_of_order_table(self, key: SingleKey | None = None) -> None:
152 if key is None:
153 for k in self._map:
154 assert k is not None
155 self._validate_out_of_order_table(k)
156 return
157 if key not in self._map or not isinstance(self._map[key], tuple):
158 return
159 OutOfOrderTableProxy.validate(self, self._map[key])
161 def append(
162 self, key: Key | str | None, item: Item, validate: bool = True
163 ) -> Container:
164 """Similar to :meth:`add` but both key and value must be given."""
165 if not isinstance(key, Key) and key is not None:
166 key = SingleKey(key)
168 if not isinstance(item, Item):
169 item = _item(item)
171 if key is not None and key.is_multi():
172 self._handle_dotted_key(key, item)
173 return self
175 if isinstance(item, (AoT, Table)) and item.name is None:
176 item.name = key.key
178 prev = self._previous_item()
179 prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev)
180 if isinstance(item, Table):
181 if not self._parsed:
182 item.invalidate_display_name()
183 if (
184 self._body
185 and not (self._parsed or item.trivia.indent or prev_ws)
186 and not key.is_dotted()
187 ):
188 item.trivia.indent = "\n"
190 if isinstance(item, AoT) and self._body and not self._parsed:
191 item.invalidate_display_name()
192 if item and not ("\n" in item[0].trivia.indent or prev_ws):
193 item[0].trivia.indent = "\n" + item[0].trivia.indent
195 if key is not None and key in self:
196 current_idx = self._map[key]
197 if isinstance(current_idx, tuple):
198 current_body_element = self._body[current_idx[-1]]
199 else:
200 current_body_element = self._body[current_idx]
202 current = current_body_element[1]
204 if isinstance(item, Table):
205 if not isinstance(current, (Table, AoT)):
206 raise KeyAlreadyPresent(key)
208 if item.is_aot_element():
209 # New AoT element found later on
210 # Adding it to the current AoT
211 if not isinstance(current, AoT):
212 current = AoT([current, item], parsed=self._parsed)
214 self._replace(key, key, current)
215 else:
216 current.append(item)
218 return self
219 elif current.is_aot():
220 if not item.is_aot_element():
221 # Tried to define a table after an AoT with the same name.
222 raise KeyAlreadyPresent(key)
224 current.append(item)
226 return self
227 elif current.is_super_table():
228 if item.is_super_table():
229 # We need to merge both super tables
230 if (
231 key.is_dotted()
232 or current_body_element[0].is_dotted()
233 or self._table_keys[-1] != current_body_element[0]
234 ):
235 if key.is_dotted() and not self._parsed:
236 idx = self._get_last_index_before_table()
237 else:
238 idx = len(self._body)
240 if idx < len(self._body):
241 self._insert_at(idx, key, item)
242 else:
243 self._raw_append(key, item)
245 if validate:
246 self._validate_out_of_order_table(key)
248 return self
250 # Create a new element to replace the old one
251 current = copy.deepcopy(current)
252 for k, v in item.value.body:
253 current.append(k, v)
254 self._body[
255 (
256 current_idx[-1]
257 if isinstance(current_idx, tuple)
258 else current_idx
259 )
260 ] = (current_body_element[0], current)
262 return self
263 elif current_body_element[0].is_dotted():
264 raise TOMLKitError("Redefinition of an existing table")
265 else:
266 # Merging a concrete table into an existing implicit/super
267 # table is only valid if it does not redefine existing
268 # subtrees via dotted keys and does not change prior types.
269 self._validate_table_candidate(current, item)
270 elif not item.is_super_table():
271 raise KeyAlreadyPresent(key)
272 elif isinstance(item, AoT):
273 if not isinstance(current, AoT):
274 # Tried to define an AoT after a table with the same name.
275 raise KeyAlreadyPresent(key)
277 for table in item.body:
278 current.append(table)
280 return self
281 else:
282 raise KeyAlreadyPresent(key)
284 is_table = isinstance(item, (Table, AoT))
285 if (
286 key is not None
287 and self._body
288 and not self._parsed
289 and (not is_table or key.is_dotted())
290 ):
291 # If there is already at least one table in the current container
292 # and the given item is not a table, we need to find the last
293 # item that is not a table and insert after it
294 # If no such item exists, insert at the top of the table
295 last_index = self._get_last_index_before_table()
297 if last_index < len(self._body):
298 after_item = self._body[last_index][1]
299 if not (
300 isinstance(after_item, Whitespace)
301 or "\n" in after_item.trivia.indent
302 ):
303 after_item.trivia.indent = "\n" + after_item.trivia.indent
304 return self._insert_at(last_index, key, item)
305 else:
306 previous_item = self._body[-1][1]
307 if not (
308 isinstance(previous_item, Whitespace)
309 or ends_with_whitespace(previous_item)
310 or "\n" in previous_item.trivia.trail
311 ):
312 previous_item.trivia.trail += "\n"
314 self._raw_append(key, item)
315 return self
317 def _validate_table_candidate(self, current: Table, candidate: Table) -> None:
318 for k, v in candidate.value.body:
319 if k is None:
320 continue
322 if k in current.value._map:
323 existing = current.value.item(k)
324 if isinstance(existing, (Table, AoT)) != isinstance(v, (Table, AoT)):
325 raise KeyAlreadyPresent(k)
326 if k.is_dotted():
327 raise TOMLKitError("Redefinition of an existing table")
328 continue
330 if not k.is_dotted():
331 continue
333 head = next(iter(k))
334 if head in current.value._map:
335 raise TOMLKitError("Redefinition of an existing table")
337 def _raw_append(self, key: Key | None, item: Item) -> None:
338 if key in self._map:
339 current_idx = self._map[key]
340 if not isinstance(current_idx, tuple):
341 current_idx = (current_idx,)
343 current = self._body[current_idx[-1]][1]
344 if key is not None and not isinstance(current, Table):
345 raise KeyAlreadyPresent(key)
347 self._map[key] = (*current_idx, len(self._body))
348 elif key is not None:
349 self._map[key] = len(self._body)
351 self._body.append((key, item))
352 if item.is_table():
353 self._table_keys.append(key)
355 if key is not None:
356 dict.__setitem__(self, key.key, item.value)
358 def _remove_at(self, idx: int) -> None:
359 key = self._body[idx][0]
360 index = self._map.get(key)
361 if index is None:
362 raise NonExistentKey(key)
363 self._body[idx] = (None, Null())
365 if isinstance(index, tuple):
366 index = list(index)
367 index.remove(idx)
368 if len(index) == 1:
369 index = index.pop()
370 else:
371 index = tuple(index)
372 self._map[key] = index
373 else:
374 dict.__delitem__(self, key.key)
375 self._map.pop(key)
377 def remove(self, key: Key | str) -> Container:
378 """Remove a key from the container."""
379 if not isinstance(key, Key):
380 key = SingleKey(key)
382 idx = self._map.pop(key, None)
383 if idx is None:
384 raise NonExistentKey(key)
386 if isinstance(idx, tuple):
387 for i in idx:
388 self._body[i] = (None, Null())
389 else:
390 self._body[idx] = (None, Null())
392 dict.__delitem__(self, key.key)
394 return self
396 def _insert_after(
397 self, key: Key | str, other_key: Key | str, item: Any
398 ) -> Container:
399 if key is None:
400 raise ValueError("Key cannot be null in insert_after()")
402 if key not in self:
403 raise NonExistentKey(key)
405 if not isinstance(key, Key):
406 key = SingleKey(key)
408 if not isinstance(other_key, Key):
409 other_key = SingleKey(other_key)
411 item = _item(item)
413 idx = self._map[key]
414 # Insert after the max index if there are many.
415 if isinstance(idx, tuple):
416 idx = max(idx)
417 current_item = self._body[idx][1]
418 if "\n" not in current_item.trivia.trail:
419 current_item.trivia.trail += "\n"
421 # Increment indices after the current index
422 for k, v in self._map.items():
423 if isinstance(v, tuple):
424 new_indices = []
425 for v_ in v:
426 if v_ > idx:
427 v_ = v_ + 1
429 new_indices.append(v_)
431 self._map[k] = tuple(new_indices)
432 elif v > idx:
433 self._map[k] = v + 1
435 self._map[other_key] = idx + 1
436 self._body.insert(idx + 1, (other_key, item))
438 if key is not None:
439 dict.__setitem__(self, other_key.key, item.value)
441 return self
443 def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container:
444 if idx > len(self._body) - 1:
445 raise ValueError(f"Unable to insert at position {idx}")
447 if not isinstance(key, Key):
448 key = SingleKey(key)
450 item = _item(item)
452 if idx > 0:
453 previous_item = self._body[idx - 1][1]
454 if not (
455 isinstance(previous_item, Whitespace)
456 or ends_with_whitespace(previous_item)
457 or isinstance(item, (AoT, Table))
458 or "\n" in previous_item.trivia.trail
459 ):
460 previous_item.trivia.trail += "\n"
462 # Increment indices after the current index
463 for k, v in self._map.items():
464 if isinstance(v, tuple):
465 new_indices = []
466 for v_ in v:
467 if v_ >= idx:
468 v_ = v_ + 1
470 new_indices.append(v_)
472 self._map[k] = tuple(new_indices)
473 elif v >= idx:
474 self._map[k] = v + 1
476 if key in self._map:
477 current_idx = self._map[key]
478 if not isinstance(current_idx, tuple):
479 current_idx = (current_idx,)
480 self._map[key] = (*current_idx, idx)
481 else:
482 self._map[key] = idx
483 self._body.insert(idx, (key, item))
485 dict.__setitem__(self, key.key, item.value)
487 return self
489 def item(self, key: Key | str) -> Item:
490 """Get an item for the given key."""
491 if not isinstance(key, Key):
492 key = SingleKey(key)
494 idx = self._map.get(key)
495 if idx is None:
496 raise NonExistentKey(key)
498 if isinstance(idx, tuple):
499 # The item we are getting is an out of order table
500 # so we need a proxy to retrieve the proper objects
501 # from the parent container
502 return OutOfOrderTableProxy(self, idx)
504 return self._body[idx][1]
506 def last_item(self) -> Item | None:
507 """Get the last item."""
508 if self._body:
509 return self._body[-1][1]
511 def as_string(self) -> str:
512 """Render as TOML string."""
513 s = ""
514 for k, v in self._body:
515 if k is not None:
516 if isinstance(v, Table):
517 if (
518 s.strip(" ")
519 and not s.strip(" ").endswith("\n")
520 and "\n" not in v.trivia.indent
521 ):
522 s += "\n"
523 s += self._render_table(k, v)
524 elif isinstance(v, AoT):
525 if (
526 s.strip(" ")
527 and not s.strip(" ").endswith("\n")
528 and "\n" not in v.trivia.indent
529 ):
530 s += "\n"
531 s += self._render_aot(k, v)
532 else:
533 s += self._render_simple_item(k, v)
534 else:
535 s += self._render_simple_item(k, v)
537 return s
539 def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
540 cur = ""
542 if table.display_name is not None:
543 _key = table.display_name
544 else:
545 _key = key.as_string()
547 if prefix is not None:
548 _key = prefix + "." + _key
550 if (
551 not table.is_super_table()
552 or (
553 any(
554 not isinstance(v, (Table, AoT, Whitespace, Null))
555 for _, v in table.value.body
556 )
557 and not key.is_dotted()
558 )
559 or (
560 any(k.is_dotted() for k, v in table.value.body if isinstance(v, Table))
561 and not key.is_dotted()
562 )
563 ):
564 open_, close = "[", "]"
565 if table.is_aot_element():
566 open_, close = "[[", "]]"
568 newline_in_table_trivia = (
569 "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else ""
570 )
571 cur += (
572 f"{table.trivia.indent}"
573 f"{open_}"
574 f"{decode(_key)}"
575 f"{close}"
576 f"{table.trivia.comment_ws}"
577 f"{decode(table.trivia.comment)}"
578 f"{table.trivia.trail}"
579 f"{newline_in_table_trivia}"
580 )
581 elif table.trivia.indent == "\n":
582 cur += table.trivia.indent
584 for k, v in table.value.body:
585 if isinstance(v, Table):
586 if (
587 cur.strip(" ")
588 and not cur.strip(" ").endswith("\n")
589 and "\n" not in v.trivia.indent
590 ):
591 cur += "\n"
592 if v.is_super_table():
593 if k.is_dotted() and not key.is_dotted():
594 # Dotted key inside table
595 cur += self._render_table(k, v)
596 else:
597 cur += self._render_table(k, v, prefix=_key)
598 else:
599 cur += self._render_table(k, v, prefix=_key)
600 elif isinstance(v, AoT):
601 if (
602 cur.strip(" ")
603 and not cur.strip(" ").endswith("\n")
604 and "\n" not in v.trivia.indent
605 ):
606 cur += "\n"
607 cur += self._render_aot(k, v, prefix=_key)
608 else:
609 cur += self._render_simple_item(
610 k, v, prefix=_key if key.is_dotted() else None
611 )
613 return cur
615 def _render_aot(self, key, aot, prefix=None):
616 _key = key.as_string()
617 if prefix is not None:
618 _key = prefix + "." + _key
620 cur = ""
621 _key = decode(_key)
622 for table in aot.body:
623 cur += self._render_aot_table(table, prefix=_key)
625 return cur
627 def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
628 cur = ""
629 _key = prefix or ""
630 open_, close = "[[", "]]"
632 cur += (
633 f"{table.trivia.indent}"
634 f"{open_}"
635 f"{decode(_key)}"
636 f"{close}"
637 f"{table.trivia.comment_ws}"
638 f"{decode(table.trivia.comment)}"
639 f"{table.trivia.trail}"
640 )
642 for k, v in table.value.body:
643 if isinstance(v, Table):
644 if v.is_super_table():
645 if k.is_dotted():
646 # Dotted key inside table
647 cur += self._render_table(k, v)
648 else:
649 cur += self._render_table(k, v, prefix=_key)
650 else:
651 cur += self._render_table(k, v, prefix=_key)
652 elif isinstance(v, AoT):
653 cur += self._render_aot(k, v, prefix=_key)
654 else:
655 cur += self._render_simple_item(k, v)
657 return cur
659 def _render_simple_item(self, key, item, prefix=None):
660 if key is None:
661 return item.as_string()
663 _key = key.as_string()
664 if prefix is not None:
665 _key = prefix + "." + _key
667 return (
668 f"{item.trivia.indent}"
669 f"{decode(_key)}"
670 f"{key.sep}"
671 f"{decode(item.as_string())}"
672 f"{item.trivia.comment_ws}"
673 f"{decode(item.trivia.comment)}"
674 f"{item.trivia.trail}"
675 )
677 def __len__(self) -> int:
678 return dict.__len__(self)
680 def __iter__(self) -> Iterator[str]:
681 return iter(dict.keys(self))
683 # Dictionary methods
684 def __getitem__(self, key: Key | str) -> Item | Container:
685 item = self.item(key)
686 if isinstance(item, Item) and item.is_boolean():
687 return item.value
689 return item
691 def __setitem__(self, key: Key | str, value: Any) -> None:
692 if key is not None and key in self:
693 old_key = next(filter(lambda k: k == key, self._map))
694 self._replace(old_key, key, value)
695 else:
696 self.append(key, value)
698 def __delitem__(self, key: Key | str) -> None:
699 self.remove(key)
701 def setdefault(self, key: Key | str, default: Any) -> Any:
702 super().setdefault(key, default=default)
703 return self[key]
705 def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
706 if not isinstance(key, Key):
707 key = SingleKey(key)
709 idx = self._map.get(key)
710 if idx is None:
711 raise NonExistentKey(key)
713 self._replace_at(idx, new_key, value)
715 def _replace_at(
716 self, idx: int | tuple[int], new_key: Key | str, value: Item
717 ) -> None:
718 value = _item(value)
720 if isinstance(idx, tuple):
721 for i in idx[1:]:
722 self._body[i] = (None, Null())
724 idx = idx[0]
726 k, v = self._body[idx]
727 if not isinstance(new_key, Key):
728 if (
729 isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
730 or new_key != k.key
731 ):
732 new_key = SingleKey(new_key)
733 else: # Inherit the sep of the old key
734 new_key = k
736 del self._map[k]
737 self._map[new_key] = idx
738 if new_key != k:
739 dict.__delitem__(self, k)
741 if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)):
742 # new tables should appear after all non-table values
743 self.remove(k)
744 for i in range(idx, len(self._body)):
745 if isinstance(self._body[i][1], (AoT, Table)):
746 self._insert_at(i, new_key, value)
747 idx = i
748 break
749 else:
750 idx = -1
751 self.append(new_key, value)
752 else:
753 # Copying trivia
754 if not isinstance(value, (Whitespace, AoT)):
755 value.trivia.indent = v.trivia.indent
756 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws
757 value.trivia.comment = value.trivia.comment or v.trivia.comment
758 value.trivia.trail = v.trivia.trail
759 self._body[idx] = (new_key, value)
761 if hasattr(value, "invalidate_display_name"):
762 value.invalidate_display_name() # type: ignore[attr-defined]
764 if isinstance(value, Table):
765 # Insert a cosmetic new line for tables if:
766 # - it does not have it yet OR is not followed by one
767 # - it is not the last item, or
768 # - The table being replaced has a newline
769 last, _ = self._previous_item_with_index()
770 idx = last if idx < 0 else idx
771 has_ws = ends_with_whitespace(value)
772 replace_has_ws = (
773 isinstance(v, Table)
774 and v.value.body
775 and isinstance(v.value.body[-1][1], Whitespace)
776 )
777 next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace)
778 if (idx < last or replace_has_ws) and not (next_ws or has_ws):
779 value.append(None, Whitespace("\n"))
781 dict.__setitem__(self, new_key.key, value.value)
783 def __str__(self) -> str:
784 return str(self.value)
786 def __repr__(self) -> str:
787 return repr(self.value)
789 def __eq__(self, other: dict) -> bool:
790 if not isinstance(other, dict):
791 return NotImplemented
793 return _equal_with_nan(self.value, other)
795 def _getstate(self, protocol):
796 return (self._parsed,)
798 def __reduce__(self):
799 return self.__reduce_ex__(2)
801 def __reduce_ex__(self, protocol):
802 return (
803 self.__class__,
804 self._getstate(protocol),
805 (self._map, self._body, self._parsed, self._table_keys),
806 )
808 def __setstate__(self, state):
809 self._map = state[0]
810 self._body = state[1]
811 self._parsed = state[2]
812 self._table_keys = state[3]
814 for key, item in self._body:
815 if key is not None:
816 dict.__setitem__(self, key.key, item.value)
818 def copy(self) -> Container:
819 return copy.copy(self)
821 def __copy__(self) -> Container:
822 c = self.__class__(self._parsed)
823 for k, v in dict.items(self):
824 dict.__setitem__(c, k, v)
826 c._body += self.body
827 c._map.update(self._map)
829 return c
831 def _previous_item_with_index(
832 self, idx: int | None = None, ignore=(Null,)
833 ) -> tuple[int, Item] | None:
834 """Find the immediate previous item before index ``idx``"""
835 if idx is None or idx > len(self._body):
836 idx = len(self._body)
837 for i in range(idx - 1, -1, -1):
838 v = self._body[i][-1]
839 if not isinstance(v, ignore):
840 return i, v
841 return None
843 def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None:
844 """Find the immediate previous item before index ``idx``.
845 If ``idx`` is not given, the last item is returned.
846 """
847 prev = self._previous_item_with_index(idx, ignore)
848 return prev[-1] if prev else None
851class OutOfOrderTableProxy(_CustomDict):
852 @staticmethod
853 def validate(container: Container, indices: tuple[int, ...]) -> None:
854 """Validate out of order tables in the given container"""
855 # Append all items to a temp container to see if there is any error
856 temp_container = Container(True)
857 for i in indices:
858 _, item = container._body[i]
860 if isinstance(item, Table):
861 for k, v in item.value.body:
862 temp_container.append(k, v, validate=True)
864 temp_container._validate_out_of_order_table()
866 def __init__(self, container: Container, indices: tuple[int, ...]) -> None:
867 self._container = container
868 self._internal_container = Container(True)
869 self._tables: list[Table] = []
870 self._tables_map: dict[Key, list[int]] = {}
872 for i in indices:
873 _, item = self._container._body[i]
875 if isinstance(item, Table):
876 self._tables.append(item)
877 table_idx = len(self._tables) - 1
878 for k, v in item.value.body:
879 self._internal_container._raw_append(k, v)
880 indices = self._tables_map.setdefault(k, [])
881 if table_idx not in indices:
882 indices.append(table_idx)
883 if k is not None:
884 dict.__setitem__(self, k.key, v)
886 self._internal_container._validate_out_of_order_table()
888 def unwrap(self) -> str:
889 return self._internal_container.unwrap()
891 @property
892 def value(self):
893 return self._internal_container.value
895 def __getitem__(self, key: Key | str) -> Any:
896 if key not in self._internal_container:
897 raise NonExistentKey(key)
899 return self._internal_container[key]
901 def __setitem__(self, key: Key | str, value: Any) -> None:
902 from .items import item
904 def _is_table_or_aot(it: Any) -> bool:
905 return isinstance(item(it), (Table, AoT))
907 if key in self._tables_map:
908 # Overwrite the first table and remove others
909 indices = self._tables_map[key]
910 while len(indices) > 1:
911 table = self._tables[indices.pop()]
912 self._remove_table(table)
913 old_value = self._tables[indices[0]][key]
914 if _is_table_or_aot(old_value) and not _is_table_or_aot(value):
915 # Remove the entry from the map and set value again.
916 del self._tables[indices[0]][key]
917 del self._tables_map[key]
918 self[key] = value
919 return
920 self._tables[indices[0]][key] = value
921 elif self._tables:
922 if not _is_table_or_aot(value): # if the value is a plain value
923 for table in self._tables:
924 # find the first table that allows plain values
925 if any(not _is_table_or_aot(v) for _, v in table.items()):
926 table[key] = value
927 break
928 else:
929 self._tables[0][key] = value
930 else:
931 self._tables[0][key] = value
932 else:
933 self._container[key] = value
935 self._internal_container[key] = value
936 if key is not None:
937 dict.__setitem__(self, key, value)
939 def _remove_table(self, table: Table) -> None:
940 """Remove table from the parent container"""
941 self._tables.remove(table)
942 for idx, item in enumerate(self._container._body):
943 if item[1] is table:
944 self._container._remove_at(idx)
945 break
947 def __delitem__(self, key: Key | str) -> None:
948 if key not in self._tables_map:
949 raise NonExistentKey(key)
951 for i in reversed(self._tables_map[key]):
952 table = self._tables[i]
953 del table[key]
954 if not table and len(self._tables) > 1:
955 self._remove_table(table)
957 del self._tables_map[key]
958 del self._internal_container[key]
959 if key is not None:
960 dict.__delitem__(self, key)
962 def __iter__(self) -> Iterator[str]:
963 return iter(dict.keys(self))
965 def __len__(self) -> int:
966 return dict.__len__(self)
968 def setdefault(self, key: Key | str, default: Any) -> Any:
969 super().setdefault(key, default=default)
970 return self[key]
973def ends_with_whitespace(it: Any) -> bool:
974 """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object
975 ending with a ``Whitespace``.
976 """
977 return (
978 isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace)
979 ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))
982def _equal_with_nan(left: Any, right: Any) -> bool:
983 if isinstance(left, dict) and isinstance(right, dict):
984 if left.keys() != right.keys():
985 return False
986 return all(_equal_with_nan(left[k], right[k]) for k in left)
988 if isinstance(left, list) and isinstance(right, list):
989 if len(left) != len(right):
990 return False
991 return all(_equal_with_nan(l, r) for l, r in zip(left, right)) # noqa: B905, E741
993 if isinstance(left, float) and isinstance(right, float):
994 if math.isnan(left) and math.isnan(right):
995 return True
997 return left == right