container.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. from __future__ import annotations
  2. import copy
  3. from typing import Any
  4. from typing import Iterator
  5. from tomlkit._compat import decode
  6. from tomlkit._types import _CustomDict
  7. from tomlkit._utils import merge_dicts
  8. from tomlkit.exceptions import KeyAlreadyPresent
  9. from tomlkit.exceptions import NonExistentKey
  10. from tomlkit.exceptions import TOMLKitError
  11. from tomlkit.items import AoT
  12. from tomlkit.items import Comment
  13. from tomlkit.items import Item
  14. from tomlkit.items import Key
  15. from tomlkit.items import Null
  16. from tomlkit.items import SingleKey
  17. from tomlkit.items import Table
  18. from tomlkit.items import Trivia
  19. from tomlkit.items import Whitespace
  20. from tomlkit.items import item as _item
  21. _NOT_SET = object()
  22. class Container(_CustomDict):
  23. """
  24. A container for items within a TOMLDocument.
  25. This class implements the `dict` interface with copy/deepcopy protocol.
  26. """
  27. def __init__(self, parsed: bool = False) -> None:
  28. self._map: dict[SingleKey, int | tuple[int, ...]] = {}
  29. self._body: list[tuple[Key | None, Item]] = []
  30. self._parsed = parsed
  31. self._table_keys = []
  32. @property
  33. def body(self) -> list[tuple[Key | None, Item]]:
  34. return self._body
  35. def unwrap(self) -> dict[str, Any]:
  36. unwrapped = {}
  37. for k, v in self.items():
  38. if k is None:
  39. continue
  40. if isinstance(k, Key):
  41. k = k.key
  42. if hasattr(v, "unwrap"):
  43. v = v.unwrap()
  44. if k in unwrapped:
  45. merge_dicts(unwrapped[k], v)
  46. else:
  47. unwrapped[k] = v
  48. return unwrapped
  49. @property
  50. def value(self) -> dict[str, Any]:
  51. d = {}
  52. for k, v in self._body:
  53. if k is None:
  54. continue
  55. k = k.key
  56. v = v.value
  57. if isinstance(v, Container):
  58. v = v.value
  59. if k in d:
  60. merge_dicts(d[k], v)
  61. else:
  62. d[k] = v
  63. return d
  64. def parsing(self, parsing: bool) -> None:
  65. self._parsed = parsing
  66. for _, v in self._body:
  67. if isinstance(v, Table):
  68. v.value.parsing(parsing)
  69. elif isinstance(v, AoT):
  70. for t in v.body:
  71. t.value.parsing(parsing)
  72. def add(self, key: Key | Item | str, item: Item | None = None) -> Container:
  73. """
  74. Adds an item to the current Container.
  75. :Example:
  76. >>> # add a key-value pair
  77. >>> doc.add('key', 'value')
  78. >>> # add a comment or whitespace or newline
  79. >>> doc.add(comment('# comment'))
  80. """
  81. if item is None:
  82. if not isinstance(key, (Comment, Whitespace)):
  83. raise ValueError(
  84. "Non comment/whitespace items must have an associated key"
  85. )
  86. key, item = None, key
  87. return self.append(key, item)
  88. def _handle_dotted_key(self, key: Key, value: Item) -> None:
  89. if isinstance(value, (Table, AoT)):
  90. raise TOMLKitError("Can't add a table to a dotted key")
  91. name, *mid, last = key
  92. name._dotted = True
  93. table = current = Table(Container(True), Trivia(), False, is_super_table=True)
  94. for _name in mid:
  95. _name._dotted = True
  96. new_table = Table(Container(True), Trivia(), False, is_super_table=True)
  97. current.append(_name, new_table)
  98. current = new_table
  99. last.sep = key.sep
  100. current.append(last, value)
  101. self.append(name, table)
  102. return
  103. def _get_last_index_before_table(self) -> int:
  104. last_index = -1
  105. for i, (k, v) in enumerate(self._body):
  106. if isinstance(v, Null):
  107. continue # Null elements are inserted after deletion
  108. if isinstance(v, Whitespace) and not v.is_fixed():
  109. continue
  110. if isinstance(v, (Table, AoT)) and not k.is_dotted():
  111. break
  112. last_index = i
  113. return last_index + 1
  114. def append(self, key: Key | str | None, item: Item) -> Container:
  115. """Similar to :meth:`add` but both key and value must be given."""
  116. if not isinstance(key, Key) and key is not None:
  117. key = SingleKey(key)
  118. if not isinstance(item, Item):
  119. item = _item(item)
  120. if key is not None and key.is_multi():
  121. self._handle_dotted_key(key, item)
  122. return self
  123. if isinstance(item, (AoT, Table)) and item.name is None:
  124. item.name = key.key
  125. prev = self._previous_item()
  126. prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev)
  127. if isinstance(item, Table):
  128. if not self._parsed:
  129. item.invalidate_display_name()
  130. if (
  131. self._body
  132. and not (self._parsed or item.trivia.indent or prev_ws)
  133. and not key.is_dotted()
  134. ):
  135. item.trivia.indent = "\n"
  136. if isinstance(item, AoT) and self._body and not self._parsed:
  137. item.invalidate_display_name()
  138. if item and not ("\n" in item[0].trivia.indent or prev_ws):
  139. item[0].trivia.indent = "\n" + item[0].trivia.indent
  140. if key is not None and key in self:
  141. current_idx = self._map[key]
  142. if isinstance(current_idx, tuple):
  143. current_body_element = self._body[current_idx[-1]]
  144. else:
  145. current_body_element = self._body[current_idx]
  146. current = current_body_element[1]
  147. if isinstance(item, Table):
  148. if not isinstance(current, (Table, AoT)):
  149. raise KeyAlreadyPresent(key)
  150. if item.is_aot_element():
  151. # New AoT element found later on
  152. # Adding it to the current AoT
  153. if not isinstance(current, AoT):
  154. current = AoT([current, item], parsed=self._parsed)
  155. self._replace(key, key, current)
  156. else:
  157. current.append(item)
  158. return self
  159. elif current.is_aot():
  160. if not item.is_aot_element():
  161. # Tried to define a table after an AoT with the same name.
  162. raise KeyAlreadyPresent(key)
  163. current.append(item)
  164. return self
  165. elif current.is_super_table():
  166. if item.is_super_table():
  167. # We need to merge both super tables
  168. if (
  169. self._table_keys[-1] != current_body_element[0]
  170. or key.is_dotted()
  171. or current_body_element[0].is_dotted()
  172. ):
  173. if key.is_dotted() and not self._parsed:
  174. idx = self._get_last_index_before_table()
  175. else:
  176. idx = len(self._body)
  177. if idx < len(self._body):
  178. self._insert_at(idx, key, item)
  179. else:
  180. self._raw_append(key, item)
  181. # Building a temporary proxy to check for errors
  182. OutOfOrderTableProxy(self, self._map[key])
  183. return self
  184. # Create a new element to replace the old one
  185. current = copy.deepcopy(current)
  186. for k, v in item.value.body:
  187. current.append(k, v)
  188. self._body[
  189. current_idx[-1]
  190. if isinstance(current_idx, tuple)
  191. else current_idx
  192. ] = (current_body_element[0], current)
  193. return self
  194. elif current_body_element[0].is_dotted():
  195. raise TOMLKitError("Redefinition of an existing table")
  196. elif not item.is_super_table():
  197. raise KeyAlreadyPresent(key)
  198. elif isinstance(item, AoT):
  199. if not isinstance(current, AoT):
  200. # Tried to define an AoT after a table with the same name.
  201. raise KeyAlreadyPresent(key)
  202. for table in item.body:
  203. current.append(table)
  204. return self
  205. else:
  206. raise KeyAlreadyPresent(key)
  207. is_table = isinstance(item, (Table, AoT))
  208. if (
  209. key is not None
  210. and self._body
  211. and not self._parsed
  212. and (not is_table or key.is_dotted())
  213. ):
  214. # If there is already at least one table in the current container
  215. # and the given item is not a table, we need to find the last
  216. # item that is not a table and insert after it
  217. # If no such item exists, insert at the top of the table
  218. last_index = self._get_last_index_before_table()
  219. if last_index < len(self._body):
  220. return self._insert_at(last_index, key, item)
  221. else:
  222. previous_item = self._body[-1][1]
  223. if not (
  224. isinstance(previous_item, Whitespace)
  225. or ends_with_whitespace(previous_item)
  226. or "\n" in previous_item.trivia.trail
  227. ):
  228. previous_item.trivia.trail += "\n"
  229. self._raw_append(key, item)
  230. return self
  231. def _raw_append(self, key: Key, item: Item) -> None:
  232. if key in self._map:
  233. current_idx = self._map[key]
  234. if not isinstance(current_idx, tuple):
  235. current_idx = (current_idx,)
  236. current = self._body[current_idx[-1]][1]
  237. if key is not None and not isinstance(current, Table):
  238. raise KeyAlreadyPresent(key)
  239. self._map[key] = current_idx + (len(self._body),)
  240. else:
  241. self._map[key] = len(self._body)
  242. self._body.append((key, item))
  243. if item.is_table():
  244. self._table_keys.append(key)
  245. if key is not None:
  246. dict.__setitem__(self, key.key, item.value)
  247. return self
  248. def _remove_at(self, idx: int) -> None:
  249. key = self._body[idx][0]
  250. index = self._map.get(key)
  251. if index is None:
  252. raise NonExistentKey(key)
  253. self._body[idx] = (None, Null())
  254. if isinstance(index, tuple):
  255. index = list(index)
  256. index.remove(idx)
  257. if len(index) == 1:
  258. index = index.pop()
  259. else:
  260. index = tuple(index)
  261. self._map[key] = index
  262. else:
  263. dict.__delitem__(self, key.key)
  264. self._map.pop(key)
  265. def remove(self, key: Key | str) -> Container:
  266. """Remove a key from the container."""
  267. if not isinstance(key, Key):
  268. key = SingleKey(key)
  269. idx = self._map.pop(key, None)
  270. if idx is None:
  271. raise NonExistentKey(key)
  272. if isinstance(idx, tuple):
  273. for i in idx:
  274. self._body[i] = (None, Null())
  275. else:
  276. self._body[idx] = (None, Null())
  277. dict.__delitem__(self, key.key)
  278. return self
  279. def _insert_after(
  280. self, key: Key | str, other_key: Key | str, item: Any
  281. ) -> Container:
  282. if key is None:
  283. raise ValueError("Key cannot be null in insert_after()")
  284. if key not in self:
  285. raise NonExistentKey(key)
  286. if not isinstance(key, Key):
  287. key = SingleKey(key)
  288. if not isinstance(other_key, Key):
  289. other_key = SingleKey(other_key)
  290. item = _item(item)
  291. idx = self._map[key]
  292. # Insert after the max index if there are many.
  293. if isinstance(idx, tuple):
  294. idx = max(idx)
  295. current_item = self._body[idx][1]
  296. if "\n" not in current_item.trivia.trail:
  297. current_item.trivia.trail += "\n"
  298. # Increment indices after the current index
  299. for k, v in self._map.items():
  300. if isinstance(v, tuple):
  301. new_indices = []
  302. for v_ in v:
  303. if v_ > idx:
  304. v_ = v_ + 1
  305. new_indices.append(v_)
  306. self._map[k] = tuple(new_indices)
  307. elif v > idx:
  308. self._map[k] = v + 1
  309. self._map[other_key] = idx + 1
  310. self._body.insert(idx + 1, (other_key, item))
  311. if key is not None:
  312. dict.__setitem__(self, other_key.key, item.value)
  313. return self
  314. def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container:
  315. if idx > len(self._body) - 1:
  316. raise ValueError(f"Unable to insert at position {idx}")
  317. if not isinstance(key, Key):
  318. key = SingleKey(key)
  319. item = _item(item)
  320. if idx > 0:
  321. previous_item = self._body[idx - 1][1]
  322. if not (
  323. isinstance(previous_item, Whitespace)
  324. or ends_with_whitespace(previous_item)
  325. or isinstance(item, (AoT, Table))
  326. or "\n" in previous_item.trivia.trail
  327. ):
  328. previous_item.trivia.trail += "\n"
  329. # Increment indices after the current index
  330. for k, v in self._map.items():
  331. if isinstance(v, tuple):
  332. new_indices = []
  333. for v_ in v:
  334. if v_ >= idx:
  335. v_ = v_ + 1
  336. new_indices.append(v_)
  337. self._map[k] = tuple(new_indices)
  338. elif v >= idx:
  339. self._map[k] = v + 1
  340. if key in self._map:
  341. current_idx = self._map[key]
  342. if not isinstance(current_idx, tuple):
  343. current_idx = (current_idx,)
  344. self._map[key] = current_idx + (idx,)
  345. else:
  346. self._map[key] = idx
  347. self._body.insert(idx, (key, item))
  348. dict.__setitem__(self, key.key, item.value)
  349. return self
  350. def item(self, key: Key | str) -> Item:
  351. """Get an item for the given key."""
  352. if not isinstance(key, Key):
  353. key = SingleKey(key)
  354. idx = self._map.get(key)
  355. if idx is None:
  356. raise NonExistentKey(key)
  357. if isinstance(idx, tuple):
  358. # The item we are getting is an out of order table
  359. # so we need a proxy to retrieve the proper objects
  360. # from the parent container
  361. return OutOfOrderTableProxy(self, idx)
  362. return self._body[idx][1]
  363. def last_item(self) -> Item | None:
  364. """Get the last item."""
  365. if self._body:
  366. return self._body[-1][1]
  367. def as_string(self) -> str:
  368. """Render as TOML string."""
  369. s = ""
  370. for k, v in self._body:
  371. if k is not None:
  372. if isinstance(v, Table):
  373. s += self._render_table(k, v)
  374. elif isinstance(v, AoT):
  375. s += self._render_aot(k, v)
  376. else:
  377. s += self._render_simple_item(k, v)
  378. else:
  379. s += self._render_simple_item(k, v)
  380. return s
  381. def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
  382. cur = ""
  383. if table.display_name is not None:
  384. _key = table.display_name
  385. else:
  386. _key = key.as_string()
  387. if prefix is not None:
  388. _key = prefix + "." + _key
  389. if not table.is_super_table() or (
  390. any(
  391. not isinstance(v, (Table, AoT, Whitespace, Null))
  392. for _, v in table.value.body
  393. )
  394. and not key.is_dotted()
  395. ):
  396. open_, close = "[", "]"
  397. if table.is_aot_element():
  398. open_, close = "[[", "]]"
  399. newline_in_table_trivia = (
  400. "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else ""
  401. )
  402. cur += (
  403. f"{table.trivia.indent}"
  404. f"{open_}"
  405. f"{decode(_key)}"
  406. f"{close}"
  407. f"{table.trivia.comment_ws}"
  408. f"{decode(table.trivia.comment)}"
  409. f"{table.trivia.trail}"
  410. f"{newline_in_table_trivia}"
  411. )
  412. elif table.trivia.indent == "\n":
  413. cur += table.trivia.indent
  414. for k, v in table.value.body:
  415. if isinstance(v, Table):
  416. if v.is_super_table():
  417. if k.is_dotted() and not key.is_dotted():
  418. # Dotted key inside table
  419. cur += self._render_table(k, v)
  420. else:
  421. cur += self._render_table(k, v, prefix=_key)
  422. else:
  423. cur += self._render_table(k, v, prefix=_key)
  424. elif isinstance(v, AoT):
  425. cur += self._render_aot(k, v, prefix=_key)
  426. else:
  427. cur += self._render_simple_item(
  428. k, v, prefix=_key if key.is_dotted() else None
  429. )
  430. return cur
  431. def _render_aot(self, key, aot, prefix=None):
  432. _key = key.as_string()
  433. if prefix is not None:
  434. _key = prefix + "." + _key
  435. cur = ""
  436. _key = decode(_key)
  437. for table in aot.body:
  438. cur += self._render_aot_table(table, prefix=_key)
  439. return cur
  440. def _render_aot_table(self, table: Table, prefix: str | None = None) -> str:
  441. cur = ""
  442. _key = prefix or ""
  443. open_, close = "[[", "]]"
  444. cur += (
  445. f"{table.trivia.indent}"
  446. f"{open_}"
  447. f"{decode(_key)}"
  448. f"{close}"
  449. f"{table.trivia.comment_ws}"
  450. f"{decode(table.trivia.comment)}"
  451. f"{table.trivia.trail}"
  452. )
  453. for k, v in table.value.body:
  454. if isinstance(v, Table):
  455. if v.is_super_table():
  456. if k.is_dotted():
  457. # Dotted key inside table
  458. cur += self._render_table(k, v)
  459. else:
  460. cur += self._render_table(k, v, prefix=_key)
  461. else:
  462. cur += self._render_table(k, v, prefix=_key)
  463. elif isinstance(v, AoT):
  464. cur += self._render_aot(k, v, prefix=_key)
  465. else:
  466. cur += self._render_simple_item(k, v)
  467. return cur
  468. def _render_simple_item(self, key, item, prefix=None):
  469. if key is None:
  470. return item.as_string()
  471. _key = key.as_string()
  472. if prefix is not None:
  473. _key = prefix + "." + _key
  474. return (
  475. f"{item.trivia.indent}"
  476. f"{decode(_key)}"
  477. f"{key.sep}"
  478. f"{decode(item.as_string())}"
  479. f"{item.trivia.comment_ws}"
  480. f"{decode(item.trivia.comment)}"
  481. f"{item.trivia.trail}"
  482. )
  483. def __len__(self) -> int:
  484. return dict.__len__(self)
  485. def __iter__(self) -> Iterator[str]:
  486. return iter(dict.keys(self))
  487. # Dictionary methods
  488. def __getitem__(self, key: Key | str) -> Item | Container:
  489. if not isinstance(key, Key):
  490. key = SingleKey(key)
  491. idx = self._map.get(key)
  492. if idx is None:
  493. raise NonExistentKey(key)
  494. if isinstance(idx, tuple):
  495. # The item we are getting is an out of order table
  496. # so we need a proxy to retrieve the proper objects
  497. # from the parent container
  498. return OutOfOrderTableProxy(self, idx)
  499. item = self._body[idx][1]
  500. if item.is_boolean():
  501. return item.value
  502. return item
  503. def __setitem__(self, key: Key | str, value: Any) -> None:
  504. if key is not None and key in self:
  505. old_key = next(filter(lambda k: k == key, self._map))
  506. self._replace(old_key, key, value)
  507. else:
  508. self.append(key, value)
  509. def __delitem__(self, key: Key | str) -> None:
  510. self.remove(key)
  511. def setdefault(self, key: Key | str, default: Any) -> Any:
  512. super().setdefault(key, default=default)
  513. return self[key]
  514. def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None:
  515. if not isinstance(key, Key):
  516. key = SingleKey(key)
  517. idx = self._map.get(key)
  518. if idx is None:
  519. raise NonExistentKey(key)
  520. self._replace_at(idx, new_key, value)
  521. def _replace_at(
  522. self, idx: int | tuple[int], new_key: Key | str, value: Item
  523. ) -> None:
  524. value = _item(value)
  525. if isinstance(idx, tuple):
  526. for i in idx[1:]:
  527. self._body[i] = (None, Null())
  528. idx = idx[0]
  529. k, v = self._body[idx]
  530. if not isinstance(new_key, Key):
  531. if (
  532. isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
  533. or new_key != k.key
  534. ):
  535. new_key = SingleKey(new_key)
  536. else: # Inherit the sep of the old key
  537. new_key = k
  538. del self._map[k]
  539. self._map[new_key] = idx
  540. if new_key != k:
  541. dict.__delitem__(self, k)
  542. if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)):
  543. # new tables should appear after all non-table values
  544. self.remove(k)
  545. for i in range(idx, len(self._body)):
  546. if isinstance(self._body[i][1], (AoT, Table)):
  547. self._insert_at(i, new_key, value)
  548. idx = i
  549. break
  550. else:
  551. idx = -1
  552. self.append(new_key, value)
  553. else:
  554. # Copying trivia
  555. if not isinstance(value, (Whitespace, AoT)):
  556. value.trivia.indent = v.trivia.indent
  557. value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws
  558. value.trivia.comment = value.trivia.comment or v.trivia.comment
  559. value.trivia.trail = v.trivia.trail
  560. self._body[idx] = (new_key, value)
  561. if hasattr(value, "invalidate_display_name"):
  562. value.invalidate_display_name() # type: ignore[attr-defined]
  563. if isinstance(value, Table):
  564. # Insert a cosmetic new line for tables if:
  565. # - it does not have it yet OR is not followed by one
  566. # - it is not the last item
  567. last, _ = self._previous_item_with_index()
  568. idx = last if idx < 0 else idx
  569. has_ws = ends_with_whitespace(value)
  570. next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace)
  571. if idx < last and not (next_ws or has_ws):
  572. value.append(None, Whitespace("\n"))
  573. dict.__setitem__(self, new_key.key, value.value)
  574. def __str__(self) -> str:
  575. return str(self.value)
  576. def __repr__(self) -> str:
  577. return repr(self.value)
  578. def __eq__(self, other: dict) -> bool:
  579. if not isinstance(other, dict):
  580. return NotImplemented
  581. return self.value == other
  582. def _getstate(self, protocol):
  583. return (self._parsed,)
  584. def __reduce__(self):
  585. return self.__reduce_ex__(2)
  586. def __reduce_ex__(self, protocol):
  587. return (
  588. self.__class__,
  589. self._getstate(protocol),
  590. (self._map, self._body, self._parsed, self._table_keys),
  591. )
  592. def __setstate__(self, state):
  593. self._map = state[0]
  594. self._body = state[1]
  595. self._parsed = state[2]
  596. self._table_keys = state[3]
  597. for key, item in self._body:
  598. if key is not None:
  599. dict.__setitem__(self, key.key, item.value)
  600. def copy(self) -> Container:
  601. return copy.copy(self)
  602. def __copy__(self) -> Container:
  603. c = self.__class__(self._parsed)
  604. for k, v in dict.items(self):
  605. dict.__setitem__(c, k, v)
  606. c._body += self.body
  607. c._map.update(self._map)
  608. return c
  609. def _previous_item_with_index(
  610. self, idx: int | None = None, ignore=(Null,)
  611. ) -> tuple[int, Item] | None:
  612. """Find the immediate previous item before index ``idx``"""
  613. if idx is None or idx > len(self._body):
  614. idx = len(self._body)
  615. for i in range(idx - 1, -1, -1):
  616. v = self._body[i][-1]
  617. if not isinstance(v, ignore):
  618. return i, v
  619. return None
  620. def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None:
  621. """Find the immediate previous item before index ``idx``.
  622. If ``idx`` is not given, the last item is returned.
  623. """
  624. prev = self._previous_item_with_index(idx, ignore)
  625. return prev[-1] if prev else None
  626. class OutOfOrderTableProxy(_CustomDict):
  627. def __init__(self, container: Container, indices: tuple[int]) -> None:
  628. self._container = container
  629. self._internal_container = Container(True)
  630. self._tables = []
  631. self._tables_map = {}
  632. for i in indices:
  633. _, item = self._container._body[i]
  634. if isinstance(item, Table):
  635. self._tables.append(item)
  636. table_idx = len(self._tables) - 1
  637. for k, v in item.value.body:
  638. self._internal_container.append(k, v)
  639. self._tables_map[k] = table_idx
  640. if k is not None:
  641. dict.__setitem__(self, k.key, v)
  642. def unwrap(self) -> str:
  643. return self._internal_container.unwrap()
  644. @property
  645. def value(self):
  646. return self._internal_container.value
  647. def __getitem__(self, key: Key | str) -> Any:
  648. if key not in self._internal_container:
  649. raise NonExistentKey(key)
  650. return self._internal_container[key]
  651. def __setitem__(self, key: Key | str, item: Any) -> None:
  652. if key in self._tables_map:
  653. table = self._tables[self._tables_map[key]]
  654. table[key] = item
  655. elif self._tables:
  656. table = self._tables[0]
  657. table[key] = item
  658. else:
  659. self._container[key] = item
  660. self._internal_container[key] = item
  661. if key is not None:
  662. dict.__setitem__(self, key, item)
  663. def _remove_table(self, table: Table) -> None:
  664. """Remove table from the parent container"""
  665. self._tables.remove(table)
  666. for idx, item in enumerate(self._container._body):
  667. if item[1] is table:
  668. self._container._remove_at(idx)
  669. break
  670. def __delitem__(self, key: Key | str) -> None:
  671. if key in self._tables_map:
  672. table = self._tables[self._tables_map[key]]
  673. del table[key]
  674. if not table and len(self._tables) > 1:
  675. self._remove_table(table)
  676. del self._tables_map[key]
  677. else:
  678. raise NonExistentKey(key)
  679. del self._internal_container[key]
  680. if key is not None:
  681. dict.__delitem__(self, key)
  682. def __iter__(self) -> Iterator[str]:
  683. return iter(dict.keys(self))
  684. def __len__(self) -> int:
  685. return dict.__len__(self)
  686. def setdefault(self, key: Key | str, default: Any) -> Any:
  687. super().setdefault(key, default=default)
  688. return self[key]
  689. def ends_with_whitespace(it: Any) -> bool:
  690. """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object
  691. ending with a ``Whitespace``.
  692. """
  693. return (
  694. isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace)
  695. ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))