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

612 statements  

1from __future__ import annotations 

2 

3import copy 

4import math 

5 

6from collections.abc import Iterator 

7from typing import Any 

8 

9from tomlkit._compat import decode 

10from tomlkit._types import _CustomDict 

11from tomlkit._utils import merge_dicts 

12from tomlkit.exceptions import KeyAlreadyPresent 

13from tomlkit.exceptions import NonExistentKey 

14from tomlkit.exceptions import TOMLKitError 

15from tomlkit.items import AoT 

16from tomlkit.items import Comment 

17from tomlkit.items import Item 

18from tomlkit.items import Key 

19from tomlkit.items import Null 

20from tomlkit.items import SingleKey 

21from tomlkit.items import Table 

22from tomlkit.items import Trivia 

23from tomlkit.items import Whitespace 

24from tomlkit.items import item as _item 

25 

26 

27_NOT_SET = object() 

28 

29 

30class Container(_CustomDict): 

31 """ 

32 A container for items within a TOMLDocument. 

33 

34 This class implements the `dict` interface with copy/deepcopy protocol. 

35 """ 

36 

37 def __init__(self, parsed: bool = False) -> None: 

38 self._map: dict[SingleKey, int | tuple[int, ...]] = {} 

39 self._body: list[tuple[Key | None, Item]] = [] 

40 self._parsed = parsed 

41 self._table_keys = [] 

42 

43 @property 

44 def body(self) -> list[tuple[Key | None, Item]]: 

45 return self._body 

46 

47 def unwrap(self) -> dict[str, Any]: 

48 """Returns as pure python object (ppo)""" 

49 unwrapped = {} 

50 for k, v in self.items(): 

51 if k is None: 

52 continue 

53 

54 if isinstance(k, Key): 

55 k = k.key 

56 

57 if hasattr(v, "unwrap"): 

58 v = v.unwrap() 

59 

60 if k in unwrapped: 

61 merge_dicts(unwrapped[k], v) 

62 else: 

63 unwrapped[k] = v 

64 

65 return unwrapped 

66 

67 @property 

68 def value(self) -> dict[str, Any]: 

69 """The wrapped dict value""" 

70 d = {} 

71 for k, v in self._body: 

72 if k is None: 

73 continue 

74 

75 k = k.key 

76 v = v.value 

77 

78 if isinstance(v, Container): 

79 v = v.value 

80 

81 if k in d: 

82 merge_dicts(d[k], v) 

83 else: 

84 d[k] = v 

85 

86 return d 

87 

88 def parsing(self, parsing: bool) -> None: 

89 self._parsed = parsing 

90 

91 for _, v in self._body: 

92 if isinstance(v, Table): 

93 v.value.parsing(parsing) 

94 elif isinstance(v, AoT): 

95 for t in v.body: 

96 t.value.parsing(parsing) 

97 

98 def add(self, key: Key | Item | str, item: Item | None = None) -> Container: 

99 """ 

100 Adds an item to the current Container. 

101 

102 :Example: 

103 

104 >>> # add a key-value pair 

105 >>> doc.add('key', 'value') 

106 >>> # add a comment or whitespace or newline 

107 >>> doc.add(comment('# comment')) 

108 """ 

109 if item is None: 

110 if not isinstance(key, (Comment, Whitespace)): 

111 raise ValueError( 

112 "Non comment/whitespace items must have an associated key" 

113 ) 

114 

115 key, item = None, key 

116 

117 return self.append(key, item) 

118 

119 def _handle_dotted_key(self, key: Key, value: Item) -> None: 

120 if isinstance(value, (Table, AoT)): 

121 raise TOMLKitError("Can't add a table to a dotted key") 

122 name, *mid, last = key 

123 name._dotted = True 

124 table = current = Table(Container(True), Trivia(), False, is_super_table=True) 

125 for _name in mid: 

126 _name._dotted = True 

127 new_table = Table(Container(True), Trivia(), False, is_super_table=True) 

128 current.append(_name, new_table) 

129 current = new_table 

130 

131 last.sep = key.sep 

132 current.append(last, value) 

133 

134 self.append(name, table) 

135 return 

136 

137 def _get_last_index_before_table(self) -> int: 

138 last_index = -1 

139 for i, (k, v) in enumerate(self._body): 

140 if isinstance(v, Null): 

141 continue # Null elements are inserted after deletion 

142 

143 if isinstance(v, Whitespace) and not v.is_fixed(): 

144 continue 

145 

146 if isinstance(v, (Table, AoT)) and not k.is_dotted(): 

147 break 

148 last_index = i 

149 return last_index + 1 

150 

151 def _validate_out_of_order_table(self, key: SingleKey | None = None) -> None: 

152 if key is None: 

153 for k in self._map: 

154 assert k is not None 

155 self._validate_out_of_order_table(k) 

156 return 

157 if key not in self._map or not isinstance(self._map[key], tuple): 

158 return 

159 OutOfOrderTableProxy.validate(self, self._map[key]) 

160 

161 def append( 

162 self, key: Key | str | None, item: Item, validate: bool = True 

163 ) -> Container: 

164 """Similar to :meth:`add` but both key and value must be given.""" 

165 if not isinstance(key, Key) and key is not None: 

166 key = SingleKey(key) 

167 

168 if not isinstance(item, Item): 

169 item = _item(item) 

170 

171 if key is not None and key.is_multi(): 

172 self._handle_dotted_key(key, item) 

173 return self 

174 

175 if isinstance(item, (AoT, Table)) and item.name is None: 

176 item.name = key.key 

177 

178 prev = self._previous_item() 

179 prev_ws = isinstance(prev, Whitespace) or ends_with_whitespace(prev) 

180 if isinstance(item, Table): 

181 if not self._parsed: 

182 item.invalidate_display_name() 

183 if ( 

184 self._body 

185 and not (self._parsed or item.trivia.indent or prev_ws) 

186 and not key.is_dotted() 

187 ): 

188 item.trivia.indent = "\n" 

189 

190 if isinstance(item, AoT) and self._body and not self._parsed: 

191 item.invalidate_display_name() 

192 if item and not ("\n" in item[0].trivia.indent or prev_ws): 

193 item[0].trivia.indent = "\n" + item[0].trivia.indent 

194 

195 if key is not None and key in self: 

196 current_idx = self._map[key] 

197 if isinstance(current_idx, tuple): 

198 current_body_element = self._body[current_idx[-1]] 

199 else: 

200 current_body_element = self._body[current_idx] 

201 

202 current = current_body_element[1] 

203 

204 if isinstance(item, Table): 

205 if not isinstance(current, (Table, AoT)): 

206 raise KeyAlreadyPresent(key) 

207 

208 if item.is_aot_element(): 

209 # New AoT element found later on 

210 # Adding it to the current AoT 

211 if not isinstance(current, AoT): 

212 current = AoT([current, item], parsed=self._parsed) 

213 

214 self._replace(key, key, current) 

215 else: 

216 current.append(item) 

217 

218 return self 

219 elif current.is_aot(): 

220 if not item.is_aot_element(): 

221 # Tried to define a table after an AoT with the same name. 

222 raise KeyAlreadyPresent(key) 

223 

224 current.append(item) 

225 

226 return self 

227 elif current.is_super_table(): 

228 if item.is_super_table(): 

229 # We need to merge both super tables 

230 if ( 

231 key.is_dotted() 

232 or current_body_element[0].is_dotted() 

233 or self._table_keys[-1] != current_body_element[0] 

234 ): 

235 if key.is_dotted() and not self._parsed: 

236 idx = self._get_last_index_before_table() 

237 else: 

238 idx = len(self._body) 

239 

240 if idx < len(self._body): 

241 self._insert_at(idx, key, item) 

242 else: 

243 self._raw_append(key, item) 

244 

245 if validate: 

246 self._validate_out_of_order_table(key) 

247 

248 return self 

249 

250 # Create a new element to replace the old one 

251 current = copy.deepcopy(current) 

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

253 current.append(k, v) 

254 self._body[ 

255 ( 

256 current_idx[-1] 

257 if isinstance(current_idx, tuple) 

258 else current_idx 

259 ) 

260 ] = (current_body_element[0], current) 

261 

262 return self 

263 elif current_body_element[0].is_dotted(): 

264 raise TOMLKitError("Redefinition of an existing table") 

265 else: 

266 # Merging a concrete table into an existing implicit/super 

267 # table is only valid if it does not redefine existing 

268 # subtrees via dotted keys and does not change prior types. 

269 self._validate_table_candidate(current, item) 

270 elif not item.is_super_table(): 

271 raise KeyAlreadyPresent(key) 

272 elif isinstance(item, AoT): 

273 if not isinstance(current, AoT): 

274 # Tried to define an AoT after a table with the same name. 

275 raise KeyAlreadyPresent(key) 

276 

277 for table in item.body: 

278 current.append(table) 

279 

280 return self 

281 else: 

282 raise KeyAlreadyPresent(key) 

283 

284 is_table = isinstance(item, (Table, AoT)) 

285 if ( 

286 key is not None 

287 and self._body 

288 and not self._parsed 

289 and (not is_table or key.is_dotted()) 

290 ): 

291 # If there is already at least one table in the current container 

292 # and the given item is not a table, we need to find the last 

293 # item that is not a table and insert after it 

294 # If no such item exists, insert at the top of the table 

295 last_index = self._get_last_index_before_table() 

296 

297 if last_index < len(self._body): 

298 after_item = self._body[last_index][1] 

299 if not ( 

300 isinstance(after_item, Whitespace) 

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

302 ): 

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

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

305 else: 

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

307 if not ( 

308 isinstance(previous_item, Whitespace) 

309 or ends_with_whitespace(previous_item) 

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

311 ): 

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

313 

314 self._raw_append(key, item) 

315 return self 

316 

317 def _validate_table_candidate(self, current: Table, candidate: Table) -> None: 

318 for k, v in candidate.value.body: 

319 if k is None: 

320 continue 

321 

322 if k in current.value._map: 

323 existing = current.value.item(k) 

324 if isinstance(existing, (Table, AoT)) != isinstance(v, (Table, AoT)): 

325 raise KeyAlreadyPresent(k) 

326 if k.is_dotted(): 

327 raise TOMLKitError("Redefinition of an existing table") 

328 continue 

329 

330 if not k.is_dotted(): 

331 continue 

332 

333 head = next(iter(k)) 

334 if head in current.value._map: 

335 raise TOMLKitError("Redefinition of an existing table") 

336 

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

338 if key in self._map: 

339 current_idx = self._map[key] 

340 if not isinstance(current_idx, tuple): 

341 current_idx = (current_idx,) 

342 

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

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

345 raise KeyAlreadyPresent(key) 

346 

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

348 elif key is not None: 

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

350 

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

352 if item.is_table(): 

353 self._table_keys.append(key) 

354 

355 if key is not None: 

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

357 

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

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

360 index = self._map.get(key) 

361 if index is None: 

362 raise NonExistentKey(key) 

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

364 

365 if isinstance(index, tuple): 

366 index = list(index) 

367 index.remove(idx) 

368 if len(index) == 1: 

369 index = index.pop() 

370 else: 

371 index = tuple(index) 

372 self._map[key] = index 

373 else: 

374 dict.__delitem__(self, key.key) 

375 self._map.pop(key) 

376 

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

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

379 if not isinstance(key, Key): 

380 key = SingleKey(key) 

381 

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

383 if idx is None: 

384 raise NonExistentKey(key) 

385 

386 if isinstance(idx, tuple): 

387 for i in idx: 

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

389 else: 

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

391 

392 dict.__delitem__(self, key.key) 

393 

394 return self 

395 

396 def _insert_after( 

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

398 ) -> Container: 

399 if key is None: 

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

401 

402 if key not in self: 

403 raise NonExistentKey(key) 

404 

405 if not isinstance(key, Key): 

406 key = SingleKey(key) 

407 

408 if not isinstance(other_key, Key): 

409 other_key = SingleKey(other_key) 

410 

411 item = _item(item) 

412 

413 idx = self._map[key] 

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

415 if isinstance(idx, tuple): 

416 idx = max(idx) 

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

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

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

420 

421 # Increment indices after the current index 

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

423 if isinstance(v, tuple): 

424 new_indices = [] 

425 for v_ in v: 

426 if v_ > idx: 

427 v_ = v_ + 1 

428 

429 new_indices.append(v_) 

430 

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

432 elif v > idx: 

433 self._map[k] = v + 1 

434 

435 self._map[other_key] = idx + 1 

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

437 

438 if key is not None: 

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

440 

441 return self 

442 

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

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

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

446 

447 if not isinstance(key, Key): 

448 key = SingleKey(key) 

449 

450 item = _item(item) 

451 

452 if idx > 0: 

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

454 if not ( 

455 isinstance(previous_item, Whitespace) 

456 or ends_with_whitespace(previous_item) 

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

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

459 ): 

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

461 

462 # Increment indices after the current index 

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

464 if isinstance(v, tuple): 

465 new_indices = [] 

466 for v_ in v: 

467 if v_ >= idx: 

468 v_ = v_ + 1 

469 

470 new_indices.append(v_) 

471 

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

473 elif v >= idx: 

474 self._map[k] = v + 1 

475 

476 if key in self._map: 

477 current_idx = self._map[key] 

478 if not isinstance(current_idx, tuple): 

479 current_idx = (current_idx,) 

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

481 else: 

482 self._map[key] = idx 

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

484 

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

486 

487 return self 

488 

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

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

491 if not isinstance(key, Key): 

492 key = SingleKey(key) 

493 

494 idx = self._map.get(key) 

495 if idx is None: 

496 raise NonExistentKey(key) 

497 

498 if isinstance(idx, tuple): 

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

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

501 # from the parent container 

502 return OutOfOrderTableProxy(self, idx) 

503 

504 return self._body[idx][1] 

505 

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

507 """Get the last item.""" 

508 if self._body: 

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

510 

511 def as_string(self) -> str: 

512 """Render as TOML string.""" 

513 s = "" 

514 for k, v in self._body: 

515 if k is not None: 

516 if isinstance(v, Table): 

517 if ( 

518 s.strip(" ") 

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

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

521 ): 

522 s += "\n" 

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

524 elif isinstance(v, AoT): 

525 if ( 

526 s.strip(" ") 

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

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

529 ): 

530 s += "\n" 

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

532 else: 

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

534 else: 

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

536 

537 return s 

538 

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

540 cur = "" 

541 

542 if table.display_name is not None: 

543 _key = table.display_name 

544 else: 

545 _key = key.as_string() 

546 

547 if prefix is not None: 

548 _key = prefix + "." + _key 

549 

550 if ( 

551 not table.is_super_table() 

552 or ( 

553 any( 

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

555 for _, v in table.value.body 

556 ) 

557 and not key.is_dotted() 

558 ) 

559 or ( 

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

561 and not key.is_dotted() 

562 ) 

563 ): 

564 open_, close = "[", "]" 

565 if table.is_aot_element(): 

566 open_, close = "[[", "]]" 

567 

568 newline_in_table_trivia = ( 

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

570 ) 

571 cur += ( 

572 f"{table.trivia.indent}" 

573 f"{open_}" 

574 f"{decode(_key)}" 

575 f"{close}" 

576 f"{table.trivia.comment_ws}" 

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

578 f"{table.trivia.trail}" 

579 f"{newline_in_table_trivia}" 

580 ) 

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

582 cur += table.trivia.indent 

583 

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

585 if isinstance(v, Table): 

586 if ( 

587 cur.strip(" ") 

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

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

590 ): 

591 cur += "\n" 

592 if v.is_super_table(): 

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

594 # Dotted key inside table 

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

596 else: 

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

598 else: 

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

600 elif isinstance(v, AoT): 

601 if ( 

602 cur.strip(" ") 

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

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

605 ): 

606 cur += "\n" 

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

608 else: 

609 cur += self._render_simple_item( 

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

611 ) 

612 

613 return cur 

614 

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

616 _key = key.as_string() 

617 if prefix is not None: 

618 _key = prefix + "." + _key 

619 

620 cur = "" 

621 _key = decode(_key) 

622 for table in aot.body: 

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

624 

625 return cur 

626 

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

628 cur = "" 

629 _key = prefix or "" 

630 open_, close = "[[", "]]" 

631 

632 cur += ( 

633 f"{table.trivia.indent}" 

634 f"{open_}" 

635 f"{decode(_key)}" 

636 f"{close}" 

637 f"{table.trivia.comment_ws}" 

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

639 f"{table.trivia.trail}" 

640 ) 

641 

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

643 if isinstance(v, Table): 

644 if v.is_super_table(): 

645 if k.is_dotted(): 

646 # Dotted key inside table 

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

648 else: 

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

650 else: 

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

652 elif isinstance(v, AoT): 

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

654 else: 

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

656 

657 return cur 

658 

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

660 if key is None: 

661 return item.as_string() 

662 

663 _key = key.as_string() 

664 if prefix is not None: 

665 _key = prefix + "." + _key 

666 

667 return ( 

668 f"{item.trivia.indent}" 

669 f"{decode(_key)}" 

670 f"{key.sep}" 

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

672 f"{item.trivia.comment_ws}" 

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

674 f"{item.trivia.trail}" 

675 ) 

676 

677 def __len__(self) -> int: 

678 return dict.__len__(self) 

679 

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

681 return iter(dict.keys(self)) 

682 

683 # Dictionary methods 

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

685 item = self.item(key) 

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

687 return item.value 

688 

689 return item 

690 

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

692 if key is not None and key in self: 

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

694 self._replace(old_key, key, value) 

695 else: 

696 self.append(key, value) 

697 

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

699 self.remove(key) 

700 

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

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

703 return self[key] 

704 

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

706 if not isinstance(key, Key): 

707 key = SingleKey(key) 

708 

709 idx = self._map.get(key) 

710 if idx is None: 

711 raise NonExistentKey(key) 

712 

713 self._replace_at(idx, new_key, value) 

714 

715 def _replace_at( 

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

717 ) -> None: 

718 value = _item(value) 

719 

720 if isinstance(idx, tuple): 

721 for i in idx[1:]: 

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

723 

724 idx = idx[0] 

725 

726 k, v = self._body[idx] 

727 if not isinstance(new_key, Key): 

728 if ( 

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

730 or new_key != k.key 

731 ): 

732 new_key = SingleKey(new_key) 

733 else: # Inherit the sep of the old key 

734 new_key = k 

735 

736 del self._map[k] 

737 self._map[new_key] = idx 

738 if new_key != k: 

739 dict.__delitem__(self, k) 

740 

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

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

743 self.remove(k) 

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

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

746 self._insert_at(i, new_key, value) 

747 idx = i 

748 break 

749 else: 

750 idx = -1 

751 self.append(new_key, value) 

752 else: 

753 # Copying trivia 

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

755 value.trivia.indent = v.trivia.indent 

756 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws 

757 value.trivia.comment = value.trivia.comment or v.trivia.comment 

758 value.trivia.trail = v.trivia.trail 

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

760 

761 if hasattr(value, "invalidate_display_name"): 

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

763 

764 if isinstance(value, Table): 

765 # Insert a cosmetic new line for tables if: 

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

767 # - it is not the last item, or 

768 # - The table being replaced has a newline 

769 last, _ = self._previous_item_with_index() 

770 idx = last if idx < 0 else idx 

771 has_ws = ends_with_whitespace(value) 

772 replace_has_ws = ( 

773 isinstance(v, Table) 

774 and v.value.body 

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

776 ) 

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

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

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

780 

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

782 

783 def __str__(self) -> str: 

784 return str(self.value) 

785 

786 def __repr__(self) -> str: 

787 return repr(self.value) 

788 

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

790 if not isinstance(other, dict): 

791 return NotImplemented 

792 

793 return _equal_with_nan(self.value, other) 

794 

795 def _getstate(self, protocol): 

796 return (self._parsed,) 

797 

798 def __reduce__(self): 

799 return self.__reduce_ex__(2) 

800 

801 def __reduce_ex__(self, protocol): 

802 return ( 

803 self.__class__, 

804 self._getstate(protocol), 

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

806 ) 

807 

808 def __setstate__(self, state): 

809 self._map = state[0] 

810 self._body = state[1] 

811 self._parsed = state[2] 

812 self._table_keys = state[3] 

813 

814 for key, item in self._body: 

815 if key is not None: 

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

817 

818 def copy(self) -> Container: 

819 return copy.copy(self) 

820 

821 def __copy__(self) -> Container: 

822 c = self.__class__(self._parsed) 

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

824 dict.__setitem__(c, k, v) 

825 

826 c._body += self.body 

827 c._map.update(self._map) 

828 

829 return c 

830 

831 def _previous_item_with_index( 

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

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

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

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

836 idx = len(self._body) 

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

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

839 if not isinstance(v, ignore): 

840 return i, v 

841 return None 

842 

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

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

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

846 """ 

847 prev = self._previous_item_with_index(idx, ignore) 

848 return prev[-1] if prev else None 

849 

850 

851class OutOfOrderTableProxy(_CustomDict): 

852 @staticmethod 

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

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

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

856 temp_container = Container(True) 

857 for i in indices: 

858 _, item = container._body[i] 

859 

860 if isinstance(item, Table): 

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

862 temp_container.append(k, v, validate=True) 

863 

864 temp_container._validate_out_of_order_table() 

865 

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

867 self._container = container 

868 self._internal_container = Container(True) 

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

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

871 

872 for i in indices: 

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

874 

875 if isinstance(item, Table): 

876 self._tables.append(item) 

877 table_idx = len(self._tables) - 1 

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

879 self._internal_container._raw_append(k, v) 

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

881 if table_idx not in indices: 

882 indices.append(table_idx) 

883 if k is not None: 

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

885 

886 self._internal_container._validate_out_of_order_table() 

887 

888 def unwrap(self) -> str: 

889 return self._internal_container.unwrap() 

890 

891 @property 

892 def value(self): 

893 return self._internal_container.value 

894 

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

896 if key not in self._internal_container: 

897 raise NonExistentKey(key) 

898 

899 return self._internal_container[key] 

900 

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

902 from .items import item 

903 

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

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

906 

907 if key in self._tables_map: 

908 # Overwrite the first table and remove others 

909 indices = self._tables_map[key] 

910 while len(indices) > 1: 

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

912 self._remove_table(table) 

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

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

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

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

917 del self._tables_map[key] 

918 self[key] = value 

919 return 

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

921 elif self._tables: 

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

923 for table in self._tables: 

924 # find the first table that allows plain values 

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

926 table[key] = value 

927 break 

928 else: 

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

930 else: 

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

932 else: 

933 self._container[key] = value 

934 

935 self._internal_container[key] = value 

936 if key is not None: 

937 dict.__setitem__(self, key, value) 

938 

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

940 """Remove table from the parent container""" 

941 self._tables.remove(table) 

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

943 if item[1] is table: 

944 self._container._remove_at(idx) 

945 break 

946 

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

948 if key not in self._tables_map: 

949 raise NonExistentKey(key) 

950 

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

952 table = self._tables[i] 

953 del table[key] 

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

955 self._remove_table(table) 

956 

957 del self._tables_map[key] 

958 del self._internal_container[key] 

959 if key is not None: 

960 dict.__delitem__(self, key) 

961 

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

963 return iter(dict.keys(self)) 

964 

965 def __len__(self) -> int: 

966 return dict.__len__(self) 

967 

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

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

970 return self[key] 

971 

972 

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

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

975 ending with a ``Whitespace``. 

976 """ 

977 return ( 

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

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

980 

981 

982def _equal_with_nan(left: Any, right: Any) -> bool: 

983 if isinstance(left, dict) and isinstance(right, dict): 

984 if left.keys() != right.keys(): 

985 return False 

986 return all(_equal_with_nan(left[k], right[k]) for k in left) 

987 

988 if isinstance(left, list) and isinstance(right, list): 

989 if len(left) != len(right): 

990 return False 

991 return all(_equal_with_nan(l, r) for l, r in zip(left, right)) # noqa: B905, E741 

992 

993 if isinstance(left, float) and isinstance(right, float): 

994 if math.isnan(left) and math.isnan(right): 

995 return True 

996 

997 return left == right