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

642 statements  

1from __future__ import annotations 

2 

3import copy 

4import math 

5 

6from collections.abc import Iterator 

7from typing import TYPE_CHECKING 

8from typing import Any 

9 

10 

11if TYPE_CHECKING: 

12 from typing import Self 

13 

14from tomlkit._compat import decode 

15from tomlkit._types import _CustomDict 

16from tomlkit._utils import merge_dicts 

17from tomlkit.exceptions import KeyAlreadyPresent 

18from tomlkit.exceptions import NonExistentKey 

19from tomlkit.exceptions import TOMLKitError 

20from tomlkit.items import AoT 

21from tomlkit.items import Comment 

22from tomlkit.items import Item 

23from tomlkit.items import Key 

24from tomlkit.items import Null 

25from tomlkit.items import SingleKey 

26from tomlkit.items import Table 

27from tomlkit.items import Trivia 

28from tomlkit.items import Whitespace 

29from tomlkit.items import item as _item 

30 

31 

32_NOT_SET = object() 

33 

34 

35class Container(_CustomDict): # type: ignore[type-arg] 

36 """ 

37 A container for items within a TOMLDocument. 

38 

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

40 """ 

41 

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

43 self._map: dict[Key, int | tuple[int, ...]] = {} 

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

45 self._parsed = parsed 

46 self._table_keys: list[Key] = [] 

47 

48 @property 

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

50 return self._body 

51 

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

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

54 unwrapped: dict[str, Any] = {} 

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

56 if k is None: 

57 continue 

58 

59 key_str: str = k.key if isinstance(k, Key) else k 

60 val: Any = v.unwrap() if hasattr(v, "unwrap") else v 

61 

62 if key_str in unwrapped: 

63 merge_dicts(unwrapped[key_str], val) 

64 else: 

65 unwrapped[key_str] = val 

66 

67 return unwrapped 

68 

69 @property 

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

71 """The wrapped dict value""" 

72 d: dict[str, Any] = {} 

73 for k, v in self._body: 

74 if k is None: 

75 continue 

76 

77 key_str = k.key 

78 val: Any = v.value 

79 

80 if isinstance(val, Container): 

81 val = val.value 

82 

83 if key_str in d: 

84 merge_dicts(d[key_str], val) 

85 else: 

86 d[key_str] = val 

87 

88 return d 

89 

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

91 self._parsed = parsing 

92 

93 for _, v in self._body: 

94 if isinstance(v, Table): 

95 v.value.parsing(parsing) 

96 elif isinstance(v, AoT): 

97 for t in v.body: 

98 t.value.parsing(parsing) 

99 

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

101 """ 

102 Adds an item to the current Container. 

103 

104 :Example: 

105 

106 >>> # add a key-value pair 

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

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

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

110 """ 

111 if item is None: 

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

113 raise ValueError( 

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

115 ) 

116 

117 return self.append(None, key) 

118 

119 assert not isinstance(key, Item) 

120 return self.append(key, item) 

121 

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

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

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

125 name, *mid, last = key 

126 name._dotted = True 

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

128 for _name in mid: 

129 _name._dotted = True 

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

131 current.append(_name, new_table) 

132 current = new_table 

133 

134 last.sep = key.sep 

135 current.append(last, value) 

136 

137 self.append(name, table) 

138 return 

139 

140 def _get_last_index_before_table(self) -> int: 

141 last_index = -1 

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

143 if isinstance(v, Null): 

144 continue # Null elements are inserted after deletion 

145 

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

147 continue 

148 

149 if isinstance(v, (Table, AoT)) and k is not None and not k.is_dotted(): 

150 break 

151 last_index = i 

152 return last_index + 1 

153 

154 def _validate_out_of_order_table(self, key: Key | None = None) -> None: 

155 if key is None: 

156 for k in self._map: 

157 assert k is not None 

158 self._validate_out_of_order_table(k) 

159 return 

160 if key not in self._map: 

161 return 

162 current_idx = self._map[key] 

163 if not isinstance(current_idx, tuple): 

164 return 

165 OutOfOrderTableProxy.validate(self, current_idx) 

166 

167 def append( 

168 self, key: Key | str | None, item: Any, validate: bool = True 

169 ) -> Container: 

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

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

172 key = SingleKey(key) 

173 

174 if not isinstance(item, Item): 

175 item = _item(item) 

176 

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

178 self._handle_dotted_key(key, item) 

179 return self 

180 

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

182 assert isinstance(key, Key) 

183 item.name = key.key 

184 

185 prev = self._previous_item() 

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

187 if isinstance(item, Table): 

188 if not self._parsed: 

189 item.invalidate_display_name() 

190 if ( 

191 self._body 

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

193 and key is not None 

194 and not key.is_dotted() 

195 ): 

196 item.trivia.indent = "\n" 

197 

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

199 item.invalidate_display_name() 

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

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

202 

203 if key is not None and key in self: 

204 current_idx = self._map[key] 

205 if isinstance(current_idx, tuple): 

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

207 else: 

208 current_body_element = self._body[current_idx] 

209 

210 current = current_body_element[1] 

211 

212 if isinstance(item, Table): 

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

214 raise KeyAlreadyPresent(key) 

215 

216 if item.is_aot_element(): 

217 # New AoT element found later on 

218 # Adding it to the current AoT 

219 if not isinstance(current, AoT): 

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

221 

222 self._replace(key, key, current) 

223 else: 

224 current.append(item) 

225 

226 return self 

227 elif isinstance(current, AoT): 

228 if not item.is_aot_element(): 

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

230 raise KeyAlreadyPresent(key) 

231 

232 current.append(item) 

233 

234 return self 

235 elif current.is_super_table(): 

236 if item.is_super_table(): 

237 # We need to merge both super tables 

238 if ( 

239 key.is_dotted() 

240 or ( 

241 current_body_element[0] is not None 

242 and current_body_element[0].is_dotted() 

243 ) 

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

245 ): 

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

247 idx = self._get_last_index_before_table() 

248 else: 

249 idx = len(self._body) 

250 

251 if idx < len(self._body): 

252 self._insert_at(idx, key, item) 

253 else: 

254 self._raw_append(key, item) 

255 

256 if validate: 

257 self._validate_out_of_order_table(key) 

258 

259 return self 

260 

261 # Merge the new super table's body into the existing one 

262 # in place. Previously this deep-copied `current` before 

263 # appending, which is O(size of current) on every merge 

264 # and therefore O(n^2) when many subtables share a super 

265 # table (e.g. consecutive `[a.b.c]` / `[a.b.d]` headers). 

266 # Mutating in place is O(1) per merge. The defensive copy 

267 # that protected the out-of-order validation pass has been 

268 # moved into OutOfOrderTableProxy (its only consumer). 

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

270 current.append(k, v) 

271 

272 return self 

273 elif ( 

274 current_body_element[0] is not None 

275 and current_body_element[0].is_dotted() 

276 ): 

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

278 else: 

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

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

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

282 assert isinstance(current, Table) 

283 self._validate_table_candidate(current, item) 

284 elif not item.is_super_table(): 

285 raise KeyAlreadyPresent(key) 

286 elif isinstance(item, AoT): 

287 if not isinstance(current, AoT): 

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

289 raise KeyAlreadyPresent(key) 

290 

291 for table in item.body: 

292 current.append(table) 

293 

294 return self 

295 else: 

296 raise KeyAlreadyPresent(key) 

297 

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

299 if ( 

300 key is not None 

301 and self._body 

302 and not self._parsed 

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

304 ): 

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

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

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

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

309 last_index = self._get_last_index_before_table() 

310 

311 if last_index < len(self._body): 

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

313 if not ( 

314 isinstance(after_item, Whitespace) 

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

316 ): 

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

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

319 else: 

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

321 if not ( 

322 isinstance(previous_item, Whitespace) 

323 or ends_with_whitespace(previous_item) 

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

325 ): 

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

327 

328 self._raw_append(key, item) 

329 return self 

330 

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

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

333 if k is None: 

334 continue 

335 

336 if k in current.value._map: 

337 existing = current.value.item(k) 

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

339 raise KeyAlreadyPresent(k) 

340 if k.is_dotted(): 

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

342 continue 

343 

344 if not k.is_dotted(): 

345 continue 

346 

347 head = next(iter(k)) 

348 if head in current.value._map: 

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

350 

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

352 if key is not None and key in self._map: 

353 current_idx = self._map[key] 

354 if not isinstance(current_idx, tuple): 

355 current_idx = (current_idx,) 

356 

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

358 if not isinstance(current, Table): 

359 raise KeyAlreadyPresent(key) 

360 

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

362 elif key is not None: 

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

364 

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

366 if item.is_table() and key is not None: 

367 self._table_keys.append(key) 

368 

369 if key is not None: 

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

371 

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

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

374 assert key is not None 

375 index = self._map.get(key) 

376 if index is None: 

377 raise NonExistentKey(key) 

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

379 

380 if isinstance(index, tuple): 

381 index_list = list(index) 

382 index_list.remove(idx) 

383 if len(index_list) == 1: 

384 self._map[key] = index_list.pop() 

385 else: 

386 self._map[key] = tuple(index_list) 

387 else: 

388 dict.__delitem__(self, key.key) 

389 self._map.pop(key) 

390 

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

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

393 if not isinstance(key, Key): 

394 key = SingleKey(key) 

395 

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

397 if idx is None: 

398 raise NonExistentKey(key) 

399 

400 if isinstance(idx, tuple): 

401 for i in idx: 

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

403 else: 

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

405 

406 dict.__delitem__(self, key.key) 

407 

408 return self 

409 

410 def _insert_after( 

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

412 ) -> Container: 

413 if key is None: 

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

415 

416 if key not in self: 

417 raise NonExistentKey(key) 

418 

419 if not isinstance(key, Key): 

420 key = SingleKey(key) 

421 

422 if not isinstance(other_key, Key): 

423 other_key = SingleKey(other_key) 

424 

425 item = _item(item) 

426 

427 idx = self._map[key] 

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

429 if isinstance(idx, tuple): 

430 idx = max(idx) 

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

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

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

434 

435 # Increment indices after the current index 

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

437 if isinstance(v, tuple): 

438 new_indices = [] 

439 for v_ in v: 

440 if v_ > idx: 

441 v_ = v_ + 1 

442 

443 new_indices.append(v_) 

444 

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

446 elif v > idx: 

447 self._map[k] = v + 1 

448 

449 self._map[other_key] = idx + 1 

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

451 

452 if key is not None: 

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

454 

455 return self 

456 

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

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

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

460 

461 if not isinstance(key, Key): 

462 key = SingleKey(key) 

463 

464 item = _item(item) 

465 

466 if idx > 0: 

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

468 if not ( 

469 isinstance(previous_item, Whitespace) 

470 or ends_with_whitespace(previous_item) 

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

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

473 ): 

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

475 

476 # Increment indices after the current index 

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

478 if isinstance(v, tuple): 

479 new_indices = [] 

480 for v_ in v: 

481 if v_ >= idx: 

482 v_ = v_ + 1 

483 

484 new_indices.append(v_) 

485 

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

487 elif v >= idx: 

488 self._map[k] = v + 1 

489 

490 if key in self._map: 

491 current_idx = self._map[key] 

492 if not isinstance(current_idx, tuple): 

493 current_idx = (current_idx,) 

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

495 else: 

496 self._map[key] = idx 

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

498 

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

500 

501 return self 

502 

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

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

505 if not isinstance(key, Key): 

506 key = SingleKey(key) 

507 

508 idx = self._map.get(key) 

509 if idx is None: 

510 raise NonExistentKey(key) 

511 

512 if isinstance(idx, tuple): 

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

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

515 # from the parent container 

516 return OutOfOrderTableProxy(self, idx) 

517 

518 return self._body[idx][1] 

519 

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

521 """Get the last item.""" 

522 if self._body: 

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

524 return None 

525 

526 def as_string(self) -> str: 

527 """Render as TOML string.""" 

528 s = "" 

529 for k, v in self._body: 

530 if k is not None: 

531 if isinstance(v, Table): 

532 if ( 

533 s.strip(" ") 

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

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

536 ): 

537 s += "\n" 

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

539 elif isinstance(v, AoT): 

540 if ( 

541 s.strip(" ") 

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

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

544 ): 

545 s += "\n" 

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

547 else: 

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

549 else: 

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

551 

552 return s 

553 

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

555 cur = "" 

556 

557 if table.display_name is not None: 

558 _key = table.display_name 

559 else: 

560 _key = key.as_string() 

561 

562 if prefix is not None: 

563 _key = prefix + "." + _key 

564 

565 if ( 

566 not table.is_super_table() 

567 or ( 

568 any( 

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

570 for _, v in table.value.body 

571 ) 

572 and not key.is_dotted() 

573 ) 

574 or ( 

575 any( 

576 k is not None and k.is_dotted() 

577 for k, v in table.value.body 

578 if isinstance(v, Table) 

579 ) 

580 and not key.is_dotted() 

581 ) 

582 ): 

583 open_, close = "[", "]" 

584 if table.is_aot_element(): 

585 open_, close = "[[", "]]" 

586 

587 newline_in_table_trivia = ( 

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

589 ) 

590 cur += ( 

591 f"{table.trivia.indent}" 

592 f"{open_}" 

593 f"{decode(_key)}" 

594 f"{close}" 

595 f"{table.trivia.comment_ws}" 

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

597 f"{table.trivia.trail}" 

598 f"{newline_in_table_trivia}" 

599 ) 

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

601 cur += table.trivia.indent 

602 

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

604 if isinstance(v, Table): 

605 if ( 

606 cur.strip(" ") 

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

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

609 ): 

610 cur += "\n" 

611 assert k is not None 

612 if v.is_super_table(): 

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

614 # Dotted key inside table 

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

616 else: 

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

618 else: 

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

620 elif isinstance(v, AoT): 

621 if ( 

622 cur.strip(" ") 

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

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

625 ): 

626 cur += "\n" 

627 assert k is not None 

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

629 else: 

630 cur += self._render_simple_item( 

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

632 ) 

633 

634 return cur 

635 

636 def _render_aot(self, key: Key, aot: AoT, prefix: str | None = None) -> str: 

637 _key = key.as_string() 

638 if prefix is not None: 

639 _key = prefix + "." + _key 

640 

641 cur = "" 

642 _key = decode(_key) 

643 for table in aot.body: 

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

645 

646 return cur 

647 

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

649 cur = "" 

650 _key = prefix or "" 

651 open_, close = "[[", "]]" 

652 

653 cur += ( 

654 f"{table.trivia.indent}" 

655 f"{open_}" 

656 f"{decode(_key)}" 

657 f"{close}" 

658 f"{table.trivia.comment_ws}" 

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

660 f"{table.trivia.trail}" 

661 ) 

662 

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

664 if isinstance(v, Table): 

665 assert k is not None 

666 if v.is_super_table(): 

667 if k.is_dotted(): 

668 # Dotted key inside table 

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

670 else: 

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

672 else: 

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

674 elif isinstance(v, AoT): 

675 assert k is not None 

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

677 else: 

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

679 

680 return cur 

681 

682 def _render_simple_item( 

683 self, key: Key | None, item: Item, prefix: str | None = None 

684 ) -> str: 

685 if key is None: 

686 return item.as_string() 

687 

688 _key = key.as_string() 

689 if prefix is not None: 

690 _key = prefix + "." + _key 

691 

692 return ( 

693 f"{item.trivia.indent}" 

694 f"{decode(_key)}" 

695 f"{key.sep}" 

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

697 f"{item.trivia.comment_ws}" 

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

699 f"{item.trivia.trail}" 

700 ) 

701 

702 def __len__(self) -> int: 

703 return dict.__len__(self) 

704 

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

706 return iter(dict.keys(self)) 

707 

708 # Dictionary methods 

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

710 item = self.item(key) 

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

712 return item.value 

713 

714 return item 

715 

716 def __contains__(self, key: object) -> bool: 

717 # Native membership test. The inherited ``MutableMapping.__contains__`` 

718 # resolves the value via ``__getitem__``/``item()`` (and builds a 

719 # ``NonExistentKey`` on every absent key) only to discard it. Resolve the 

720 # key the same way ``item()`` does -- ``str`` becomes a ``SingleKey`` 

721 # (a non-str/non-``Key`` argument still raises ``TypeError``) -- then 

722 # probe ``_map`` directly. For an out-of-order table the proxy is still 

723 # built so its validation runs exactly as before. 

724 if not isinstance(key, Key): 

725 key = SingleKey(key) # type: ignore[arg-type] 

726 idx = self._map.get(key) 

727 if idx is None: 

728 return False 

729 if isinstance(idx, tuple): 

730 OutOfOrderTableProxy(self, idx) 

731 return True 

732 

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

734 if key in self: 

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

736 self._replace(old_key, key, value) 

737 else: 

738 self.append(key, value) 

739 

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

741 self.remove(key) 

742 

743 def setdefault(self, key: Key | str, default: Any = None) -> Any: 

744 if key not in self: 

745 self[key] = default 

746 return self[key] 

747 

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

749 if not isinstance(key, Key): 

750 key = SingleKey(key) 

751 

752 idx = self._map.get(key) 

753 if idx is None: 

754 raise NonExistentKey(key) 

755 

756 self._replace_at(idx, new_key, value) 

757 

758 def _replace_at( 

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

760 ) -> None: 

761 value = _item(value) 

762 

763 if isinstance(idx, tuple): 

764 for i in idx[1:]: 

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

766 

767 idx = idx[0] 

768 

769 k, v = self._body[idx] 

770 assert k is not None 

771 if not isinstance(new_key, Key): 

772 if ( 

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

774 or new_key != k.key 

775 ): 

776 new_key = SingleKey(new_key) 

777 else: # Inherit the sep of the old key 

778 new_key = k 

779 

780 del self._map[k] 

781 self._map[new_key] = idx 

782 if new_key != k: 

783 dict.__delitem__(self, k.key) 

784 

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

786 self.remove(k) 

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

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

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

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

791 self._insert_at(i, new_key, value) 

792 idx = i 

793 break 

794 else: 

795 idx = -1 

796 self.append(new_key, value) 

797 else: 

798 # the replaced table's slot lies in the table region, where a 

799 # plain value would be captured by the preceding table on 

800 # round-trip; append() puts it with the other root-level values 

801 idx = -1 

802 self.append(new_key, value) 

803 else: 

804 # Copying trivia 

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

806 value.trivia.indent = v.trivia.indent 

807 value.trivia.comment_ws = value.trivia.comment_ws or v.trivia.comment_ws 

808 value.trivia.comment = value.trivia.comment or v.trivia.comment 

809 value.trivia.trail = v.trivia.trail 

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

811 

812 if hasattr(value, "invalidate_display_name"): 

813 value.invalidate_display_name() 

814 

815 if isinstance(value, Table): 

816 # Insert a cosmetic new line for tables if: 

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

818 # - it is not the last item, or 

819 # - The table being replaced has a newline 

820 result = self._previous_item_with_index() 

821 assert result is not None 

822 last, _ = result 

823 idx = last if idx < 0 else idx 

824 has_ws = ends_with_whitespace(value) 

825 replace_has_ws = ( 

826 isinstance(v, Table) 

827 and v.value.body 

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

829 ) 

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

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

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

833 

834 assert isinstance(new_key, Key) 

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

836 

837 def __str__(self) -> str: 

838 return str(self.value) 

839 

840 def __repr__(self) -> str: 

841 return repr(self.value) 

842 

843 def __eq__(self, other: object) -> bool: 

844 if not isinstance(other, dict): 

845 return NotImplemented 

846 

847 return bool(_equal_with_nan(self.value, other)) 

848 

849 def _getstate(self, protocol: int) -> tuple[bool]: 

850 return (self._parsed,) 

851 

852 def __reduce__(self) -> tuple[type, tuple[bool], tuple[Any, ...]]: 

853 return self.__reduce_ex__(2) 

854 

855 def __reduce_ex__(self, protocol: int) -> tuple[type, tuple[bool], tuple[Any, ...]]: # type: ignore[override] 

856 return ( 

857 self.__class__, 

858 self._getstate(protocol), 

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

860 ) 

861 

862 def __setstate__(self, state: tuple[Any, ...]) -> None: 

863 self._map = state[0] 

864 self._body = state[1] 

865 self._parsed = state[2] 

866 self._table_keys = state[3] 

867 

868 for key, item in self._body: 

869 if key is not None: 

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

871 

872 def copy(self) -> Self: 

873 return copy.copy(self) 

874 

875 def __copy__(self) -> Self: 

876 c = self.__class__(self._parsed) 

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

878 dict.__setitem__(c, k, v) 

879 

880 c._body += self.body 

881 c._map.update(self._map) 

882 

883 return c 

884 

885 def _previous_item_with_index( 

886 self, idx: int | None = None, ignore: tuple[type, ...] = (Null,) 

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

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

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

890 idx = len(self._body) 

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

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

893 if not isinstance(v, ignore): 

894 return i, v 

895 return None 

896 

897 def _previous_item( 

898 self, idx: int | None = None, ignore: tuple[type, ...] = (Null,) 

899 ) -> Item | None: 

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

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

902 """ 

903 prev = self._previous_item_with_index(idx, ignore) 

904 return prev[-1] if prev else None 

905 

906 

907class OutOfOrderTableProxy(_CustomDict): # type: ignore[type-arg] 

908 @staticmethod 

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

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

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

912 # We deep-copy each value before appending: appending a super table 

913 # merges it in place into a matching one already in temp_container, and 

914 # those values are the live subtables of `container`. Without the copy 

915 # the merge would mutate the caller's tables (and corrupt a later 

916 # validation pass). The container merge itself is now copy-free for 

917 # speed, so this is where the isolation lives. 

918 temp_container = Container(True) 

919 for i in indices: 

920 _, item = container._body[i] 

921 

922 if isinstance(item, Table): 

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

924 temp_container.append(k, copy.deepcopy(v), validate=True) 

925 

926 temp_container._validate_out_of_order_table() 

927 

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

929 self._container = container 

930 self._internal_container = Container(True) 

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

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

933 

934 for i in indices: 

935 _, _item = self._container._body[i] 

936 

937 if isinstance(_item, Table): 

938 self._tables.append(_item) 

939 table_idx = len(self._tables) - 1 

940 for k, v in _item.value.body: 

941 self._internal_container._raw_append(k, v) 

942 key_indices = self._tables_map.setdefault(k, []) # type: ignore[arg-type] 

943 if table_idx not in key_indices: 

944 key_indices.append(table_idx) 

945 if k is not None: 

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

947 

948 self._internal_container._validate_out_of_order_table() 

949 

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

951 return self._internal_container.unwrap() 

952 

953 @property 

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

955 return self._internal_container.value 

956 

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

958 if key not in self._internal_container: 

959 raise NonExistentKey(key) 

960 

961 return self._internal_container[key] 

962 

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

964 from .items import item as _item_fn 

965 

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

967 return isinstance(_item_fn(it), (Table, AoT)) 

968 

969 _key: Key = key if isinstance(key, Key) else SingleKey(key) 

970 

971 if _key in self._tables_map: 

972 # Overwrite the first table and remove others 

973 map_indices = self._tables_map[_key] 

974 while len(map_indices) > 1: 

975 table = self._tables[map_indices.pop()] 

976 self._remove_table(table) 

977 old_value = self._tables[map_indices[0]][key] 

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

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

980 del self._tables[map_indices[0]][key] 

981 del self._tables_map[_key] 

982 self[key] = value 

983 return 

984 self._tables[map_indices[0]][key] = value 

985 elif self._tables: 

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

987 for table in self._tables: 

988 # find the first table that allows plain values 

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

990 table[key] = value 

991 break 

992 else: 

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

994 else: 

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

996 else: 

997 self._container[key] = value 

998 

999 self._internal_container[key] = value 

1000 if key is not None: 

1001 dict.__setitem__(self, key, value) 

1002 

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

1004 """Remove table from the parent container""" 

1005 self._tables.remove(table) 

1006 for idx, body_item in enumerate(self._container._body): 

1007 if body_item[1] is table: 

1008 self._container._remove_at(idx) 

1009 break 

1010 

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

1012 _key: Key = key if isinstance(key, Key) else SingleKey(key) 

1013 if _key not in self._tables_map: 

1014 raise NonExistentKey(key) 

1015 

1016 for i in reversed(self._tables_map[_key]): 

1017 table = self._tables[i] 

1018 del table[key] 

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

1020 self._remove_table(table) 

1021 

1022 del self._tables_map[_key] 

1023 del self._internal_container[key] 

1024 if key is not None: 

1025 dict.__delitem__(self, key) 

1026 

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

1028 return iter(dict.keys(self)) 

1029 

1030 def __len__(self) -> int: 

1031 return dict.__len__(self) 

1032 

1033 def setdefault(self, key: Key | str, default: Any = None) -> Any: 

1034 if key not in self: 

1035 self[key] = default 

1036 return self[key] 

1037 

1038 

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

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

1041 ending with a ``Whitespace``. 

1042 """ 

1043 return ( 

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

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

1046 

1047 

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

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

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

1051 return False 

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

1053 

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

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

1056 return False 

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

1058 

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

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

1061 return True 

1062 

1063 return bool(left == right)