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

581 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 after_item = self._body[last_index][1] 

293 if not ( 

294 isinstance(after_item, Whitespace) 

295 or "\n" in after_item.trivia.indent 

296 ): 

297 after_item.trivia.indent = "\n" + after_item.trivia.indent 

298 return self._insert_at(last_index, key, item) 

299 else: 

300 previous_item = self._body[-1][1] 

301 if not ( 

302 isinstance(previous_item, Whitespace) 

303 or ends_with_whitespace(previous_item) 

304 or "\n" in previous_item.trivia.trail 

305 ): 

306 previous_item.trivia.trail += "\n" 

307 

308 self._raw_append(key, item) 

309 return self 

310 

311 def _raw_append(self, key: Key | None, item: Item) -> None: 

312 if key in self._map: 

313 current_idx = self._map[key] 

314 if not isinstance(current_idx, tuple): 

315 current_idx = (current_idx,) 

316 

317 current = self._body[current_idx[-1]][1] 

318 if key is not None and not isinstance(current, Table): 

319 raise KeyAlreadyPresent(key) 

320 

321 self._map[key] = (*current_idx, len(self._body)) 

322 elif key is not None: 

323 self._map[key] = len(self._body) 

324 

325 self._body.append((key, item)) 

326 if item.is_table(): 

327 self._table_keys.append(key) 

328 

329 if key is not None: 

330 dict.__setitem__(self, key.key, item.value) 

331 

332 def _remove_at(self, idx: int) -> None: 

333 key = self._body[idx][0] 

334 index = self._map.get(key) 

335 if index is None: 

336 raise NonExistentKey(key) 

337 self._body[idx] = (None, Null()) 

338 

339 if isinstance(index, tuple): 

340 index = list(index) 

341 index.remove(idx) 

342 if len(index) == 1: 

343 index = index.pop() 

344 else: 

345 index = tuple(index) 

346 self._map[key] = index 

347 else: 

348 dict.__delitem__(self, key.key) 

349 self._map.pop(key) 

350 

351 def remove(self, key: Key | str) -> Container: 

352 """Remove a key from the container.""" 

353 if not isinstance(key, Key): 

354 key = SingleKey(key) 

355 

356 idx = self._map.pop(key, None) 

357 if idx is None: 

358 raise NonExistentKey(key) 

359 

360 if isinstance(idx, tuple): 

361 for i in idx: 

362 self._body[i] = (None, Null()) 

363 else: 

364 self._body[idx] = (None, Null()) 

365 

366 dict.__delitem__(self, key.key) 

367 

368 return self 

369 

370 def _insert_after( 

371 self, key: Key | str, other_key: Key | str, item: Any 

372 ) -> Container: 

373 if key is None: 

374 raise ValueError("Key cannot be null in insert_after()") 

375 

376 if key not in self: 

377 raise NonExistentKey(key) 

378 

379 if not isinstance(key, Key): 

380 key = SingleKey(key) 

381 

382 if not isinstance(other_key, Key): 

383 other_key = SingleKey(other_key) 

384 

385 item = _item(item) 

386 

387 idx = self._map[key] 

388 # Insert after the max index if there are many. 

389 if isinstance(idx, tuple): 

390 idx = max(idx) 

391 current_item = self._body[idx][1] 

392 if "\n" not in current_item.trivia.trail: 

393 current_item.trivia.trail += "\n" 

394 

395 # Increment indices after the current index 

396 for k, v in self._map.items(): 

397 if isinstance(v, tuple): 

398 new_indices = [] 

399 for v_ in v: 

400 if v_ > idx: 

401 v_ = v_ + 1 

402 

403 new_indices.append(v_) 

404 

405 self._map[k] = tuple(new_indices) 

406 elif v > idx: 

407 self._map[k] = v + 1 

408 

409 self._map[other_key] = idx + 1 

410 self._body.insert(idx + 1, (other_key, item)) 

411 

412 if key is not None: 

413 dict.__setitem__(self, other_key.key, item.value) 

414 

415 return self 

416 

417 def _insert_at(self, idx: int, key: Key | str, item: Any) -> Container: 

418 if idx > len(self._body) - 1: 

419 raise ValueError(f"Unable to insert at position {idx}") 

420 

421 if not isinstance(key, Key): 

422 key = SingleKey(key) 

423 

424 item = _item(item) 

425 

426 if idx > 0: 

427 previous_item = self._body[idx - 1][1] 

428 if not ( 

429 isinstance(previous_item, Whitespace) 

430 or ends_with_whitespace(previous_item) 

431 or isinstance(item, (AoT, Table)) 

432 or "\n" in previous_item.trivia.trail 

433 ): 

434 previous_item.trivia.trail += "\n" 

435 

436 # Increment indices after the current index 

437 for k, v in self._map.items(): 

438 if isinstance(v, tuple): 

439 new_indices = [] 

440 for v_ in v: 

441 if v_ >= idx: 

442 v_ = v_ + 1 

443 

444 new_indices.append(v_) 

445 

446 self._map[k] = tuple(new_indices) 

447 elif v >= idx: 

448 self._map[k] = v + 1 

449 

450 if key in self._map: 

451 current_idx = self._map[key] 

452 if not isinstance(current_idx, tuple): 

453 current_idx = (current_idx,) 

454 self._map[key] = (*current_idx, idx) 

455 else: 

456 self._map[key] = idx 

457 self._body.insert(idx, (key, item)) 

458 

459 dict.__setitem__(self, key.key, item.value) 

460 

461 return self 

462 

463 def item(self, key: Key | str) -> Item: 

464 """Get an item for the given key.""" 

465 if not isinstance(key, Key): 

466 key = SingleKey(key) 

467 

468 idx = self._map.get(key) 

469 if idx is None: 

470 raise NonExistentKey(key) 

471 

472 if isinstance(idx, tuple): 

473 # The item we are getting is an out of order table 

474 # so we need a proxy to retrieve the proper objects 

475 # from the parent container 

476 return OutOfOrderTableProxy(self, idx) 

477 

478 return self._body[idx][1] 

479 

480 def last_item(self) -> Item | None: 

481 """Get the last item.""" 

482 if self._body: 

483 return self._body[-1][1] 

484 

485 def as_string(self) -> str: 

486 """Render as TOML string.""" 

487 s = "" 

488 for k, v in self._body: 

489 if k is not None: 

490 if isinstance(v, Table): 

491 if ( 

492 s.strip(" ") 

493 and not s.strip(" ").endswith("\n") 

494 and "\n" not in v.trivia.indent 

495 ): 

496 s += "\n" 

497 s += self._render_table(k, v) 

498 elif isinstance(v, AoT): 

499 if ( 

500 s.strip(" ") 

501 and not s.strip(" ").endswith("\n") 

502 and "\n" not in v.trivia.indent 

503 ): 

504 s += "\n" 

505 s += self._render_aot(k, v) 

506 else: 

507 s += self._render_simple_item(k, v) 

508 else: 

509 s += self._render_simple_item(k, v) 

510 

511 return s 

512 

513 def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str: 

514 cur = "" 

515 

516 if table.display_name is not None: 

517 _key = table.display_name 

518 else: 

519 _key = key.as_string() 

520 

521 if prefix is not None: 

522 _key = prefix + "." + _key 

523 

524 if ( 

525 not table.is_super_table() 

526 or ( 

527 any( 

528 not isinstance(v, (Table, AoT, Whitespace, Null)) 

529 for _, v in table.value.body 

530 ) 

531 and not key.is_dotted() 

532 ) 

533 or ( 

534 any(k.is_dotted() for k, v in table.value.body if isinstance(v, Table)) 

535 and not key.is_dotted() 

536 ) 

537 ): 

538 open_, close = "[", "]" 

539 if table.is_aot_element(): 

540 open_, close = "[[", "]]" 

541 

542 newline_in_table_trivia = ( 

543 "\n" if "\n" not in table.trivia.trail and len(table.value) > 0 else "" 

544 ) 

545 cur += ( 

546 f"{table.trivia.indent}" 

547 f"{open_}" 

548 f"{decode(_key)}" 

549 f"{close}" 

550 f"{table.trivia.comment_ws}" 

551 f"{decode(table.trivia.comment)}" 

552 f"{table.trivia.trail}" 

553 f"{newline_in_table_trivia}" 

554 ) 

555 elif table.trivia.indent == "\n": 

556 cur += table.trivia.indent 

557 

558 for k, v in table.value.body: 

559 if isinstance(v, Table): 

560 if ( 

561 cur.strip(" ") 

562 and not cur.strip(" ").endswith("\n") 

563 and "\n" not in v.trivia.indent 

564 ): 

565 cur += "\n" 

566 if v.is_super_table(): 

567 if k.is_dotted() and not key.is_dotted(): 

568 # Dotted key inside table 

569 cur += self._render_table(k, v) 

570 else: 

571 cur += self._render_table(k, v, prefix=_key) 

572 else: 

573 cur += self._render_table(k, v, prefix=_key) 

574 elif isinstance(v, AoT): 

575 if ( 

576 cur.strip(" ") 

577 and not cur.strip(" ").endswith("\n") 

578 and "\n" not in v.trivia.indent 

579 ): 

580 cur += "\n" 

581 cur += self._render_aot(k, v, prefix=_key) 

582 else: 

583 cur += self._render_simple_item( 

584 k, v, prefix=_key if key.is_dotted() else None 

585 ) 

586 

587 return cur 

588 

589 def _render_aot(self, key, aot, prefix=None): 

590 _key = key.as_string() 

591 if prefix is not None: 

592 _key = prefix + "." + _key 

593 

594 cur = "" 

595 _key = decode(_key) 

596 for table in aot.body: 

597 cur += self._render_aot_table(table, prefix=_key) 

598 

599 return cur 

600 

601 def _render_aot_table(self, table: Table, prefix: str | None = None) -> str: 

602 cur = "" 

603 _key = prefix or "" 

604 open_, close = "[[", "]]" 

605 

606 cur += ( 

607 f"{table.trivia.indent}" 

608 f"{open_}" 

609 f"{decode(_key)}" 

610 f"{close}" 

611 f"{table.trivia.comment_ws}" 

612 f"{decode(table.trivia.comment)}" 

613 f"{table.trivia.trail}" 

614 ) 

615 

616 for k, v in table.value.body: 

617 if isinstance(v, Table): 

618 if v.is_super_table(): 

619 if k.is_dotted(): 

620 # Dotted key inside table 

621 cur += self._render_table(k, v) 

622 else: 

623 cur += self._render_table(k, v, prefix=_key) 

624 else: 

625 cur += self._render_table(k, v, prefix=_key) 

626 elif isinstance(v, AoT): 

627 cur += self._render_aot(k, v, prefix=_key) 

628 else: 

629 cur += self._render_simple_item(k, v) 

630 

631 return cur 

632 

633 def _render_simple_item(self, key, item, prefix=None): 

634 if key is None: 

635 return item.as_string() 

636 

637 _key = key.as_string() 

638 if prefix is not None: 

639 _key = prefix + "." + _key 

640 

641 return ( 

642 f"{item.trivia.indent}" 

643 f"{decode(_key)}" 

644 f"{key.sep}" 

645 f"{decode(item.as_string())}" 

646 f"{item.trivia.comment_ws}" 

647 f"{decode(item.trivia.comment)}" 

648 f"{item.trivia.trail}" 

649 ) 

650 

651 def __len__(self) -> int: 

652 return dict.__len__(self) 

653 

654 def __iter__(self) -> Iterator[str]: 

655 return iter(dict.keys(self)) 

656 

657 # Dictionary methods 

658 def __getitem__(self, key: Key | str) -> Item | Container: 

659 item = self.item(key) 

660 if isinstance(item, Item) and item.is_boolean(): 

661 return item.value 

662 

663 return item 

664 

665 def __setitem__(self, key: Key | str, value: Any) -> None: 

666 if key is not None and key in self: 

667 old_key = next(filter(lambda k: k == key, self._map)) 

668 self._replace(old_key, key, value) 

669 else: 

670 self.append(key, value) 

671 

672 def __delitem__(self, key: Key | str) -> None: 

673 self.remove(key) 

674 

675 def setdefault(self, key: Key | str, default: Any) -> Any: 

676 super().setdefault(key, default=default) 

677 return self[key] 

678 

679 def _replace(self, key: Key | str, new_key: Key | str, value: Item) -> None: 

680 if not isinstance(key, Key): 

681 key = SingleKey(key) 

682 

683 idx = self._map.get(key) 

684 if idx is None: 

685 raise NonExistentKey(key) 

686 

687 self._replace_at(idx, new_key, value) 

688 

689 def _replace_at( 

690 self, idx: int | tuple[int], new_key: Key | str, value: Item 

691 ) -> None: 

692 value = _item(value) 

693 

694 if isinstance(idx, tuple): 

695 for i in idx[1:]: 

696 self._body[i] = (None, Null()) 

697 

698 idx = idx[0] 

699 

700 k, v = self._body[idx] 

701 if not isinstance(new_key, Key): 

702 if ( 

703 isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)) 

704 or new_key != k.key 

705 ): 

706 new_key = SingleKey(new_key) 

707 else: # Inherit the sep of the old key 

708 new_key = k 

709 

710 del self._map[k] 

711 self._map[new_key] = idx 

712 if new_key != k: 

713 dict.__delitem__(self, k) 

714 

715 if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)): 

716 # new tables should appear after all non-table values 

717 self.remove(k) 

718 for i in range(idx, len(self._body)): 

719 if isinstance(self._body[i][1], (AoT, Table)): 

720 self._insert_at(i, new_key, value) 

721 idx = i 

722 break 

723 else: 

724 idx = -1 

725 self.append(new_key, value) 

726 else: 

727 # Copying trivia 

728 if not isinstance(value, (Whitespace, AoT)): 

729 value.trivia.indent = v.trivia.indent 

730 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws 

731 value.trivia.comment = value.trivia.comment or v.trivia.comment 

732 value.trivia.trail = v.trivia.trail 

733 self._body[idx] = (new_key, value) 

734 

735 if hasattr(value, "invalidate_display_name"): 

736 value.invalidate_display_name() # type: ignore[attr-defined] 

737 

738 if isinstance(value, Table): 

739 # Insert a cosmetic new line for tables if: 

740 # - it does not have it yet OR is not followed by one 

741 # - it is not the last item, or 

742 # - The table being replaced has a newline 

743 last, _ = self._previous_item_with_index() 

744 idx = last if idx < 0 else idx 

745 has_ws = ends_with_whitespace(value) 

746 replace_has_ws = ( 

747 isinstance(v, Table) 

748 and v.value.body 

749 and isinstance(v.value.body[-1][1], Whitespace) 

750 ) 

751 next_ws = idx < last and isinstance(self._body[idx + 1][1], Whitespace) 

752 if (idx < last or replace_has_ws) and not (next_ws or has_ws): 

753 value.append(None, Whitespace("\n")) 

754 

755 dict.__setitem__(self, new_key.key, value.value) 

756 

757 def __str__(self) -> str: 

758 return str(self.value) 

759 

760 def __repr__(self) -> str: 

761 return repr(self.value) 

762 

763 def __eq__(self, other: dict) -> bool: 

764 if not isinstance(other, dict): 

765 return NotImplemented 

766 

767 return self.value == other 

768 

769 def _getstate(self, protocol): 

770 return (self._parsed,) 

771 

772 def __reduce__(self): 

773 return self.__reduce_ex__(2) 

774 

775 def __reduce_ex__(self, protocol): 

776 return ( 

777 self.__class__, 

778 self._getstate(protocol), 

779 (self._map, self._body, self._parsed, self._table_keys), 

780 ) 

781 

782 def __setstate__(self, state): 

783 self._map = state[0] 

784 self._body = state[1] 

785 self._parsed = state[2] 

786 self._table_keys = state[3] 

787 

788 for key, item in self._body: 

789 if key is not None: 

790 dict.__setitem__(self, key.key, item.value) 

791 

792 def copy(self) -> Container: 

793 return copy.copy(self) 

794 

795 def __copy__(self) -> Container: 

796 c = self.__class__(self._parsed) 

797 for k, v in dict.items(self): 

798 dict.__setitem__(c, k, v) 

799 

800 c._body += self.body 

801 c._map.update(self._map) 

802 

803 return c 

804 

805 def _previous_item_with_index( 

806 self, idx: int | None = None, ignore=(Null,) 

807 ) -> tuple[int, Item] | None: 

808 """Find the immediate previous item before index ``idx``""" 

809 if idx is None or idx > len(self._body): 

810 idx = len(self._body) 

811 for i in range(idx - 1, -1, -1): 

812 v = self._body[i][-1] 

813 if not isinstance(v, ignore): 

814 return i, v 

815 return None 

816 

817 def _previous_item(self, idx: int | None = None, ignore=(Null,)) -> Item | None: 

818 """Find the immediate previous item before index ``idx``. 

819 If ``idx`` is not given, the last item is returned. 

820 """ 

821 prev = self._previous_item_with_index(idx, ignore) 

822 return prev[-1] if prev else None 

823 

824 

825class OutOfOrderTableProxy(_CustomDict): 

826 @staticmethod 

827 def validate(container: Container, indices: tuple[int, ...]) -> None: 

828 """Validate out of order tables in the given container""" 

829 # Append all items to a temp container to see if there is any error 

830 temp_container = Container(True) 

831 for i in indices: 

832 _, item = container._body[i] 

833 

834 if isinstance(item, Table): 

835 for k, v in item.value.body: 

836 temp_container.append(k, v, validate=False) 

837 

838 temp_container._validate_out_of_order_table() 

839 

840 def __init__(self, container: Container, indices: tuple[int, ...]) -> None: 

841 self._container = container 

842 self._internal_container = Container(True) 

843 self._tables: list[Table] = [] 

844 self._tables_map: dict[Key, list[int]] = {} 

845 

846 for i in indices: 

847 _, item = self._container._body[i] 

848 

849 if isinstance(item, Table): 

850 self._tables.append(item) 

851 table_idx = len(self._tables) - 1 

852 for k, v in item.value.body: 

853 self._internal_container._raw_append(k, v) 

854 indices = self._tables_map.setdefault(k, []) 

855 if table_idx not in indices: 

856 indices.append(table_idx) 

857 if k is not None: 

858 dict.__setitem__(self, k.key, v) 

859 

860 self._internal_container._validate_out_of_order_table() 

861 

862 def unwrap(self) -> str: 

863 return self._internal_container.unwrap() 

864 

865 @property 

866 def value(self): 

867 return self._internal_container.value 

868 

869 def __getitem__(self, key: Key | str) -> Any: 

870 if key not in self._internal_container: 

871 raise NonExistentKey(key) 

872 

873 return self._internal_container[key] 

874 

875 def __setitem__(self, key: Key | str, value: Any) -> None: 

876 from .items import item 

877 

878 def _is_table_or_aot(it: Any) -> bool: 

879 return isinstance(item(it), (Table, AoT)) 

880 

881 if key in self._tables_map: 

882 # Overwrite the first table and remove others 

883 indices = self._tables_map[key] 

884 while len(indices) > 1: 

885 table = self._tables[indices.pop()] 

886 self._remove_table(table) 

887 old_value = self._tables[indices[0]][key] 

888 if _is_table_or_aot(old_value) and not _is_table_or_aot(value): 

889 # Remove the entry from the map and set value again. 

890 del self._tables[indices[0]][key] 

891 del self._tables_map[key] 

892 self[key] = value 

893 return 

894 self._tables[indices[0]][key] = value 

895 elif self._tables: 

896 if not _is_table_or_aot(value): # if the value is a plain value 

897 for table in self._tables: 

898 # find the first table that allows plain values 

899 if any(not _is_table_or_aot(v) for _, v in table.items()): 

900 table[key] = value 

901 break 

902 else: 

903 self._tables[0][key] = value 

904 else: 

905 self._tables[0][key] = value 

906 else: 

907 self._container[key] = value 

908 

909 self._internal_container[key] = value 

910 if key is not None: 

911 dict.__setitem__(self, key, value) 

912 

913 def _remove_table(self, table: Table) -> None: 

914 """Remove table from the parent container""" 

915 self._tables.remove(table) 

916 for idx, item in enumerate(self._container._body): 

917 if item[1] is table: 

918 self._container._remove_at(idx) 

919 break 

920 

921 def __delitem__(self, key: Key | str) -> None: 

922 if key not in self._tables_map: 

923 raise NonExistentKey(key) 

924 

925 for i in reversed(self._tables_map[key]): 

926 table = self._tables[i] 

927 del table[key] 

928 if not table and len(self._tables) > 1: 

929 self._remove_table(table) 

930 

931 del self._tables_map[key] 

932 del self._internal_container[key] 

933 if key is not None: 

934 dict.__delitem__(self, key) 

935 

936 def __iter__(self) -> Iterator[str]: 

937 return iter(dict.keys(self)) 

938 

939 def __len__(self) -> int: 

940 return dict.__len__(self) 

941 

942 def setdefault(self, key: Key | str, default: Any) -> Any: 

943 super().setdefault(key, default=default) 

944 return self[key] 

945 

946 

947def ends_with_whitespace(it: Any) -> bool: 

948 """Returns ``True`` if the given item ``it`` is a ``Table`` or ``AoT`` object 

949 ending with a ``Whitespace``. 

950 """ 

951 return ( 

952 isinstance(it, Table) and isinstance(it.value._previous_item(), Whitespace) 

953 ) or (isinstance(it, AoT) and len(it) > 0 and isinstance(it[-1], Whitespace))