Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/tomlkit/container.py: 66%

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

554 statements  

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))