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 TYPE_CHECKING
8from typing import Any
11if TYPE_CHECKING:
12 from typing import Self
14from tomlkit._compat import decode
15from tomlkit._types import _CustomDict
16from tomlkit._utils import merge_dicts
17from tomlkit.exceptions import KeyAlreadyPresent
18from tomlkit.exceptions import NonExistentKey
19from tomlkit.exceptions import TOMLKitError
20from tomlkit.items import AoT
21from tomlkit.items import Comment
22from tomlkit.items import Item
23from tomlkit.items import Key
24from tomlkit.items import Null
25from tomlkit.items import SingleKey
26from tomlkit.items import Table
27from tomlkit.items import Trivia
28from tomlkit.items import Whitespace
29from tomlkit.items import item as _item
32_NOT_SET = object()
35class Container(_CustomDict): # type: ignore[type-arg]
36 """
37 A container for items within a TOMLDocument.
39 This class implements the `dict` interface with copy/deepcopy protocol.
40 """
42 def __init__(self, parsed: bool = False) -> None:
43 self._map: dict[Key, int | tuple[int, ...]] = {}
44 self._body: list[tuple[Key | None, Item]] = []
45 self._parsed = parsed
46 self._table_keys: list[Key] = []
48 @property
49 def body(self) -> list[tuple[Key | None, Item]]:
50 return self._body
52 def unwrap(self) -> dict[str, Any]:
53 """Returns as pure python object (ppo)"""
54 unwrapped: dict[str, Any] = {}
55 for k, v in self.items():
56 if k is None:
57 continue
59 key_str: str = k.key if isinstance(k, Key) else k
60 val: Any = v.unwrap() if hasattr(v, "unwrap") else v
62 if key_str in unwrapped:
63 merge_dicts(unwrapped[key_str], val)
64 else:
65 unwrapped[key_str] = val
67 return unwrapped
69 @property
70 def value(self) -> dict[str, Any]:
71 """The wrapped dict value"""
72 d: dict[str, Any] = {}
73 for k, v in self._body:
74 if k is None:
75 continue
77 key_str = k.key
78 val: Any = v.value
80 if isinstance(val, Container):
81 val = val.value
83 if key_str in d:
84 merge_dicts(d[key_str], val)
85 else:
86 d[key_str] = val
88 return d
90 def parsing(self, parsing: bool) -> None:
91 self._parsed = parsing
93 for _, v in self._body:
94 if isinstance(v, Table):
95 v.value.parsing(parsing)
96 elif isinstance(v, AoT):
97 for t in v.body:
98 t.value.parsing(parsing)
100 def add(self, key: Key | Item | str, item: Any = None) -> Container:
101 """
102 Adds an item to the current Container.
104 :Example:
106 >>> # add a key-value pair
107 >>> doc.add('key', 'value')
108 >>> # add a comment or whitespace or newline
109 >>> doc.add(comment('# comment'))
110 """
111 if item is None:
112 if not isinstance(key, (Comment, Whitespace)):
113 raise ValueError(
114 "Non comment/whitespace items must have an associated key"
115 )
117 return self.append(None, key)
119 assert not isinstance(key, Item)
120 return self.append(key, item)
122 def _handle_dotted_key(self, key: Key, value: Item) -> None:
123 if isinstance(value, (Table, AoT)):
124 raise TOMLKitError("Can't add a table to a dotted key")
125 name, *mid, last = key
126 name._dotted = True
127 table = current = Table(Container(True), Trivia(), False, is_super_table=True)
128 for _name in mid:
129 _name._dotted = True
130 new_table = Table(Container(True), Trivia(), False, is_super_table=True)
131 current.append(_name, new_table)
132 current = new_table
134 last.sep = key.sep
135 current.append(last, value)
137 self.append(name, table)
138 return
140 def _get_last_index_before_table(self) -> int:
141 last_index = -1
142 for i, (k, v) in enumerate(self._body):
143 if isinstance(v, Null):
144 continue # Null elements are inserted after deletion
146 if isinstance(v, Whitespace) and not v.is_fixed():
147 continue
149 if isinstance(v, (Table, AoT)) and k is not None and not k.is_dotted():
150 break
151 last_index = i
152 return last_index + 1
154 def _validate_out_of_order_table(self, key: Key | None = None) -> None:
155 if key is None:
156 for k in self._map:
157 assert k is not None
158 self._validate_out_of_order_table(k)
159 return
160 if key not in self._map:
161 return
162 current_idx = self._map[key]
163 if not isinstance(current_idx, tuple):
164 return
165 OutOfOrderTableProxy.validate(self, current_idx)
167 def append(
168 self, key: Key | str | None, item: Any, validate: bool = True
169 ) -> Container:
170 """Similar to :meth:`add` but both key and value must be given."""
171 if not isinstance(key, Key) and key is not None:
172 key = SingleKey(key)
174 if not isinstance(item, Item):
175 item = _item(item)
177 if key is not None and key.is_multi():
178 self._handle_dotted_key(key, item)
179 return self
181 if isinstance(item, (AoT, Table)) and item.name is None:
182 assert isinstance(key, Key)
183 item.name = key.key
185 prev = self._previous_item()
186 prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev)
187 if isinstance(item, Table):
188 if not self._parsed:
189 item.invalidate_display_name()
190 if (
191 self._body
192 and not (self._parsed or item.trivia.indent or prev_ws)
193 and key is not None
194 and not key.is_dotted()
195 ):
196 item.trivia.indent = "\n"
198 if isinstance(item, AoT) and self._body and not self._parsed:
199 item.invalidate_display_name()
200 if item and not ("\n" in item[0].trivia.indent or prev_ws):
201 item[0].trivia.indent = "\n" + item[0].trivia.indent
203 if key is not None and key in self:
204 current_idx = self._map[key]
205 if isinstance(current_idx, tuple):
206 current_body_element = self._body[current_idx[-1]]
207 else:
208 current_body_element = self._body[current_idx]
210 current = current_body_element[1]
212 if isinstance(item, Table):
213 if not isinstance(current, (Table, AoT)):
214 raise KeyAlreadyPresent(key)
216 if item.is_aot_element():
217 # New AoT element found later on
218 # Adding it to the current AoT
219 if not isinstance(current, AoT):
220 current = AoT([current, item], parsed=self._parsed)
222 self._replace(key, key, current)
223 else:
224 current.append(item)
226 return self
227 elif isinstance(current, AoT):
228 if not item.is_aot_element():
229 # Tried to define a table after an AoT with the same name.
230 raise KeyAlreadyPresent(key)
232 current.append(item)
234 return self
235 elif current.is_super_table():
236 if item.is_super_table():
237 # We need to merge both super tables
238 if (
239 key.is_dotted()
240 or (
241 current_body_element[0] is not None
242 and current_body_element[0].is_dotted()
243 )
244 or self._table_keys[-1] != current_body_element[0]
245 ):
246 if key.is_dotted() and not self._parsed:
247 idx = self._get_last_index_before_table()
248 else:
249 idx = len(self._body)
251 if idx < len(self._body):
252 self._insert_at(idx, key, item)
253 else:
254 self._raw_append(key, item)
256 if validate:
257 self._validate_out_of_order_table(key)
259 return self
261 # Merge the new super table's body into the existing one
262 # in place. Previously this deep-copied `current` before
263 # appending, which is O(size of current) on every merge
264 # and therefore O(n^2) when many subtables share a super
265 # table (e.g. consecutive `[a.b.c]` / `[a.b.d]` headers).
266 # Mutating in place is O(1) per merge. The defensive copy
267 # that protected the out-of-order validation pass has been
268 # moved into OutOfOrderTableProxy (its only consumer).
269 for k, v in item.value.body:
270 current.append(k, v)
272 return self
273 elif (
274 current_body_element[0] is not None
275 and current_body_element[0].is_dotted()
276 ):
277 raise TOMLKitError("Redefinition of an existing table")
278 else:
279 # Merging a concrete table into an existing implicit/super
280 # table is only valid if it does not redefine existing
281 # subtrees via dotted keys and does not change prior types.
282 assert isinstance(current, Table)
283 self._validate_table_candidate(current, item)
284 elif not item.is_super_table():
285 raise KeyAlreadyPresent(key)
286 elif isinstance(item, AoT):
287 if not isinstance(current, AoT):
288 # Tried to define an AoT after a table with the same name.
289 raise KeyAlreadyPresent(key)
291 for table in item.body:
292 current.append(table)
294 return self
295 else:
296 raise KeyAlreadyPresent(key)
298 is_table = isinstance(item, (Table, AoT))
299 if (
300 key is not None
301 and self._body
302 and not self._parsed
303 and (not is_table or key.is_dotted())
304 ):
305 # If there is already at least one table in the current container
306 # and the given item is not a table, we need to find the last
307 # item that is not a table and insert after it
308 # If no such item exists, insert at the top of the table
309 last_index = self._get_last_index_before_table()
311 if last_index < len(self._body):
312 after_item = self._body[last_index][1]
313 if not (
314 isinstance(after_item, Whitespace)
315 or "\n" in after_item.trivia.indent
316 ):
317 after_item.trivia.indent = "\n" + after_item.trivia.indent
318 return self._insert_at(last_index, key, item)
319 else:
320 previous_item = self._body[-1][1]
321 if not (
322 isinstance(previous_item, Whitespace)
323 or ends_with_whitespace(previous_item)
324 or "\n" in previous_item.trivia.trail
325 ):
326 previous_item.trivia.trail += "\n"
328 self._raw_append(key, item)
329 return self
331 def _validate_table_candidate(self, current: Table, candidate: Table) -> None:
332 for k, v in candidate.value.body:
333 if k is None:
334 continue
336 if k in current.value._map:
337 existing = current.value.item(k)
338 if isinstance(existing, (Table, AoT)) != isinstance(v, (Table, AoT)):
339 raise KeyAlreadyPresent(k)
340 if k.is_dotted():
341 raise TOMLKitError("Redefinition of an existing table")
342 continue
344 if not k.is_dotted():
345 continue
347 head = next(iter(k))
348 if head in current.value._map:
349 raise TOMLKitError("Redefinition of an existing table")
351 def _raw_append(self, key: Key | None, item: Item) -> None:
352 if key is not None and key in self._map:
353 current_idx = self._map[key]
354 if not isinstance(current_idx, tuple):
355 current_idx = (current_idx,)
357 current = self._body[current_idx[-1]][1]
358 if not isinstance(current, Table):
359 raise KeyAlreadyPresent(key)
361 self._map[key] = (*current_idx, len(self._body))
362 elif key is not None:
363 self._map[key] = len(self._body)
365 self._body.append((key, item))
366 if item.is_table() and key is not None:
367 self._table_keys.append(key)
369 if key is not None:
370 dict.__setitem__(self, key.key, item.value)
372 def _remove_at(self, idx: int) -> None:
373 key = self._body[idx][0]
374 assert key is not None
375 index = self._map.get(key)
376 if index is None:
377 raise NonExistentKey(key)
378 self._body[idx] = (None, Null())
380 if isinstance(index, tuple):
381 index_list = list(index)
382 index_list.remove(idx)
383 if len(index_list) == 1:
384 self._map[key] = index_list.pop()
385 else:
386 self._map[key] = tuple(index_list)
387 else:
388 dict.__delitem__(self, key.key)
389 self._map.pop(key)
391 def remove(self, key: Key | str) -> Container:
392 """Remove a key from the container."""
393 if not isinstance(key, Key):
394 key = SingleKey(key)
396 idx = self._map.pop(key, None)
397 if idx is None:
398 raise NonExistentKey(key)
400 if isinstance(idx, tuple):
401 for i in idx:
402 self._body[i] = (None, Null())
403 else:
404 self._body[idx] = (None, Null())
406 dict.__delitem__(self, key.key)
408 return self
410 def _insert_after(
411 self, key: Key | str, other_key: Key | str, item: Any
412 ) -> Container:
413 if key is None:
414 raise ValueError("Key cannot be null in insert_after()")
416 if key not in self:
417 raise NonExistentKey(key)
419 if not isinstance(key, Key):
420 key = SingleKey(key)
422 if not isinstance(other_key, Key):
423 other_key = SingleKey(other_key)
425 item = _item(item)
427 idx = self._map[key]
428 # Insert after the max index if there are many.
429 if isinstance(idx, tuple):
430 idx = max(idx)
431 current_item = self._body[idx][1]
432 if "\n" not in current_item.trivia.trail:
433 current_item.trivia.trail += "\n"
435 # Increment indices after the current index
436 for k, v in self._map.items():
437 if isinstance(v, tuple):
438 new_indices = []
439 for v_ in v:
440 if v_ > idx:
441 v_ = v_ + 1
443 new_indices.append(v_)
445 self._map[k] = tuple(new_indices)
446 elif v > idx:
447 self._map[k] = v + 1
449 self._map[other_key] = idx + 1
450 self._body.insert(idx + 1, (other_key, item))
452 if key is not None:
453 dict.__setitem__(self, other_key.key, item.value)
455 return self
457 def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container:
458 if idx > len(self._body) - 1:
459 raise ValueError(f"Unable to insert at position {idx}")
461 if not isinstance(key, Key):
462 key = SingleKey(key)
464 item = _item(item)
466 if idx > 0:
467 previous_item = self._body[idx - 1][1]
468 if not (
469 isinstance(previous_item, Whitespace)
470 or ends_with_whitespace(previous_item)
471 or isinstance(item, (AoT, Table))
472 or "\n" in previous_item.trivia.trail
473 ):
474 previous_item.trivia.trail += "\n"
476 # Increment indices after the current index
477 for k, v in self._map.items():
478 if isinstance(v, tuple):
479 new_indices = []
480 for v_ in v:
481 if v_ >= idx:
482 v_ = v_ + 1
484 new_indices.append(v_)
486 self._map[k] = tuple(new_indices)
487 elif v >= idx:
488 self._map[k] = v + 1
490 if key in self._map:
491 current_idx = self._map[key]
492 if not isinstance(current_idx, tuple):
493 current_idx = (current_idx,)
494 self._map[key] = (*current_idx, idx)
495 else:
496 self._map[key] = idx
497 self._body.insert(idx, (key, item))
499 dict.__setitem__(self, key.key, item.value)
501 return self
503 def item(self, key: Key | str) -> Item | OutOfOrderTableProxy:
504 """Get an item for the given key."""
505 if not isinstance(key, Key):
506 key = SingleKey(key)
508 idx = self._map.get(key)
509 if idx is None:
510 raise NonExistentKey(key)
512 if isinstance(idx, tuple):
513 # The item we are getting is an out of order table
514 # so we need a proxy to retrieve the proper objects
515 # from the parent container
516 return OutOfOrderTableProxy(self, idx)
518 return self._body[idx][1]
520 def last_item(self) -> Item | None:
521 """Get the last item."""
522 if self._body:
523 return self._body[-1][1]
524 return None
526 def as_string(self) -> str:
527 """Render as TOML string."""
528 s = ""
529 for k, v in self._body:
530 if k is not None:
531 if isinstance(v, Table):
532 if (
533 s.strip(" ")
534 and not s.strip(" ").endswith("\n")
535 and "\n" not in v.trivia.indent
536 ):
537 s += "\n"
538 s += self._render_table(k, v)
539 elif isinstance(v, AoT):
540 if (
541 s.strip(" ")
542 and not s.strip(" ").endswith("\n")
543 and "\n" not in v.trivia.indent
544 ):
545 s += "\n"
546 s += self._render_aot(k, v)
547 else:
548 s += self._render_simple_item(k, v)
549 else:
550 s += self._render_simple_item(k, v)
552 return s
554 def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
555 cur = ""
557 if table.display_name is not None:
558 _key = table.display_name
559 else:
560 _key = key.as_string()
562 if prefix is not None:
563 _key = prefix + "." + _key
565 if (
566 not table.is_super_table()
567 or (
568 any(
569 not isinstance(v, (Table, AoT, Whitespace, Null))
570 for _, v in table.value.body
571 )
572 and not key.is_dotted()
573 )
574 or (
575 any(
576 k is not None and k.is_dotted()
577 for k, v in table.value.body
578 if isinstance(v, Table)
579 )
580 and not key.is_dotted()
581 )
582 ):
583 open_, close = "[", "]"
584 if table.is_aot_element():
585 open_, close = "[[", "]]"
587 newline_in_table_trivia = (
588 "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else ""
589 )
590 cur += (
591 f"{table.trivia.indent}"
592 f"{open_}"
593 f"{decode(_key)}"
594 f"{close}"
595 f"{table.trivia.comment_ws}"
596 f"{decode(table.trivia.comment)}"
597 f"{table.trivia.trail}"
598 f"{newline_in_table_trivia}"
599 )
600 elif table.trivia.indent == "\n":
601 cur += table.trivia.indent
603 for k, v in table.value.body:
604 if isinstance(v, Table):
605 if (
606 cur.strip(" ")
607 and not cur.strip(" ").endswith("\n")
608 and "\n" not in v.trivia.indent
609 ):
610 cur += "\n"
611 assert k is not None
612 if v.is_super_table():
613 if k.is_dotted() and not key.is_dotted():
614 # Dotted key inside table
615 cur += self._render_table(k, v)
616 else:
617 cur += self._render_table(k, v, prefix=_key)
618 else:
619 cur += self._render_table(k, v, prefix=_key)
620 elif isinstance(v, AoT):
621 if (
622 cur.strip(" ")
623 and not cur.strip(" ").endswith("\n")
624 and "\n" not in v.trivia.indent
625 ):
626 cur += "\n"
627 assert k is not None
628 cur += self._render_aot(k, v, prefix=_key)
629 else:
630 cur += self._render_simple_item(
631 k, v, prefix=_key if key.is_dotted() else None
632 )
634 return cur
636 def _render_aot(self, key: Key, aot: AoT, prefix: str | None = None) -> str:
637 _key = key.as_string()
638 if prefix is not None:
639 _key = prefix + "." + _key
641 cur = ""
642 _key = decode(_key)
643 for table in aot.body:
644 cur += self._render_aot_table(table, prefix=_key)
646 return cur
648 def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
649 cur = ""
650 _key = prefix or ""
651 open_, close = "[[", "]]"
653 cur += (
654 f"{table.trivia.indent}"
655 f"{open_}"
656 f"{decode(_key)}"
657 f"{close}"
658 f"{table.trivia.comment_ws}"
659 f"{decode(table.trivia.comment)}"
660 f"{table.trivia.trail}"
661 )
663 for k, v in table.value.body:
664 if isinstance(v, Table):
665 assert k is not None
666 if v.is_super_table():
667 if k.is_dotted():
668 # Dotted key inside table
669 cur += self._render_table(k, v)
670 else:
671 cur += self._render_table(k, v, prefix=_key)
672 else:
673 cur += self._render_table(k, v, prefix=_key)
674 elif isinstance(v, AoT):
675 assert k is not None
676 cur += self._render_aot(k, v, prefix=_key)
677 else:
678 cur += self._render_simple_item(k, v)
680 return cur
682 def _render_simple_item(
683 self, key: Key | None, item: Item, prefix: str | None = None
684 ) -> str:
685 if key is None:
686 return item.as_string()
688 _key = key.as_string()
689 if prefix is not None:
690 _key = prefix + "." + _key
692 return (
693 f"{item.trivia.indent}"
694 f"{decode(_key)}"
695 f"{key.sep}"
696 f"{decode(item.as_string())}"
697 f"{item.trivia.comment_ws}"
698 f"{decode(item.trivia.comment)}"
699 f"{item.trivia.trail}"
700 )
702 def __len__(self) -> int:
703 return dict.__len__(self)
705 def __iter__(self) -> Iterator[str]:
706 return iter(dict.keys(self))
708 # Dictionary methods
709 def __getitem__(self, key: Key | str) -> Any:
710 item = self.item(key)
711 if isinstance(item, Item) and item.is_boolean():
712 return item.value
714 return item
716 def __contains__(self, key: object) -> bool:
717 # Native membership test. The inherited ``MutableMapping.__contains__``
718 # resolves the value via ``__getitem__``/``item()`` (and builds a
719 # ``NonExistentKey`` on every absent key) only to discard it. Resolve the
720 # key the same way ``item()`` does -- ``str`` becomes a ``SingleKey``
721 # (a non-str/non-``Key`` argument still raises ``TypeError``) -- then
722 # probe ``_map`` directly. For an out-of-order table the proxy is still
723 # built so its validation runs exactly as before.
724 if not isinstance(key, Key):
725 key = SingleKey(key) # type: ignore[arg-type]
726 idx = self._map.get(key)
727 if idx is None:
728 return False
729 if isinstance(idx, tuple):
730 OutOfOrderTableProxy(self, idx)
731 return True
733 def __setitem__(self, key: Key | str, value: Any) -> None:
734 if key in self:
735 old_key = next(filter(lambda k: k == key, self._map))
736 self._replace(old_key, key, value)
737 else:
738 self.append(key, value)
740 def __delitem__(self, key: Key | str) -> None:
741 self.remove(key)
743 def setdefault(self, key: Key | str, default: Any = None) -> Any:
744 if key not in self:
745 self[key] = default
746 return self[key]
748 def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
749 if not isinstance(key, Key):
750 key = SingleKey(key)
752 idx = self._map.get(key)
753 if idx is None:
754 raise NonExistentKey(key)
756 self._replace_at(idx, new_key, value)
758 def _replace_at(
759 self, idx: int | tuple[int, ...], new_key: Key | str, value: Item
760 ) -> None:
761 value = _item(value)
763 if isinstance(idx, tuple):
764 for i in idx[1:]:
765 self._body[i] = (None, Null())
767 idx = idx[0]
769 k, v = self._body[idx]
770 assert k is not None
771 if not isinstance(new_key, Key):
772 if (
773 isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
774 or new_key != k.key
775 ):
776 new_key = SingleKey(new_key)
777 else: # Inherit the sep of the old key
778 new_key = k
780 del self._map[k]
781 self._map[new_key] = idx
782 if new_key != k:
783 dict.__delitem__(self, k.key)
785 if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)):
786 self.remove(k)
787 if isinstance(value, (AoT, Table)):
788 # new tables should appear after all non-table values
789 for i in range(idx, len(self._body)):
790 if isinstance(self._body[i][1], (AoT, Table)):
791 self._insert_at(i, new_key, value)
792 idx = i
793 break
794 else:
795 idx = -1
796 self.append(new_key, value)
797 else:
798 # the replaced table's slot lies in the table region, where a
799 # plain value would be captured by the preceding table on
800 # round-trip; append() puts it with the other root-level values
801 idx = -1
802 self.append(new_key, value)
803 else:
804 # Copying trivia
805 if not isinstance(value, (Whitespace, AoT)):
806 value.trivia.indent = v.trivia.indent
807 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws
808 value.trivia.comment = value.trivia.comment or v.trivia.comment
809 value.trivia.trail = v.trivia.trail
810 self._body[idx] = (new_key, value)
812 if hasattr(value, "invalidate_display_name"):
813 value.invalidate_display_name()
815 if isinstance(value, Table):
816 # Insert a cosmetic new line for tables if:
817 # - it does not have it yet OR is not followed by one
818 # - it is not the last item, or
819 # - The table being replaced has a newline
820 result = self._previous_item_with_index()
821 assert result is not None
822 last, _ = result
823 idx = last if idx < 0 else idx
824 has_ws = ends_with_whitespace(value)
825 replace_has_ws = (
826 isinstance(v, Table)
827 and v.value.body
828 and isinstance(v.value.body[-1][1], Whitespace)
829 )
830 next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace)
831 if (idx < last or replace_has_ws) and not (next_ws or has_ws):
832 value.append(None, Whitespace("\n"))
834 assert isinstance(new_key, Key)
835 dict.__setitem__(self, new_key.key, value.value)
837 def __str__(self) -> str:
838 return str(self.value)
840 def __repr__(self) -> str:
841 return repr(self.value)
843 def __eq__(self, other: object) -> bool:
844 if not isinstance(other, dict):
845 return NotImplemented
847 return bool(_equal_with_nan(self.value, other))
849 def _getstate(self, protocol: int) -> tuple[bool]:
850 return (self._parsed,)
852 def __reduce__(self) -> tuple[type, tuple[bool], tuple[Any, ...]]:
853 return self.__reduce_ex__(2)
855 def __reduce_ex__(self, protocol: int) -> tuple[type, tuple[bool], tuple[Any, ...]]: # type: ignore[override]
856 return (
857 self.__class__,
858 self._getstate(protocol),
859 (self._map, self._body, self._parsed, self._table_keys),
860 )
862 def __setstate__(self, state: tuple[Any, ...]) -> None:
863 self._map = state[0]
864 self._body = state[1]
865 self._parsed = state[2]
866 self._table_keys = state[3]
868 for key, item in self._body:
869 if key is not None:
870 dict.__setitem__(self, key.key, item.value)
872 def copy(self) -> Self:
873 return copy.copy(self)
875 def __copy__(self) -> Self:
876 c = self.__class__(self._parsed)
877 for k, v in dict.items(self):
878 dict.__setitem__(c, k, v)
880 c._body += self.body
881 c._map.update(self._map)
883 return c
885 def _previous_item_with_index(
886 self, idx: int | None = None, ignore: tuple[type, ...] = (Null,)
887 ) -> tuple[int, Item] | None:
888 """Find the immediate previous item before index ``idx``"""
889 if idx is None or idx > len(self._body):
890 idx = len(self._body)
891 for i in range(idx - 1, -1, -1):
892 v = self._body[i][-1]
893 if not isinstance(v, ignore):
894 return i, v
895 return None
897 def _previous_item(
898 self, idx: int | None = None, ignore: tuple[type, ...] = (Null,)
899 ) -> Item | None:
900 """Find the immediate previous item before index ``idx``.
901 If ``idx`` is not given, the last item is returned.
902 """
903 prev = self._previous_item_with_index(idx, ignore)
904 return prev[-1] if prev else None
907class OutOfOrderTableProxy(_CustomDict): # type: ignore[type-arg]
908 @staticmethod
909 def validate(container: Container, indices: tuple[int, ...]) -> None:
910 """Validate out of order tables in the given container"""
911 # Append all items to a temp container to see if there is any error.
912 # We deep-copy each value before appending: appending a super table
913 # merges it in place into a matching one already in temp_container, and
914 # those values are the live subtables of `container`. Without the copy
915 # the merge would mutate the caller's tables (and corrupt a later
916 # validation pass). The container merge itself is now copy-free for
917 # speed, so this is where the isolation lives.
918 temp_container = Container(True)
919 for i in indices:
920 _, item = container._body[i]
922 if isinstance(item, Table):
923 for k, v in item.value.body:
924 temp_container.append(k, copy.deepcopy(v), validate=True)
926 temp_container._validate_out_of_order_table()
928 def __init__(self, container: Container, indices: tuple[int, ...]) -> None:
929 self._container = container
930 self._internal_container = Container(True)
931 self._tables: list[Table] = []
932 self._tables_map: dict[Key, list[int]] = {}
934 for i in indices:
935 _, _item = self._container._body[i]
937 if isinstance(_item, Table):
938 self._tables.append(_item)
939 table_idx = len(self._tables) - 1
940 for k, v in _item.value.body:
941 self._internal_container._raw_append(k, v)
942 key_indices = self._tables_map.setdefault(k, []) # type: ignore[arg-type]
943 if table_idx not in key_indices:
944 key_indices.append(table_idx)
945 if k is not None:
946 dict.__setitem__(self, k.key, v)
948 self._internal_container._validate_out_of_order_table()
950 def unwrap(self) -> dict[str, Any]:
951 return self._internal_container.unwrap()
953 @property
954 def value(self) -> dict[str, Any]:
955 return self._internal_container.value
957 def __getitem__(self, key: Key | str) -> Any:
958 if key not in self._internal_container:
959 raise NonExistentKey(key)
961 return self._internal_container[key]
963 def __setitem__(self, key: Key | str, value: Any) -> None:
964 from .items import item as _item_fn
966 def _is_table_or_aot(it: Any) -> bool:
967 return isinstance(_item_fn(it), (Table, AoT))
969 _key: Key = key if isinstance(key, Key) else SingleKey(key)
971 if _key in self._tables_map:
972 # Overwrite the first table and remove others
973 map_indices = self._tables_map[_key]
974 while len(map_indices) > 1:
975 table = self._tables[map_indices.pop()]
976 self._remove_table(table)
977 old_value = self._tables[map_indices[0]][key]
978 if _is_table_or_aot(old_value) and not _is_table_or_aot(value):
979 # Remove the entry from the map and set value again.
980 del self._tables[map_indices[0]][key]
981 del self._tables_map[_key]
982 self[key] = value
983 return
984 self._tables[map_indices[0]][key] = value
985 elif self._tables:
986 if not _is_table_or_aot(value): # if the value is a plain value
987 for table in self._tables:
988 # find the first table that allows plain values
989 if any(not _is_table_or_aot(v) for _, v in table.items()):
990 table[key] = value
991 break
992 else:
993 self._tables[0][key] = value
994 else:
995 self._tables[0][key] = value
996 else:
997 self._container[key] = value
999 self._internal_container[key] = value
1000 if key is not None:
1001 dict.__setitem__(self, key, value)
1003 def _remove_table(self, table: Table) -> None:
1004 """Remove table from the parent container"""
1005 self._tables.remove(table)
1006 for idx, body_item in enumerate(self._container._body):
1007 if body_item[1] is table:
1008 self._container._remove_at(idx)
1009 break
1011 def __delitem__(self, key: Key | str) -> None:
1012 _key: Key = key if isinstance(key, Key) else SingleKey(key)
1013 if _key not in self._tables_map:
1014 raise NonExistentKey(key)
1016 for i in reversed(self._tables_map[_key]):
1017 table = self._tables[i]
1018 del table[key]
1019 if not table and len(self._tables) > 1:
1020 self._remove_table(table)
1022 del self._tables_map[_key]
1023 del self._internal_container[key]
1024 if key is not None:
1025 dict.__delitem__(self, key)
1027 def __iter__(self) -> Iterator[str]:
1028 return iter(dict.keys(self))
1030 def __len__(self) -> int:
1031 return dict.__len__(self)
1033 def setdefault(self, key: Key | str, default: Any = None) -> Any:
1034 if key not in self:
1035 self[key] = default
1036 return self[key]
1039def ends_with_whitespace(it: Any) -> bool:
1040 """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object
1041 ending with a ``Whitespace``.
1042 """
1043 return (
1044 isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace)
1045 ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))
1048def _equal_with_nan(left: Any, right: Any) -> bool:
1049 if isinstance(left, dict) and isinstance(right, dict):
1050 if left.keys() != right.keys():
1051 return False
1052 return all(_equal_with_nan(left[k], right[k]) for k in left)
1054 if isinstance(left, list) and isinstance(right, list):
1055 if len(left) != len(right):
1056 return False
1057 return all(_equal_with_nan(l, r) for l, r in zip(left, right)) # noqa: B905, E741
1059 if isinstance(left, float) and isinstance(right, float):
1060 if math.isnan(left) and math.isnan(right):
1061 return True
1063 return bool(left == right)