Coverage for src/debputy/lsp/vendoring/_deb822_repro/parsing.py: 59%

1464 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1# -*- coding: utf-8 -*- vim: fileencoding=utf-8 : 

2 

3import collections.abc 

4import contextlib 

5import sys 

6import textwrap 

7import weakref 

8from abc import ABC 

9from types import TracebackType 

10from weakref import ReferenceType 

11 

12from ._util import ( 

13 combine_into_replacement, 

14 BufferingIterator, 

15 len_check_iterator, 

16) 

17from .formatter import ( 

18 FormatterContentToken, 

19 one_value_per_line_trailing_separator, 

20 format_field, 

21) 

22from .locatable import Locatable, START_POSITION, Position, Range 

23from .tokens import ( 

24 Deb822Token, 

25 Deb822ValueToken, 

26 Deb822SemanticallySignificantWhiteSpace, 

27 Deb822SpaceSeparatorToken, 

28 Deb822CommentToken, 

29 Deb822WhitespaceToken, 

30 Deb822ValueContinuationToken, 

31 Deb822NewlineAfterValueToken, 

32 Deb822CommaToken, 

33 Deb822FieldNameToken, 

34 Deb822FieldSeparatorToken, 

35 Deb822ErrorToken, 

36 tokenize_deb822_file, 

37 comma_split_tokenizer, 

38 whitespace_split_tokenizer, 

39) 

40from .types import AmbiguousDeb822FieldKeyError, SyntaxOrParseError 

41from debian._util import ( 

42 resolve_ref, 

43 LinkedList, 

44 LinkedListNode, 

45 OrderedSet, 

46 _strI, 

47 default_field_sort_key, 

48) 

49 

50try: 

51 from typing import ( 

52 Iterable, 

53 Iterator, 

54 List, 

55 Union, 

56 Dict, 

57 Optional, 

58 Callable, 

59 Any, 

60 Generic, 

61 Type, 

62 Tuple, 

63 IO, 

64 cast, 

65 overload, 

66 Mapping, 

67 TYPE_CHECKING, 

68 Sequence, 

69 ) 

70 from debian._util import T 

71 

72 # for some reason, pylint does not see that Commentish is used in typing 

73 from .types import ( # pylint: disable=unused-import 

74 ST, 

75 VE, 

76 TE, 

77 ParagraphKey, 

78 TokenOrElement, 

79 Commentish, 

80 ParagraphKeyBase, 

81 FormatterCallback, 

82 ) 

83 

84 if TYPE_CHECKING: 

85 StreamingValueParser = Callable[ 

86 [Deb822Token, BufferingIterator[Deb822Token]], VE 

87 ] 

88 StrToValueParser = Callable[[str], Iterable[Union["Deb822Token", VE]]] 

89 KVPNode = LinkedListNode["Deb822KeyValuePairElement"] 

90 else: 

91 StreamingValueParser = None 

92 StrToValueParser = None 

93 KVPNode = None 

94except ImportError: 

95 if not TYPE_CHECKING: 

96 # pylint: disable=unnecessary-lambda-assignment 

97 cast = lambda t, v: v 

98 overload = lambda f: None 

99 

100 

101class ValueReference(Generic[TE]): 

102 """Reference to a value inside a Deb822 paragraph 

103 

104 This is useful for cases where want to modify values "in-place" or maybe 

105 conditionally remove a value after looking at it. 

106 

107 ValueReferences can be invalidated by various changes or actions performed 

108 to the underlying provider of the value reference. As an example, sorting 

109 a list of values will generally invalidate all ValueReferences related to 

110 that list. 

111 

112 The ValueReference will raise validity issues where it detects them but most 

113 of the time it will not notice. As a means to this end, the ValueReference 

114 will *not* keep a strong reference to the underlying value. This enables it 

115 to detect when the container goes out of scope. However, keep in mind that 

116 the timeliness of garbage collection is implementation defined (e.g., pypy 

117 does not use ref-counting). 

118 """ 

119 

120 __slots__ = ( 

121 "_node", 

122 "_render", 

123 "_value_factory", 

124 "_removal_handler", 

125 "_mutation_notifier", 

126 ) 

127 

128 def __init__( 

129 self, 

130 node, # type: LinkedListNode[TE] 

131 render, # type: Callable[[TE], str] 

132 value_factory, # type: Callable[[str], TE] 

133 removal_handler, # type: Callable[[LinkedListNode[TokenOrElement]], None] 

134 mutation_notifier, # type: Optional[Callable[[], None]] 

135 ): 

136 self._node = weakref.ref( 

137 node 

138 ) # type: Optional[ReferenceType[LinkedListNode[TE]]] 

139 self._render = render 

140 self._value_factory = value_factory 

141 self._removal_handler = removal_handler 

142 self._mutation_notifier = mutation_notifier 

143 

144 def _resolve_node(self): 

145 # type: () -> LinkedListNode[TE] 

146 # NB: We check whether the "ref" itself is None (instead of the ref resolving to None) 

147 # This enables us to tell the difference between "known removal" vs. "garbage collected" 

148 if self._node is None: 148 ↛ 149line 148 didn't jump to line 149, because the condition on line 148 was never true

149 raise RuntimeError("Cannot use ValueReference after remove()") 

150 node = self._node() 

151 if node is None: 151 ↛ 152line 151 didn't jump to line 152, because the condition on line 151 was never true

152 raise RuntimeError("ValueReference is invalid (garbage collected)") 

153 return node 

154 

155 @property 

156 def value(self): 

157 # type: () -> str 

158 """Resolve the reference into a str""" 

159 return self._render(self._resolve_node().value) 

160 

161 @value.setter 

162 def value(self, new_value): 

163 # type: (str) -> None 

164 """Update the reference value 

165 

166 Updating the value via this method will *not* invalidate the reference (or other 

167 references to the same container). 

168 

169 This can raise an exception if the new value does not follow the requirements 

170 for the referenced values. As an example, values in whitespace separated 

171 lists cannot contain spaces and would trigger an exception. 

172 """ 

173 self._resolve_node().value = self._value_factory(new_value) 

174 if self._mutation_notifier is not None: 

175 self._mutation_notifier() 

176 

177 @property 

178 def locatable(self): 

179 # type: () -> Locatable 

180 """Reference to a locatable that can be used to determine where this value is""" 

181 return self._resolve_node().value 

182 

183 def remove(self): 

184 # type: () -> None 

185 """Remove the underlying value 

186 

187 This will invalidate the ValueReference (and any other ValueReferences pointing 

188 to that exact value). The validity of other ValueReferences to that container 

189 remains unaffected. 

190 """ 

191 self._removal_handler( 

192 cast("LinkedListNode[TokenOrElement]", self._resolve_node()) 

193 ) 

194 self._node = None 

195 

196 

197if sys.version_info >= (3, 9) or TYPE_CHECKING: 197 ↛ 204line 197 didn't jump to line 204, because the condition on line 197 was never false

198 _Deb822ParsedTokenList_ContextManager = contextlib.AbstractContextManager[T] 

199else: 

200 # Python 3.5 - 3.8 compat - we are not allowed to subscript the abc.Iterator 

201 # - use this little hack to work around it 

202 # Note that Python 3.5 is so old that it does not have AbstractContextManager, 

203 # so we re-implement it here. 

204 class _Deb822ParsedTokenList_ContextManager(Generic[T]): 

205 

206 def __enter__(self): 

207 return self 

208 

209 def __exit__(self, exc_type, exc_val, exc_tb): 

210 return None 

211 

212 

213class Deb822ParsedTokenList( 

214 Generic[VE, ST], 

215 _Deb822ParsedTokenList_ContextManager["Deb822ParsedTokenList[VE, ST]"], 

216): 

217 

218 def __init__( 

219 self, 

220 kvpair_element, # type: 'Deb822KeyValuePairElement' 

221 interpreted_value_element, # type: Deb822InterpretationProxyElement 

222 vtype, # type: Type[VE] 

223 stype, # type: Type[ST] 

224 str2value_parser, # type: StrToValueParser[VE] 

225 default_separator_factory, # type: Callable[[], ST] 

226 render, # type: Callable[[VE], str] 

227 ): 

228 # type: (...) -> None 

229 self._kvpair_element = kvpair_element 

230 self._proxy_element = interpreted_value_element 

231 self._token_list = LinkedList(interpreted_value_element.parts) 

232 self._vtype = vtype 

233 self._stype = stype 

234 self._str2value_parser = str2value_parser 

235 self._default_separator_factory = default_separator_factory 

236 self._value_factory = _parser_to_value_factory(str2value_parser, vtype) 

237 self._render = render 

238 self._format_preserve_original_formatting = True 

239 self._formatter = ( 

240 one_value_per_line_trailing_separator 

241 ) # type: FormatterCallback 

242 self._changed = False 

243 self.__continuation_line_char = None # type: Optional[str] 

244 assert self._token_list 

245 last_token = self._token_list.tail 

246 

247 if last_token is not None and isinstance( 247 ↛ exitline 247 didn't return from function '__init__', because the condition on line 247 was never false

248 last_token, Deb822NewlineAfterValueToken 

249 ): 

250 # We always remove the last newline (if present), because then 

251 # adding values will happen after the last value rather than on 

252 # a new line by default. 

253 # 

254 # On write, we always ensure the value ends on a newline (even 

255 # if it did not before). This is simpler and should be a 

256 # non-issue in practise. 

257 self._token_list.pop() 

258 

259 def __iter__(self): 

260 # type: () -> Iterator[str] 

261 yield from (self._render(v) for v in self.value_parts) 

262 

263 def __bool__(self): 

264 # type: () -> bool 

265 return next(iter(self), None) is not None 

266 

267 def __exit__( 

268 self, 

269 exc_type, # type: Optional[Type[BaseException]] 

270 exc_val, # type: Optional[BaseException] 

271 exc_tb, # type: Optional[TracebackType] 

272 ): 

273 # type: (...) -> Optional[bool] 

274 if exc_type is None and self._changed: 274 ↛ 276line 274 didn't jump to line 276, because the condition on line 274 was never false

275 self._update_field() 

276 return super().__exit__(exc_type, exc_val, exc_tb) 

277 

278 @property 

279 def value_parts(self): 

280 # type: () -> Iterator[VE] 

281 yield from (v for v in self._token_list if isinstance(v, self._vtype)) 

282 

283 def _mark_changed(self): 

284 # type: () -> None 

285 self._changed = True 

286 

287 def iter_value_references(self): 

288 # type: () -> Iterator[ValueReference[VE]] 

289 """Iterate over all values in the list (as ValueReferences) 

290 

291 This is useful for doing inplace modification of the values or even 

292 streaming removal of field values. It is in general also more 

293 efficient when more than one value is updated or removed. 

294 """ 

295 yield from ( 

296 ValueReference( 

297 cast("LinkedListNode[VE]", n), 

298 self._render, 

299 self._value_factory, 

300 self._remove_node, 

301 self._mark_changed, 

302 ) 

303 for n in self._token_list.iter_nodes() 

304 if isinstance(n.value, self._vtype) 

305 ) 

306 

307 def append_separator(self, space_after_separator=True): 

308 # type: (bool) -> None 

309 

310 separator_token = self._default_separator_factory() 

311 if separator_token.is_whitespace: 311 ↛ 314line 311 didn't jump to line 314, because the condition on line 311 was never false

312 space_after_separator = False 

313 

314 self._changed = True 

315 self._append_continuation_line_token_if_necessary() 

316 self._token_list.append(separator_token) 

317 

318 if space_after_separator and not separator_token.is_whitespace: 318 ↛ 319line 318 didn't jump to line 319, because the condition on line 318 was never true

319 self._token_list.append(Deb822WhitespaceToken(" ")) 

320 

321 def replace(self, orig_value, new_value): 

322 # type: (str, str) -> None 

323 """Replace the first instance of a value with another 

324 

325 This method will *not* affect the validity of ValueReferences. 

326 """ 

327 vtype = self._vtype 

328 for node in self._token_list.iter_nodes(): 328 ↛ 334line 328 didn't jump to line 334, because the loop on line 328 didn't complete

329 if isinstance(node.value, vtype) and self._render(node.value) == orig_value: 

330 node.value = self._value_factory(new_value) 

331 self._changed = True 

332 break 

333 else: 

334 raise ValueError("list.replace(x, y): x not in list") 

335 

336 def remove(self, value): 

337 # type: (str) -> None 

338 """Remove the first instance of a value 

339 

340 Removal will invalidate ValueReferences to the value being removed. 

341 ValueReferences to other values will be unaffected. 

342 """ 

343 vtype = self._vtype 

344 for node in self._token_list.iter_nodes(): 

345 if isinstance(node.value, vtype) and self._render(node.value) == value: 

346 node_to_remove = node 

347 break 

348 else: 

349 raise ValueError("list.remove(x): x not in list") 

350 

351 return self._remove_node(node_to_remove) 

352 

353 def _remove_node(self, node_to_remove): 

354 # type: (LinkedListNode[TokenOrElement]) -> None 

355 vtype = self._vtype 

356 self._changed = True 

357 

358 # We naively want to remove the node and every thing to the left of it 

359 # until the previous value. That is the basic idea for now (ignoring 

360 # special-cases for now). 

361 # 

362 # Example: 

363 # 

364 # """ 

365 # Multiline-Keywords: bar[ 

366 # # Comment about foo 

367 # foo] 

368 # baz 

369 # Keywords: bar[ foo] baz 

370 # Comma-List: bar[, foo], baz, 

371 # Multiline-Comma-List: bar[, 

372 # # Comment about foo 

373 # foo], 

374 # baz, 

375 # """ 

376 # 

377 # Assuming we want to remove "foo" for the lists, the []-markers 

378 # show what we aim to remove. This has the nice side-effect of 

379 # preserving whether nor not the value has a trailing separator. 

380 # Note that we do *not* attempt to repair missing separators but 

381 # it may fix duplicated separators by "accident". 

382 # 

383 # Now, there are two special cases to be aware of, where this approach 

384 # has short comings: 

385 # 

386 # 1) If foo is the only value (in which case, "delete everything" 

387 # is the only option). 

388 # 2) If foo is the first value 

389 # 3) If foo is not the only value on the line and we see a comment 

390 # inside the deletion range. 

391 # 

392 # For 2) + 3), we attempt to flip and range to delete and every 

393 # thing after it (up to but exclusion "baz") instead. This 

394 # definitely fixes 3), but 2) has yet another corner case, namely: 

395 # 

396 # """ 

397 # Multiline-Comma-List: foo, 

398 # # Remark about bar 

399 # bar, 

400 # Another-Case: foo 

401 # # Remark, also we use leading separator 

402 # , bar 

403 # """ 

404 # 

405 # The options include: 

406 # 

407 # A) Discard the comment - brain-dead simple 

408 # B) Hoist the comment up to a field comment, but then what if the 

409 # field already has a comment? 

410 # C) Clear the first value line leaving just the newline and 

411 # replace the separator before "bar" (if present) with a space. 

412 # (leaving you with the value of the form "\n# ...\n bar") 

413 # 

414 

415 first_value_on_lhs = None # type: Optional[LinkedListNode[TokenOrElement]] 

416 first_value_on_rhs = None # type: Optional[LinkedListNode[TokenOrElement]] 

417 comment_before_previous_value = False 

418 comment_before_next_value = False 

419 for past_node in node_to_remove.iter_previous(skip_current=True): 

420 past_token = past_node.value 

421 if isinstance(past_token, Deb822Token) and past_token.is_comment: 

422 comment_before_previous_value = True 

423 continue 

424 if isinstance(past_token, vtype): 

425 first_value_on_lhs = past_node 

426 break 

427 

428 for future_node in node_to_remove.iter_next(skip_current=True): 

429 future_token = future_node.value 

430 if isinstance(future_token, Deb822Token) and future_token.is_comment: 

431 comment_before_next_value = True 

432 continue 

433 if isinstance(future_token, vtype): 

434 first_value_on_rhs = future_node 

435 break 

436 

437 if first_value_on_rhs is None and first_value_on_lhs is None: 

438 # This was the last value, just remove everything. 

439 self._token_list.clear() 

440 return 

441 

442 if first_value_on_lhs is not None and not comment_before_previous_value: 

443 # Delete left 

444 delete_lhs_of_node = True 

445 elif first_value_on_rhs is not None and not comment_before_next_value: 

446 # Delete right 

447 delete_lhs_of_node = False 

448 else: 

449 # There is a comment on either side (or no value on one and a 

450 # comment and the other). Keep it simple, we just delete to 

451 # one side (preferring deleting to left if possible). 

452 delete_lhs_of_node = first_value_on_lhs is not None 

453 

454 if delete_lhs_of_node: 

455 first_remain_lhs = first_value_on_lhs 

456 first_remain_rhs = node_to_remove.next_node 

457 else: 

458 first_remain_lhs = node_to_remove.previous_node 

459 first_remain_rhs = first_value_on_rhs 

460 

461 # Actual deletion - with some manual labour to update HEAD/TAIL of 

462 # the list in case we do a "delete everything left/right this node". 

463 if first_remain_lhs is None: 

464 self._token_list.head_node = first_remain_rhs 

465 if first_remain_rhs is None: 

466 self._token_list.tail_node = first_remain_lhs 

467 LinkedListNode.link_nodes(first_remain_lhs, first_remain_rhs) 

468 

469 def append(self, value): 

470 # type: (str) -> None 

471 vt = self._value_factory(value) 

472 self.append_value(vt) 

473 

474 def append_value(self, vt): 

475 # type: (VE) -> None 

476 value_parts = self._token_list 

477 if value_parts: 

478 needs_separator = False 

479 stype = self._stype 

480 vtype = self._vtype 

481 for t in reversed(value_parts): 481 ↛ 488line 481 didn't jump to line 488, because the loop on line 481 didn't complete

482 if isinstance(t, vtype): 

483 needs_separator = True 

484 break 

485 if isinstance(t, stype): 

486 break 

487 

488 if needs_separator: 

489 self.append_separator() 

490 else: 

491 # Looks nicer if there is a space before the very first value 

492 self._token_list.append(Deb822WhitespaceToken(" ")) 

493 self._append_continuation_line_token_if_necessary() 

494 self._changed = True 

495 value_parts.append(vt) 

496 

497 def _previous_is_newline(self): 

498 # type: () -> bool 

499 tail = self._token_list.tail 

500 return tail is not None and tail.convert_to_text().endswith("\n") 

501 

502 def append_newline(self): 

503 # type: () -> None 

504 if self._previous_is_newline(): 504 ↛ 505line 504 didn't jump to line 505, because the condition on line 504 was never true

505 raise ValueError( 

506 "Cannot add a newline after a token that ends on a newline" 

507 ) 

508 self._token_list.append(Deb822NewlineAfterValueToken()) 

509 

510 def append_comment(self, comment_text): 

511 # type: (str) -> None 

512 tail = self._token_list.tail 

513 if tail is None or not tail.convert_to_text().endswith("\n"): 

514 self.append_newline() 

515 comment_token = Deb822CommentToken(_format_comment(comment_text)) 

516 self._token_list.append(comment_token) 

517 

518 @property 

519 def _continuation_line_char(self): 

520 # type: () -> str 

521 char = self.__continuation_line_char 

522 if char is None: 

523 # Use ' ' by default but match the existing field if possible. 

524 char = " " 

525 for token in self._token_list: 

526 if isinstance(token, Deb822ValueContinuationToken): 

527 char = token.text 

528 break 

529 self.__continuation_line_char = char 

530 return char 

531 

532 def _append_continuation_line_token_if_necessary(self): 

533 # type: () -> None 

534 tail = self._token_list.tail 

535 if tail is not None and tail.convert_to_text().endswith("\n"): 535 ↛ 536line 535 didn't jump to line 536, because the condition on line 535 was never true

536 self._token_list.append( 

537 Deb822ValueContinuationToken(self._continuation_line_char) 

538 ) 

539 

540 def reformat_when_finished(self): 

541 # type: () -> None 

542 self._enable_reformatting() 

543 self._changed = True 

544 

545 def _enable_reformatting(self): 

546 # type: () -> None 

547 self._format_preserve_original_formatting = False 

548 

549 def no_reformatting_when_finished(self): 

550 # type: () -> None 

551 self._format_preserve_original_formatting = True 

552 

553 def value_formatter( 

554 self, 

555 formatter, # type: FormatterCallback 

556 force_reformat=False, # type: bool 

557 ): 

558 # type: (...) -> None 

559 """Use a custom formatter when formatting the value 

560 

561 :param formatter: A formatter (see debian._deb822_repro.formatter.format_field 

562 for details) 

563 :param force_reformat: If True, always reformat the field even if there are 

564 no (other) changes performed. By default, fields are only reformatted if 

565 they are changed. 

566 """ 

567 self._formatter = formatter 

568 self._format_preserve_original_formatting = False 

569 if force_reformat: 

570 self._changed = True 

571 

572 def clear(self): 

573 # type: () -> None 

574 """Like list.clear() - removes all content (including comments and spaces)""" 

575 if self._token_list: 

576 self._changed = True 

577 self._token_list.clear() 

578 

579 def _iter_content_as_tokens(self): 

580 # type: () -> Iterable[Deb822Token] 

581 for te in self._token_list: 

582 if isinstance(te, Deb822Element): 

583 yield from te.iter_tokens() 

584 else: 

585 yield te 

586 

587 def _generate_reformatted_field_content(self): 

588 # type: () -> str 

589 separator_token = self._default_separator_factory() 

590 vtype = self._vtype 

591 stype = self._stype 

592 token_list = self._token_list 

593 

594 def _token_iter(): 

595 # type: () -> Iterator[FormatterContentToken] 

596 text = "" # type: str 

597 for te in token_list: 

598 if isinstance(te, Deb822Token): 

599 if te.is_comment: 

600 yield FormatterContentToken.comment_token(te.text) 

601 elif isinstance(te, stype): 

602 text = te.text 

603 yield FormatterContentToken.separator_token(text) 

604 else: 

605 assert isinstance(te, vtype) 

606 text = te.convert_to_text() 

607 yield FormatterContentToken.value_token(text) 

608 

609 return format_field( 

610 self._formatter, 

611 self._kvpair_element.field_name, 

612 FormatterContentToken.separator_token(separator_token.text), 

613 _token_iter(), 

614 ) 

615 

616 def _generate_field_content(self): 

617 # type: () -> str 

618 return "".join(t.text for t in self._iter_content_as_tokens()) 

619 

620 def _update_field(self): 

621 # type: () -> None 

622 kvpair_element = self._kvpair_element 

623 field_name = kvpair_element.field_name 

624 token_list = self._token_list 

625 tail = token_list.tail 

626 had_tokens = False 

627 

628 for t in self._iter_content_as_tokens(): 628 ↛ 633line 628 didn't jump to line 633, because the loop on line 628 didn't complete

629 had_tokens = True 

630 if not t.is_comment and not t.is_whitespace: 

631 break 

632 else: 

633 if had_tokens: 

634 raise ValueError( 

635 "Field must be completely empty or have content " 

636 "(i.e. non-whitespace and non-comments)" 

637 ) 

638 if tail is not None: 638 ↛ 656line 638 didn't jump to line 656, because the condition on line 638 was never false

639 if isinstance(tail, Deb822Token) and tail.is_comment: 639 ↛ 640line 639 didn't jump to line 640, because the condition on line 639 was never true

640 raise ValueError("Fields must not end on a comment") 

641 if not tail.convert_to_text().endswith("\n"): 641 ↛ 645line 641 didn't jump to line 645, because the condition on line 641 was never false

642 # Always end on a newline 

643 self.append_newline() 

644 

645 if self._format_preserve_original_formatting: 

646 value_text = self._generate_field_content() 

647 text = ":".join((field_name, value_text)) 

648 else: 

649 text = self._generate_reformatted_field_content() 

650 

651 new_content = text.splitlines(keepends=True) 

652 else: 

653 # Special-case for the empty list which will be mapped to 

654 # an empty field. Always end on a newline (avoids errors 

655 # if there is a field after this) 

656 new_content = [field_name + ":\n"] 

657 

658 # As absurd as it might seem, it is easier to just use the parser to 

659 # construct the AST correctly 

660 deb822_file = parse_deb822_file(iter(new_content)) 

661 error_token = deb822_file.find_first_error_element() 

662 if error_token: 662 ↛ 664line 662 didn't jump to line 664, because the condition on line 662 was never true

663 # _print_ast(deb822_file) 

664 raise ValueError("Syntax error in new field value for " + field_name) 

665 paragraph = next(iter(deb822_file)) 

666 assert isinstance(paragraph, Deb822NoDuplicateFieldsParagraphElement) 

667 new_kvpair_element = paragraph.get_kvpair_element(field_name) 

668 assert new_kvpair_element is not None 

669 kvpair_element.value_element = new_kvpair_element.value_element 

670 self._changed = False 

671 

672 def sort_elements( 

673 self, 

674 *, 

675 key=None, # type: Optional[Callable[[VE], Any]] 

676 reverse=False, # type: bool 

677 ): 

678 # type: (...) -> None 

679 """Sort the elements (abstract values) in this list. 

680 

681 This method will sort the logical values of the list. It will 

682 attempt to preserve comments associated with a given value where 

683 possible. Whether space and separators are preserved depends on 

684 the contents of the field as well as the formatting settings. 

685 

686 Sorting (without reformatting) is likely to leave you with "awkward" 

687 whitespace. Therefore, you almost always want to apply reformatting 

688 such as the reformat_when_finished() method. 

689 

690 Sorting will invalidate all ValueReferences. 

691 """ 

692 comment_start_node = None 

693 vtype = self._vtype 

694 stype = self._stype 

695 

696 def key_func(x): 

697 # type: (Tuple[VE, List[TokenOrElement]]) -> Any 

698 if key: 698 ↛ 699line 698 didn't jump to line 699, because the condition on line 698 was never true

699 return key(x[0]) 

700 return x[0].convert_to_text() 

701 

702 parts = [] 

703 

704 for node in self._token_list.iter_nodes(): 

705 value = node.value 

706 if isinstance(value, Deb822Token) and value.is_comment: 

707 if comment_start_node is None: 707 ↛ 709line 707 didn't jump to line 709, because the condition on line 707 was never false

708 comment_start_node = node 

709 continue 

710 

711 if isinstance(value, vtype): 

712 comments = [] 

713 if comment_start_node is not None: 

714 for keep_node in comment_start_node.iter_next(skip_current=False): 714 ↛ 718line 714 didn't jump to line 718, because the loop on line 714 didn't complete

715 if keep_node is node: 

716 break 

717 comments.append(keep_node.value) 

718 parts.append((value, comments)) 

719 comment_start_node = None 

720 

721 parts.sort(key=key_func, reverse=reverse) 

722 

723 self._changed = True 

724 self._token_list.clear() 

725 first_value = True 

726 

727 separator_is_space = self._default_separator_factory().is_whitespace 

728 

729 for value, comments in parts: 

730 if first_value: 

731 first_value = False 

732 if comments: 732 ↛ 735line 732 didn't jump to line 735, because the condition on line 732 was never true

733 # While unlikely, there could be a separator between the comments. 

734 # It would be in the way and we remove it. 

735 comments = [x for x in comments if not isinstance(x, stype)] 

736 # Comments cannot start the field, so inject a newline to 

737 # work around that 

738 self.append_newline() 

739 else: 

740 if not separator_is_space and not any( 740 ↛ exit,   740 ↛ 7472 missed branches: 1) line 740 didn't run the generator expression on line 740, 2) line 740 didn't jump to line 747, because the condition on line 740 was never true

741 isinstance(x, stype) for x in comments 

742 ): 

743 # While unlikely, you can hide a comma between two comments and expect 

744 # us to preserve it. However, the more common case is that the separator 

745 # appeared before the comments and was thus omitted (leaving us to re-add 

746 # it here). 

747 self.append_separator(space_after_separator=False) 

748 if comments: 

749 self.append_newline() 

750 else: 

751 self._token_list.append(Deb822WhitespaceToken(" ")) 

752 

753 self._token_list.extend(comments) 

754 self.append_value(value) 

755 

756 def sort( 

757 self, 

758 *, 

759 key=None, # type: Optional[Callable[[str], Any]] 

760 **kwargs, # type: Any 

761 ): 

762 # type: (...) -> None 

763 """Sort the values (rendered as str) in this list. 

764 

765 This method will sort the logical values of the list. It will 

766 attempt to preserve comments associated with a given value where 

767 possible. Whether space and separators are preserved depends on 

768 the contents of the field as well as the formatting settings. 

769 

770 Sorting (without reformatting) is likely to leave you with "awkward" 

771 whitespace. Therefore, you almost always want to apply reformatting 

772 such as the reformat_when_finished() method. 

773 

774 Sorting will invalidate all ValueReferences. 

775 """ 

776 if key is not None: 776 ↛ 777line 776 didn't jump to line 777, because the condition on line 776 was never true

777 render = self._render 

778 kwargs["key"] = lambda vt: key(render(vt)) 

779 self.sort_elements(**kwargs) 

780 

781 

782class Interpretation(Generic[T]): 

783 

784 def interpret( 

785 self, 

786 kvpair_element, # type: Deb822KeyValuePairElement 

787 discard_comments_on_read=True, # type: bool 

788 ): 

789 # type: (...) -> T 

790 raise NotImplementedError # pragma: no cover 

791 

792 

793class GenericContentBasedInterpretation(Interpretation[T], Generic[T, VE]): 

794 

795 def __init__( 

796 self, 

797 tokenizer, # type: Callable[[str], Iterable['Deb822Token']] 

798 value_parser, # type: StreamingValueParser[VE] 

799 ): 

800 # type: (...) -> None 

801 super().__init__() 

802 self._tokenizer = tokenizer 

803 self._value_parser = value_parser 

804 

805 def _high_level_interpretation( 

806 self, 

807 kvpair_element, # type: Deb822KeyValuePairElement 

808 proxy_element, # type: Deb822InterpretationProxyElement 

809 discard_comments_on_read=True, # type: bool 

810 ): 

811 # type: (...) -> T 

812 raise NotImplementedError # pragma: no cover 

813 

814 def _parse_stream( 

815 self, buffered_iterator # type: BufferingIterator[Deb822Token] 

816 ): 

817 # type: (...) -> Iterable[Union[Deb822Token, VE]] 

818 

819 value_parser = self._value_parser 

820 for token in buffered_iterator: 

821 if isinstance(token, Deb822ValueToken): 

822 yield value_parser(token, buffered_iterator) 

823 else: 

824 yield token 

825 

826 def _parse_kvpair( 

827 self, kvpair # type: Deb822KeyValuePairElement 

828 ): 

829 # type: (...) -> Deb822InterpretationProxyElement 

830 value_element = kvpair.value_element 

831 content = value_element.convert_to_text() 

832 token_list = [] # type: List['TokenOrElement'] 

833 token_list.extend(self._parse_str(content)) 

834 return Deb822InterpretationProxyElement(value_element, token_list) 

835 

836 def _parse_str(self, content): 

837 # type: (str) -> Iterable[Union[Deb822Token, VE]] 

838 content_len = len(content) 

839 biter = BufferingIterator( 

840 len_check_iterator( 

841 content, 

842 self._tokenizer(content), 

843 content_len=content_len, 

844 ) 

845 ) 

846 yield from len_check_iterator( 

847 content, 

848 self._parse_stream(biter), 

849 content_len=content_len, 

850 ) 

851 

852 def interpret( 

853 self, 

854 kvpair_element, # type: Deb822KeyValuePairElement 

855 discard_comments_on_read=True, # type: bool 

856 ): 

857 # type: (...) -> T 

858 proxy_element = self._parse_kvpair(kvpair_element) 

859 return self._high_level_interpretation( 

860 kvpair_element, 

861 proxy_element, 

862 discard_comments_on_read=discard_comments_on_read, 

863 ) 

864 

865 

866def _parser_to_value_factory( 

867 parser, # type: StrToValueParser[VE] 

868 vtype, # type: Type[VE] 

869): 

870 # type: (...) -> Callable[[str], VE] 

871 def _value_factory(v): 

872 # type: (str) -> VE 

873 if v == "": 873 ↛ 874line 873 didn't jump to line 874, because the condition on line 873 was never true

874 raise ValueError("The empty string is not a value") 

875 token_iter = iter(parser(v)) 

876 t1 = next(token_iter, None) # type: Optional[Union[TokenOrElement]] 

877 t2 = next(token_iter, None) 

878 assert t1 is not None, ( 

879 'Bad parser - it returned None (or no TE) for "' + v + '"' 

880 ) 

881 if t2 is not None: 881 ↛ 882line 881 didn't jump to line 882, because the condition on line 881 was never true

882 msg = textwrap.dedent( 

883 """\ 

884 The input "{v}" should have been exactly one element, but the parser provided at 

885 least two. This can happen with unnecessary leading/trailing whitespace 

886 or including commas the value for a comma list. 

887 """ 

888 ).format(v=v) 

889 raise ValueError(msg) 

890 if not isinstance(t1, vtype): 890 ↛ 891line 890 didn't jump to line 891, because the condition on line 890 was never true

891 if isinstance(t1, Deb822Token) and (t1.is_comment or t1.is_whitespace): 

892 raise ValueError( 

893 'The input "{v}" is whitespace or a comment: Expected a value' 

894 ) 

895 msg = ( 

896 'The input "{v}" should have produced a element of type {vtype_name}, but' 

897 " instead it produced {t1}" 

898 ) 

899 raise ValueError(msg.format(v=v, vtype_name=vtype.__name__, t1=t1)) 

900 

901 assert len(t1.convert_to_text()) == len(v), ( 

902 "Bad tokenizer - the token did not cover the input text" 

903 " exactly ({t1_len} != {v_len}".format( 

904 t1_len=len(t1.convert_to_text()), v_len=len(v) 

905 ) 

906 ) 

907 return t1 

908 

909 return _value_factory 

910 

911 

912class ListInterpretation( 

913 GenericContentBasedInterpretation[Deb822ParsedTokenList[VE, ST], VE] 

914): 

915 

916 def __init__( 

917 self, 

918 tokenizer, # type: Callable[[str], Iterable['Deb822Token']] 

919 value_parser, # type: StreamingValueParser[VE] 

920 vtype, # type: Type[VE] 

921 stype, # type: Type[ST] 

922 default_separator_factory, # type: Callable[[], ST] 

923 render_factory, # type: Callable[[bool], Callable[[VE], str]] 

924 ): 

925 # type: (...) -> None 

926 super().__init__(tokenizer, value_parser) 

927 self._vtype = vtype 

928 self._stype = stype 

929 self._default_separator_factory = default_separator_factory 

930 self._render_factory = render_factory 

931 

932 def _high_level_interpretation( 

933 self, 

934 kvpair_element, # type: Deb822KeyValuePairElement 

935 proxy_element, # type: Deb822InterpretationProxyElement 

936 discard_comments_on_read=True, # type: bool 

937 ): 

938 # type: (...) -> Deb822ParsedTokenList[VE, ST] 

939 return Deb822ParsedTokenList( 

940 kvpair_element, 

941 proxy_element, 

942 self._vtype, 

943 self._stype, 

944 self._parse_str, 

945 self._default_separator_factory, 

946 self._render_factory(discard_comments_on_read), 

947 ) 

948 

949 

950def _parse_whitespace_list_value(token, _): 

951 # type: (Deb822Token, BufferingIterator[Deb822Token]) -> Deb822ParsedValueElement 

952 return Deb822ParsedValueElement([token]) 

953 

954 

955def _is_comma_token(v): 

956 # type: (TokenOrElement) -> bool 

957 # Consume tokens until the next comma 

958 return isinstance(v, Deb822CommaToken) 

959 

960 

961def _parse_comma_list_value(token, buffered_iterator): 

962 # type: (Deb822Token, BufferingIterator[Deb822Token]) -> Deb822ParsedValueElement 

963 comma_offset = buffered_iterator.peek_find(_is_comma_token) 

964 value_parts = [token] 

965 if comma_offset is not None: 

966 # The value is followed by a comma and now we know where it ends 

967 value_parts.extend(buffered_iterator.peek_many(comma_offset - 1)) 

968 else: 

969 # The value is the last value there is. Consume all remaining tokens 

970 # and then trim from the right. 

971 value_parts.extend(buffered_iterator.peek_buffer()) 

972 while value_parts and not isinstance(value_parts[-1], Deb822ValueToken): 

973 value_parts.pop() 

974 

975 buffered_iterator.consume_many(len(value_parts) - 1) 

976 return Deb822ParsedValueElement(value_parts) 

977 

978 

979def _parse_uploaders_list_value(token, buffered_iterator): 

980 # type: (Deb822Token, BufferingIterator[Deb822Token]) -> Deb822ParsedValueElement 

981 

982 # This is similar to _parse_comma_list_value *except* that there is an extra special 

983 # case. Namely comma only counts as a true separator if it follows ">" 

984 value_parts = [token] 

985 comma_offset = -1 # type: Optional[int] 

986 while comma_offset is not None: 

987 comma_offset = buffered_iterator.peek_find(_is_comma_token) 

988 if comma_offset is not None: 

989 # The value is followed by a comma. Verify that this is a terminating 

990 # comma (comma may appear in the name or email) 

991 # 

992 # We include value_parts[-1] to easily cope with the common case of 

993 # "foo <a@b.com>," where we will have 0 peeked element to examine. 

994 peeked_elements = [value_parts[-1]] 

995 peeked_elements.extend(buffered_iterator.peek_many(comma_offset - 1)) 

996 comma_was_separator = False 

997 i = len(peeked_elements) - 1 

998 while i >= 0: 

999 token = peeked_elements[i] 

1000 if isinstance(token, Deb822ValueToken): 

1001 if token.text.endswith(">"): 

1002 # The comma terminates the value 

1003 value_parts.extend(buffered_iterator.consume_many(i)) 

1004 assert isinstance( 

1005 value_parts[-1], Deb822ValueToken 

1006 ) and value_parts[-1].text.endswith(">"), "Got: " + str( 

1007 value_parts 

1008 ) 

1009 comma_was_separator = True 

1010 break 

1011 i -= 1 

1012 if comma_was_separator: 

1013 break 

1014 value_parts.extend(buffered_iterator.consume_many(comma_offset)) 

1015 assert isinstance(value_parts[-1], Deb822CommaToken) 

1016 else: 

1017 # The value is the last value there is. Consume all remaining tokens 

1018 # and then trim from the right. 

1019 remaining_part = buffered_iterator.peek_buffer() 

1020 consume_elements = len(remaining_part) 

1021 value_parts.extend(remaining_part) 

1022 while value_parts and not isinstance(value_parts[-1], Deb822ValueToken): 

1023 value_parts.pop() 

1024 consume_elements -= 1 

1025 buffered_iterator.consume_many(consume_elements) 

1026 

1027 return Deb822ParsedValueElement(value_parts) 

1028 

1029 

1030class Deb822Element(Locatable): 

1031 """Composite elements (consists of 1 or more tokens)""" 

1032 

1033 __slots__ = ("_parent_element", "_full_size_cache", "__weakref__") 

1034 

1035 def __init__(self): 

1036 # type: () -> None 

1037 self._parent_element = None # type: Optional[ReferenceType['Deb822Element']] 

1038 self._full_size_cache = None # type: Optional[Range] 

1039 

1040 def iter_parts(self): 

1041 # type: () -> Iterable[TokenOrElement] 

1042 raise NotImplementedError # pragma: no cover 

1043 

1044 def iter_parts_of_type(self, only_element_or_token_type): 

1045 # type: (Type[TE]) -> Iterable[TE] 

1046 for part in self.iter_parts(): 

1047 if isinstance(part, only_element_or_token_type): 

1048 yield part 

1049 

1050 def iter_tokens(self): 

1051 # type: () -> Iterable[Deb822Token] 

1052 for part in self.iter_parts(): 

1053 # Control check to catch bugs early 

1054 assert part._parent_element is not None 

1055 if isinstance(part, Deb822Element): 

1056 yield from part.iter_tokens() 

1057 else: 

1058 yield part 

1059 

1060 def iter_recurse( 

1061 self, *, only_element_or_token_type=None # type: Optional[Type[TE]] 

1062 ): 

1063 # type: (...) -> Iterable[TE] 

1064 for part in self.iter_parts(): 

1065 if only_element_or_token_type is None or isinstance( 1065 ↛ 1068line 1065 didn't jump to line 1068, because the condition on line 1065 was never true

1066 part, only_element_or_token_type 

1067 ): 

1068 yield cast("TE", part) 

1069 if isinstance(part, Deb822Element): 

1070 yield from part.iter_recurse( 

1071 only_element_or_token_type=only_element_or_token_type 

1072 ) 

1073 

1074 @property 

1075 def is_error(self): 

1076 # type: () -> bool 

1077 return False 

1078 

1079 @property 

1080 def is_comment(self): 

1081 # type: () -> bool 

1082 return False 

1083 

1084 @property 

1085 def parent_element(self): 

1086 # type: () -> Optional[Deb822Element] 

1087 return resolve_ref(self._parent_element) 

1088 

1089 @parent_element.setter 

1090 def parent_element(self, new_parent): 

1091 # type: (Optional[Deb822Element]) -> None 

1092 self._parent_element = ( 

1093 weakref.ref(new_parent) if new_parent is not None else None 

1094 ) 

1095 

1096 def _init_parent_of_parts(self): 

1097 # type: () -> None 

1098 for part in self.iter_parts(): 

1099 part.parent_element = self 

1100 

1101 # Deliberately not a "text" property, to signal that it is not necessary cheap. 

1102 def convert_to_text(self): 

1103 # type: () -> str 

1104 return "".join(t.text for t in self.iter_tokens()) 

1105 

1106 def clear_parent_if_parent(self, parent): 

1107 # type: (Deb822Element) -> None 

1108 if parent is self.parent_element: 1108 ↛ exitline 1108 didn't return from function 'clear_parent_if_parent', because the condition on line 1108 was never false

1109 self._parent_element = None 

1110 

1111 def size(self, *, skip_leading_comments: bool = True) -> Range: 

1112 size_cache = self._full_size_cache 

1113 if size_cache is None: 

1114 size_cache = Range.from_position_and_sizes( 

1115 START_POSITION, 

1116 (p.size(skip_leading_comments=False) for p in self.iter_parts()), 

1117 ) 

1118 self._full_size_cache = size_cache 

1119 return size_cache 

1120 

1121 

1122class Deb822InterpretationProxyElement(Deb822Element): 

1123 

1124 __slots__ = ("parts",) 

1125 

1126 def __init__( 

1127 self, real_element: Deb822Element, parts: List[TokenOrElement] 

1128 ) -> None: 

1129 super().__init__() 

1130 self.parent_element = real_element 

1131 self.parts = parts 

1132 for p in parts: 

1133 p.parent_element = self 

1134 

1135 def iter_parts(self): 

1136 # type: () -> Iterable[TokenOrElement] 

1137 return iter(self.parts) 

1138 

1139 def position_in_parent(self, *, skip_leading_comments: bool = True) -> Position: 

1140 parent = self.parent_element 

1141 if parent is None: 

1142 raise RuntimeError("parent was garbage collected") 

1143 return parent.position_in_parent() 

1144 

1145 def position_in_file(self, *, skip_leading_comments: bool = True) -> Position: 

1146 parent = self.parent_element 

1147 if parent is None: 

1148 raise RuntimeError("parent was garbage collected") 

1149 return parent.position_in_file() 

1150 

1151 def size(self, *, skip_leading_comments: bool = True) -> Range: 

1152 # Same as parent except we never use a cache. 

1153 sizes = (p.size(skip_leading_comments=False) for p in self.iter_parts()) 

1154 return Range.from_position_and_sizes(START_POSITION, sizes) 

1155 

1156 

1157class Deb822ErrorElement(Deb822Element): 

1158 """Element representing elements or tokens that are out of place 

1159 

1160 Commonly, it will just be instances of Deb822ErrorToken, but it can be other 

1161 things. As an example if a parser discovers out of order elements/tokens, 

1162 it can bundle them in a Deb822ErrorElement to signal that the sequence of 

1163 elements/tokens are invalid (even if the tokens themselves are valid). 

1164 """ 

1165 

1166 __slots__ = ("_parts",) 

1167 

1168 def __init__(self, parts): 

1169 # type: (Sequence[TokenOrElement]) -> None 

1170 super().__init__() 

1171 self._parts = tuple(parts) 

1172 self._init_parent_of_parts() 

1173 

1174 def iter_parts(self): 

1175 # type: () -> Iterable[TokenOrElement] 

1176 yield from self._parts 

1177 

1178 @property 

1179 def is_error(self): 

1180 # type: () -> bool 

1181 return True 

1182 

1183 

1184class Deb822ValueLineElement(Deb822Element): 

1185 """Consists of one "line" of a value""" 

1186 

1187 __slots__ = ( 

1188 "_comment_element", 

1189 "_continuation_line_token", 

1190 "_leading_whitespace_token", 

1191 "_value_tokens", 

1192 "_trailing_whitespace_token", 

1193 "_newline_token", 

1194 ) 

1195 

1196 def __init__( 

1197 self, 

1198 comment_element, # type: Optional[Deb822CommentElement] 

1199 continuation_line_token, # type: Optional[Deb822ValueContinuationToken] 

1200 leading_whitespace_token, # type: Optional[Deb822WhitespaceToken] 

1201 value_parts, # type: List[TokenOrElement] 

1202 trailing_whitespace_token, # type: Optional[Deb822WhitespaceToken] 

1203 # only optional if it is the last line of the file and the file does not 

1204 # end with a newline. 

1205 newline_token, # type: Optional[Deb822WhitespaceToken] 

1206 ): 

1207 # type: (...) -> None 

1208 super().__init__() 

1209 if comment_element is not None and continuation_line_token is None: 1209 ↛ 1210line 1209 didn't jump to line 1210, because the condition on line 1209 was never true

1210 raise ValueError("Only continuation lines can have comments") 

1211 self._comment_element = comment_element # type: Optional[Deb822CommentElement] 

1212 self._continuation_line_token = continuation_line_token 

1213 self._leading_whitespace_token = ( 

1214 leading_whitespace_token 

1215 ) # type: Optional[Deb822WhitespaceToken] 

1216 self._value_tokens = value_parts # type: List[TokenOrElement] 

1217 self._trailing_whitespace_token = trailing_whitespace_token 

1218 self._newline_token = newline_token # type: Optional[Deb822WhitespaceToken] 

1219 self._init_parent_of_parts() 

1220 

1221 @property 

1222 def comment_element(self): 

1223 # type: () -> Optional[Deb822CommentElement] 

1224 return self._comment_element 

1225 

1226 @property 

1227 def continuation_line_token(self): 

1228 # type: () -> Optional[Deb822ValueContinuationToken] 

1229 return self._continuation_line_token 

1230 

1231 @property 

1232 def newline_token(self): 

1233 # type: () -> Optional[Deb822WhitespaceToken] 

1234 return self._newline_token 

1235 

1236 def add_newline_if_missing(self): 

1237 # type: () -> bool 

1238 if self._newline_token is None: 

1239 self._newline_token = Deb822NewlineAfterValueToken() 

1240 self._newline_token.parent_element = self 

1241 self._full_size_cache = None 

1242 return True 

1243 return False 

1244 

1245 def _iter_content_parts(self): 

1246 # type: () -> Iterable[TokenOrElement] 

1247 if self._leading_whitespace_token: 1247 ↛ 1248line 1247 didn't jump to line 1248, because the condition on line 1247 was never true

1248 yield self._leading_whitespace_token 

1249 yield from self._value_tokens 

1250 if self._trailing_whitespace_token: 

1251 yield self._trailing_whitespace_token 

1252 

1253 def _iter_content_tokens(self): 

1254 # type: () -> Iterable[Deb822Token] 

1255 for part in self._iter_content_parts(): 

1256 if isinstance(part, Deb822Element): 

1257 yield from part.iter_tokens() 

1258 else: 

1259 yield part 

1260 

1261 def convert_content_to_text(self): 

1262 # type: () -> str 

1263 if ( 

1264 len(self._value_tokens) == 1 

1265 and not self._leading_whitespace_token 

1266 and not self._trailing_whitespace_token 

1267 and isinstance(self._value_tokens[0], Deb822Token) 

1268 ): 

1269 # By default, we get a single value spanning the entire line 

1270 # (minus continuation line and newline, but we are supposed to 

1271 # exclude those) 

1272 return self._value_tokens[0].text 

1273 

1274 return "".join(t.text for t in self._iter_content_tokens()) 

1275 

1276 def iter_parts(self): 

1277 # type: () -> Iterable[TokenOrElement] 

1278 if self._comment_element: 

1279 yield self._comment_element 

1280 if self._continuation_line_token: 

1281 yield self._continuation_line_token 

1282 yield from self._iter_content_parts() 

1283 if self._newline_token: 1283 ↛ exitline 1283 didn't return from function 'iter_parts', because the condition on line 1283 was never false

1284 yield self._newline_token 

1285 

1286 def size(self, *, skip_leading_comments: bool = True) -> Range: 

1287 if skip_leading_comments: 1287 ↛ 1288line 1287 didn't jump to line 1288, because the condition on line 1287 was never true

1288 return Range.from_position_and_sizes( 

1289 START_POSITION, 

1290 ( 

1291 p.size(skip_leading_comments=False) 

1292 for p in self.iter_parts() 

1293 if not p.is_comment 

1294 ), 

1295 ) 

1296 return super().size(skip_leading_comments=skip_leading_comments) 

1297 

1298 def position_in_parent(self, *, skip_leading_comments: bool = True) -> Position: 

1299 base_pos = super().position_in_parent(skip_leading_comments=False) 

1300 if skip_leading_comments: 

1301 for p in self.iter_parts(): 

1302 if p.is_comment: 

1303 continue 

1304 non_comment_pos = p.position_in_parent(skip_leading_comments=False) 

1305 base_pos = non_comment_pos.relative_to(base_pos) 

1306 return base_pos 

1307 

1308 

1309class Deb822ValueElement(Deb822Element): 

1310 __slots__ = ("_value_entry_elements",) 

1311 

1312 def __init__(self, value_entry_elements): 

1313 # type: (Sequence[Deb822ValueLineElement]) -> None 

1314 super().__init__() 

1315 # Split over two lines due to line length issues 

1316 v = tuple(value_entry_elements) 

1317 self._value_entry_elements = v # type: Sequence[Deb822ValueLineElement] 

1318 self._init_parent_of_parts() 

1319 

1320 @property 

1321 def value_lines(self): 

1322 # type: () -> Sequence[Deb822ValueLineElement] 

1323 """Read-only list of value entries""" 

1324 return self._value_entry_elements 

1325 

1326 def iter_parts(self): 

1327 # type: () -> Iterable[TokenOrElement] 

1328 yield from self._value_entry_elements 

1329 

1330 def add_final_newline_if_missing(self): 

1331 # type: () -> bool 

1332 if self._value_entry_elements: 

1333 changed = self._value_entry_elements[-1].add_newline_if_missing() 

1334 if changed: 

1335 self._full_size_cache = None 

1336 return changed 

1337 return False 

1338 

1339 

1340class Deb822ParsedValueElement(Deb822Element): 

1341 

1342 __slots__ = ("_text_cached", "_text_no_comments_cached", "_token_list") 

1343 

1344 def __init__(self, tokens): 

1345 # type: (List[Deb822Token]) -> None 

1346 super().__init__() 

1347 self._token_list = tokens 

1348 self._init_parent_of_parts() 

1349 if not isinstance(tokens[0], Deb822ValueToken) or not isinstance( 1349 ↛ 1352line 1349 didn't jump to line 1352, because the condition on line 1349 was never true

1350 tokens[-1], Deb822ValueToken 

1351 ): 

1352 raise ValueError( 

1353 self.__class__.__name__ + " MUST start and end on a Deb822ValueToken" 

1354 ) 

1355 if len(tokens) == 1: 1355 ↛ 1360line 1355 didn't jump to line 1360, because the condition on line 1355 was never false

1356 token = tokens[0] 

1357 self._text_cached = token.text # type: Optional[str] 

1358 self._text_no_comments_cached = token.text # type: Optional[str] 

1359 else: 

1360 self._text_cached = None 

1361 self._text_no_comments_cached = None 

1362 

1363 def convert_to_text(self): 

1364 # type: () -> str 

1365 if self._text_no_comments_cached is None: 1365 ↛ 1366line 1365 didn't jump to line 1366, because the condition on line 1365 was never true

1366 self._text_no_comments_cached = super().convert_to_text() 

1367 return self._text_no_comments_cached 

1368 

1369 def convert_to_text_without_comments(self): 

1370 # type: () -> str 

1371 if self._text_no_comments_cached is None: 1371 ↛ 1372line 1371 didn't jump to line 1372, because the condition on line 1371 was never true

1372 self._text_no_comments_cached = "".join( 

1373 t.text for t in self.iter_tokens() if not t.is_comment 

1374 ) 

1375 return self._text_no_comments_cached 

1376 

1377 def iter_parts(self): 

1378 # type: () -> Iterable[TokenOrElement] 

1379 yield from self._token_list 

1380 

1381 

1382class Deb822CommentElement(Deb822Element): 

1383 __slots__ = ("_comment_tokens",) 

1384 

1385 def __init__(self, comment_tokens): 

1386 # type: (Sequence[Deb822CommentToken]) -> None 

1387 super().__init__() 

1388 self._comment_tokens = tuple( 

1389 comment_tokens 

1390 ) # type: Sequence[Deb822CommentToken] 

1391 if not comment_tokens: # pragma: no cover 

1392 raise ValueError("Comment elements must have at least one comment token") 

1393 self._init_parent_of_parts() 

1394 

1395 @property 

1396 def is_comment(self): 

1397 # type: () -> bool 

1398 return True 

1399 

1400 def __len__(self): 

1401 # type: () -> int 

1402 return len(self._comment_tokens) 

1403 

1404 def __getitem__(self, item): 

1405 # type: (int) -> Deb822CommentToken 

1406 return self._comment_tokens[item] 

1407 

1408 def iter_parts(self): 

1409 # type: () -> Iterable[TokenOrElement] 

1410 yield from self._comment_tokens 

1411 

1412 

1413class Deb822KeyValuePairElement(Deb822Element): 

1414 __slots__ = ( 

1415 "_comment_element", 

1416 "_field_token", 

1417 "_separator_token", 

1418 "_value_element", 

1419 ) 

1420 

1421 def __init__( 

1422 self, 

1423 comment_element, # type: Optional[Deb822CommentElement] 

1424 field_token, # type: Deb822FieldNameToken 

1425 separator_token, # type: Deb822FieldSeparatorToken 

1426 value_element, # type: Deb822ValueElement 

1427 ): 

1428 # type: (...) -> None 

1429 super().__init__() 

1430 self._comment_element = comment_element # type: Optional[Deb822CommentElement] 

1431 self._field_token = field_token # type: Deb822FieldNameToken 

1432 self._separator_token = separator_token # type: Deb822FieldSeparatorToken 

1433 self._value_element = value_element # type: Deb822ValueElement 

1434 self._init_parent_of_parts() 

1435 

1436 @property 

1437 def field_name(self): 

1438 # type: () -> _strI 

1439 return self.field_token.text 

1440 

1441 @property 

1442 def field_token(self): 

1443 # type: () -> Deb822FieldNameToken 

1444 return self._field_token 

1445 

1446 @property 

1447 def value_element(self): 

1448 # type: () -> Deb822ValueElement 

1449 return self._value_element 

1450 

1451 @value_element.setter 

1452 def value_element(self, new_value): 

1453 # type: (Deb822ValueElement) -> None 

1454 self._full_size_cache = None 

1455 self._value_element.clear_parent_if_parent(self) 

1456 self._value_element = new_value 

1457 new_value.parent_element = self 

1458 

1459 def interpret_as( 

1460 self, 

1461 interpreter, # type: Interpretation[T] 

1462 discard_comments_on_read=True, # type: bool 

1463 ): 

1464 # type: (...) -> T 

1465 return interpreter.interpret( 

1466 self, discard_comments_on_read=discard_comments_on_read 

1467 ) 

1468 

1469 @property 

1470 def comment_element(self): 

1471 # type: () -> Optional[Deb822CommentElement] 

1472 return self._comment_element 

1473 

1474 @comment_element.setter 

1475 def comment_element(self, value): 

1476 # type: (Optional[Deb822CommentElement]) -> None 

1477 self._full_size_cache = None 

1478 if value is not None: 1478 ↛ 1479line 1478 didn't jump to line 1479, because the condition on line 1478 was never true

1479 if not value[-1].text.endswith("\n"): 

1480 raise ValueError("Field comments must end with a newline") 

1481 if self._comment_element: 1481 ↛ 1482line 1481 didn't jump to line 1482, because the condition on line 1481 was never true

1482 self._comment_element.clear_parent_if_parent(self) 

1483 if value is not None: 1483 ↛ 1484line 1483 didn't jump to line 1484, because the condition on line 1483 was never true

1484 value.parent_element = self 

1485 self._comment_element = value 

1486 

1487 def iter_parts(self): 

1488 # type: () -> Iterable[TokenOrElement] 

1489 if self._comment_element: 

1490 yield self._comment_element 

1491 yield self._field_token 

1492 yield self._separator_token 

1493 yield self._value_element 

1494 

1495 def position_in_parent( 

1496 self, 

1497 *, 

1498 skip_leading_comments: bool = True, 

1499 ) -> Position: 

1500 position = super().position_in_parent(skip_leading_comments=False) 

1501 if skip_leading_comments: 1501 ↛ 1505line 1501 didn't jump to line 1505, because the condition on line 1501 was never false

1502 if self._comment_element: 

1503 field_pos = self._field_token.position_in_parent() 

1504 position = field_pos.relative_to(position) 

1505 return position 

1506 

1507 def size(self, *, skip_leading_comments: bool = True) -> Range: 

1508 if skip_leading_comments: 

1509 return Range.from_position_and_sizes( 

1510 START_POSITION, 

1511 ( 

1512 p.size(skip_leading_comments=False) 

1513 for p in self.iter_parts() 

1514 if not p.is_comment 

1515 ), 

1516 ) 

1517 return super().size(skip_leading_comments=False) 

1518 

1519 

1520def _format_comment(c): 

1521 # type: (str) -> str 

1522 if c == "": 1522 ↛ 1524line 1522 didn't jump to line 1524, because the condition on line 1522 was never true

1523 # Special-case: Empty strings are mapped to an empty comment line 

1524 return "#\n" 

1525 if "\n" in c[:-1]: 1525 ↛ 1526line 1525 didn't jump to line 1526, because the condition on line 1525 was never true

1526 raise ValueError("Comment lines must not have embedded newlines") 

1527 if not c.endswith("\n"): 1527 ↛ 1529line 1527 didn't jump to line 1529, because the condition on line 1527 was never false

1528 c = c.rstrip() + "\n" 

1529 if not c.startswith("#"): 1529 ↛ 1531line 1529 didn't jump to line 1531, because the condition on line 1529 was never false

1530 c = "# " + c.lstrip() 

1531 return c 

1532 

1533 

1534def _unpack_key( 

1535 item, # type: ParagraphKey 

1536 raise_if_indexed=False, # type: bool 

1537): 

1538 # type: (...) -> Tuple[_strI, Optional[int], Optional[Deb822FieldNameToken]] 

1539 index = None # type: Optional[int] 

1540 name_token = None # type: Optional[Deb822FieldNameToken] 

1541 if isinstance(item, tuple): 

1542 key, index = item 

1543 if raise_if_indexed: 1543 ↛ 1550line 1543 didn't jump to line 1550, because the condition on line 1543 was never false

1544 # Fudge "(key, 0)" into a "key" callers to defensively support 

1545 # both paragraph styles with the same key. 

1546 if index != 0: 1546 ↛ 1547line 1546 didn't jump to line 1547, because the condition on line 1546 was never true

1547 msg = 'Cannot resolve key "{key}" with index {index}. The key is not indexed' 

1548 raise KeyError(msg.format(key=key, index=index)) 

1549 index = None 

1550 key = _strI(key) 

1551 else: 

1552 index = None 

1553 if isinstance(item, Deb822FieldNameToken): 1553 ↛ 1554line 1553 didn't jump to line 1554, because the condition on line 1553 was never true

1554 name_token = item 

1555 key = name_token.text 

1556 else: 

1557 key = _strI(item) 

1558 

1559 return key, index, name_token 

1560 

1561 

1562def _convert_value_lines_to_lines( 

1563 value_lines, # type: Iterable[Deb822ValueLineElement] 

1564 strip_comments, # type: bool 

1565): 

1566 # type: (...) -> Iterable[str] 

1567 if not strip_comments: 1567 ↛ 1568line 1567 didn't jump to line 1568, because the condition on line 1567 was never true

1568 yield from (v.convert_to_text() for v in value_lines) 

1569 else: 

1570 for element in value_lines: 

1571 yield "".join(x.text for x in element.iter_tokens() if not x.is_comment) 

1572 

1573 

1574if sys.version_info >= (3, 9) or TYPE_CHECKING: 1574 ↛ 1579line 1574 didn't jump to line 1579, because the condition on line 1574 was never false

1575 _ParagraphMapping_Base = collections.abc.Mapping[ParagraphKey, T] 

1576else: 

1577 # Python 3.5 - 3.8 compat - we are not allowed to subscript the abc.Iterator 

1578 # - use this little hack to work around it 

1579 class _ParagraphMapping_Base(collections.abc.Mapping, Generic[T], ABC): 

1580 pass 

1581 

1582 

1583# Deb822ParagraphElement uses this Mixin (by having `_paragraph` return self). 

1584# Therefore, the Mixin needs to call the "proper" methods on the paragraph to 

1585# avoid doing infinite recursion. 

1586class AutoResolvingMixin(Generic[T], _ParagraphMapping_Base[T]): 

1587 

1588 @property 

1589 def _auto_resolve_ambiguous_fields(self): 

1590 # type: () -> bool 

1591 return True 

1592 

1593 @property 

1594 def _paragraph(self): 

1595 # type: () -> Deb822ParagraphElement 

1596 raise NotImplementedError # pragma: no cover 

1597 

1598 def __len__(self): 

1599 # type: () -> int 

1600 return self._paragraph.kvpair_count 

1601 

1602 def __contains__(self, item): 

1603 # type: (object) -> bool 

1604 return self._paragraph.contains_kvpair_element(item) 

1605 

1606 def __iter__(self): 

1607 # type: () -> Iterator[ParagraphKey] 

1608 return iter(self._paragraph.iter_keys()) 

1609 

1610 def __getitem__(self, item): 

1611 # type: (ParagraphKey) -> T 

1612 if self._auto_resolve_ambiguous_fields and isinstance(item, str): 

1613 v = self._paragraph.get_kvpair_element((item, 0)) 

1614 else: 

1615 v = self._paragraph.get_kvpair_element(item) 

1616 assert v is not None 

1617 return self._interpret_value(item, v) 

1618 

1619 def __delitem__(self, item): 

1620 # type: (ParagraphKey) -> None 

1621 self._paragraph.remove_kvpair_element(item) 

1622 

1623 def _interpret_value(self, key, value): 

1624 # type: (ParagraphKey, Deb822KeyValuePairElement) -> T 

1625 raise NotImplementedError # pragma: no cover 

1626 

1627 

1628# Deb822ParagraphElement uses this Mixin (by having `_paragraph` return self). 

1629# Therefore, the Mixin needs to call the "proper" methods on the paragraph to 

1630# avoid doing infinite recursion. 

1631class Deb822ParagraphToStrWrapperMixin(AutoResolvingMixin[str], ABC): 

1632 

1633 @property 

1634 def _auto_map_initial_line_whitespace(self): 

1635 # type: () -> bool 

1636 return True 

1637 

1638 @property 

1639 def _discard_comments_on_read(self): 

1640 # type: () -> bool 

1641 return True 

1642 

1643 @property 

1644 def _auto_map_final_newline_in_multiline_values(self): 

1645 # type: () -> bool 

1646 return True 

1647 

1648 @property 

1649 def _preserve_field_comments_on_field_updates(self): 

1650 # type: () -> bool 

1651 return True 

1652 

1653 def _convert_value_to_str(self, kvpair_element): 

1654 # type: (Deb822KeyValuePairElement) -> str 

1655 value_element = kvpair_element.value_element 

1656 value_entries = value_element.value_lines 

1657 if len(value_entries) == 1: 

1658 # Special case single line entry (e.g. "Package: foo") as they never 

1659 # have comments and we can do some parts more efficient. 

1660 value_entry = value_entries[0] 

1661 t = value_entry.convert_to_text() 

1662 if self._auto_map_initial_line_whitespace: 

1663 t = t.strip() 

1664 return t 

1665 

1666 if self._auto_map_initial_line_whitespace or self._discard_comments_on_read: 

1667 converter = _convert_value_lines_to_lines( 

1668 value_entries, 

1669 self._discard_comments_on_read, 

1670 ) 

1671 

1672 auto_map_space = self._auto_map_initial_line_whitespace 

1673 

1674 # Because we know there are more than one line, we can unconditionally inject 

1675 # the newline after the first line 

1676 as_text = "".join( 

1677 line.strip() + "\n" if auto_map_space and i == 1 else line 

1678 for i, line in enumerate(converter, start=1) 

1679 ) 

1680 else: 

1681 # No rewrite necessary. 

1682 as_text = value_element.convert_to_text() 

1683 

1684 if self._auto_map_final_newline_in_multiline_values and as_text[-1] == "\n": 

1685 as_text = as_text[:-1] 

1686 return as_text 

1687 

1688 def __setitem__(self, item, value): 

1689 # type: (ParagraphKey, str) -> None 

1690 keep_comments = ( 

1691 self._preserve_field_comments_on_field_updates 

1692 ) # type: Optional[bool] 

1693 comment = None 

1694 if keep_comments and self._auto_resolve_ambiguous_fields: 

1695 # For ambiguous fields, we have to resolve the original field as 

1696 # the set_field_* methods do not cope with ambiguous fields. This 

1697 # means we might as well clear the keep_comments flag as we have 

1698 # resolved the comment. 

1699 keep_comments = None 

1700 key_lookup = item 

1701 if isinstance(item, str): 1701 ↛ 1703line 1701 didn't jump to line 1703, because the condition on line 1701 was never false

1702 key_lookup = (item, 0) 

1703 orig_kvpair = self._paragraph.get_kvpair_element(key_lookup, use_get=True) 

1704 if orig_kvpair is not None: 

1705 comment = orig_kvpair.comment_element 

1706 

1707 if self._auto_map_initial_line_whitespace: 

1708 try: 

1709 idx = value.index("\n") 

1710 except ValueError: 

1711 idx = -1 

1712 if idx == -1 or idx == len(value): 

1713 self._paragraph.set_field_to_simple_value( 

1714 item, 

1715 value.strip(), 

1716 preserve_original_field_comment=keep_comments, 

1717 field_comment=comment, 

1718 ) 

1719 return 

1720 # Regenerate the first line with normalized whitespace if necessary 

1721 first_line, rest = value.split("\n", 1) 

1722 if first_line and first_line[:1] not in ("\t", " "): 1722 ↛ 1723line 1722 didn't jump to line 1723, because the condition on line 1722 was never true

1723 value = "".join((" ", first_line.strip(), "\n", rest)) 

1724 else: 

1725 value = "".join((first_line, "\n", rest)) 

1726 if not value.endswith("\n"): 

1727 if not self._auto_map_final_newline_in_multiline_values: 1727 ↛ 1732line 1727 didn't jump to line 1732, because the condition on line 1727 was never false

1728 raise ValueError( 

1729 "Values must end with a newline (or be single line" 

1730 " values and use the auto whitespace mapping feature)" 

1731 ) 

1732 value += "\n" 

1733 self._paragraph.set_field_from_raw_string( 

1734 item, 

1735 value, 

1736 preserve_original_field_comment=keep_comments, 

1737 field_comment=comment, 

1738 ) 

1739 

1740 def _interpret_value(self, key, value): 

1741 # type: (ParagraphKey, Deb822KeyValuePairElement) -> str 

1742 # mypy is a bit dense and cannot see that T == str 

1743 return self._convert_value_to_str(value) 

1744 

1745 

1746class AbstractDeb822ParagraphWrapper(AutoResolvingMixin[T], ABC): 

1747 

1748 def __init__( 

1749 self, 

1750 paragraph, # type: Deb822ParagraphElement 

1751 *, 

1752 auto_resolve_ambiguous_fields=False, # type: bool 

1753 discard_comments_on_read=True, # type: bool 

1754 ): 

1755 # type: (...) -> None 

1756 self.__paragraph = paragraph 

1757 self.__auto_resolve_ambiguous_fields = auto_resolve_ambiguous_fields 

1758 self.__discard_comments_on_read = discard_comments_on_read 

1759 

1760 @property 

1761 def _paragraph(self): 

1762 # type: () -> Deb822ParagraphElement 

1763 return self.__paragraph 

1764 

1765 @property 

1766 def _discard_comments_on_read(self): 

1767 # type: () -> bool 

1768 return self.__discard_comments_on_read 

1769 

1770 @property 

1771 def _auto_resolve_ambiguous_fields(self): 

1772 # type: () -> bool 

1773 return self.__auto_resolve_ambiguous_fields 

1774 

1775 

1776class Deb822InterpretingParagraphWrapper(AbstractDeb822ParagraphWrapper[T]): 

1777 

1778 def __init__( 

1779 self, 

1780 paragraph, # type: Deb822ParagraphElement 

1781 interpretation, # type: Interpretation[T] 

1782 *, 

1783 auto_resolve_ambiguous_fields=False, # type: bool 

1784 discard_comments_on_read=True, # type: bool 

1785 ): 

1786 # type: (...) -> None 

1787 super().__init__( 

1788 paragraph, 

1789 auto_resolve_ambiguous_fields=auto_resolve_ambiguous_fields, 

1790 discard_comments_on_read=discard_comments_on_read, 

1791 ) 

1792 self._interpretation = interpretation 

1793 

1794 def _interpret_value(self, key, value): 

1795 # type: (ParagraphKey, Deb822KeyValuePairElement) -> T 

1796 return self._interpretation.interpret(value) 

1797 

1798 

1799class Deb822DictishParagraphWrapper( 

1800 AbstractDeb822ParagraphWrapper[str], Deb822ParagraphToStrWrapperMixin 

1801): 

1802 

1803 def __init__( 

1804 self, 

1805 paragraph, # type: Deb822ParagraphElement 

1806 *, 

1807 discard_comments_on_read=True, # type: bool 

1808 auto_map_initial_line_whitespace=True, # type: bool 

1809 auto_resolve_ambiguous_fields=False, # type: bool 

1810 preserve_field_comments_on_field_updates=True, # type: bool 

1811 auto_map_final_newline_in_multiline_values=True, # type: bool 

1812 ): 

1813 # type: (...) -> None 

1814 super().__init__( 

1815 paragraph, 

1816 auto_resolve_ambiguous_fields=auto_resolve_ambiguous_fields, 

1817 discard_comments_on_read=discard_comments_on_read, 

1818 ) 

1819 self.__auto_map_initial_line_whitespace = auto_map_initial_line_whitespace 

1820 self.__preserve_field_comments_on_field_updates = ( 

1821 preserve_field_comments_on_field_updates 

1822 ) 

1823 self.__auto_map_final_newline_in_multiline_values = ( 

1824 auto_map_final_newline_in_multiline_values 

1825 ) 

1826 

1827 @property 

1828 def _auto_map_initial_line_whitespace(self): 

1829 # type: () -> bool 

1830 return self.__auto_map_initial_line_whitespace 

1831 

1832 @property 

1833 def _preserve_field_comments_on_field_updates(self): 

1834 # type: () -> bool 

1835 return self.__preserve_field_comments_on_field_updates 

1836 

1837 @property 

1838 def _auto_map_final_newline_in_multiline_values(self): 

1839 # type: () -> bool 

1840 return self.__auto_map_final_newline_in_multiline_values 

1841 

1842 

1843class Deb822ParagraphElement(Deb822Element, Deb822ParagraphToStrWrapperMixin, ABC): 

1844 

1845 @classmethod 

1846 def new_empty_paragraph(cls): 

1847 # type: () -> Deb822ParagraphElement 

1848 return Deb822NoDuplicateFieldsParagraphElement([], OrderedSet()) 

1849 

1850 @classmethod 

1851 def from_dict(cls, mapping): 

1852 # type: (Mapping[str, str]) -> Deb822ParagraphElement 

1853 paragraph = cls.new_empty_paragraph() 

1854 for k, v in mapping.items(): 

1855 paragraph[k] = v 

1856 return paragraph 

1857 

1858 @classmethod 

1859 def from_kvpairs(cls, kvpair_elements): 

1860 # type: (List[Deb822KeyValuePairElement]) -> Deb822ParagraphElement 

1861 if not kvpair_elements: 1861 ↛ 1862line 1861 didn't jump to line 1862, because the condition on line 1861 was never true

1862 raise ValueError( 

1863 "A paragraph must consist of at least one field/value pair" 

1864 ) 

1865 kvpair_order = OrderedSet(kv.field_name for kv in kvpair_elements) 

1866 if len(kvpair_order) == len(kvpair_elements): 1866 ↛ 1875line 1866 didn't jump to line 1875, because the condition on line 1866 was never false

1867 # Each field occurs at most once, which is good because that 

1868 # means it is a valid paragraph and we can use the optimized 

1869 # implementation. 

1870 return Deb822NoDuplicateFieldsParagraphElement( 

1871 kvpair_elements, kvpair_order 

1872 ) 

1873 # Fallback implementation, that can cope with the repeated field names 

1874 # at the cost of complexity. 

1875 return Deb822DuplicateFieldsParagraphElement(kvpair_elements) 

1876 

1877 @property 

1878 def has_duplicate_fields(self): 

1879 # type: () -> bool 

1880 """Tell whether this paragraph has duplicate fields""" 

1881 return False 

1882 

1883 def as_interpreted_dict_view( 

1884 self, 

1885 interpretation, # type: Interpretation[T] 

1886 *, 

1887 auto_resolve_ambiguous_fields=True, # type: bool 

1888 ): 

1889 # type: (...) -> Deb822InterpretingParagraphWrapper[T] 

1890 r"""Provide a Dict-like view of the paragraph 

1891 

1892 This method returns a dict-like object representing this paragraph and 

1893 is useful for accessing fields in a given interpretation. It is possible 

1894 to use multiple versions of this dict-like view with different interpretations 

1895 on the same paragraph at the same time (for different fields). 

1896 

1897 >>> example_deb822_paragraph = ''' 

1898 ... Package: foo 

1899 ... # Field comment (because it becomes just before a field) 

1900 ... Architecture: amd64 

1901 ... # Inline comment (associated with the next line) 

1902 ... i386 

1903 ... # We also support arm 

1904 ... arm64 

1905 ... armel 

1906 ... ''' 

1907 >>> dfile = parse_deb822_file(example_deb822_paragraph.splitlines()) 

1908 >>> paragraph = next(iter(dfile)) 

1909 >>> list_view = paragraph.as_interpreted_dict_view(LIST_SPACE_SEPARATED_INTERPRETATION) 

1910 >>> # With the defaults, you only deal with the semantic values 

1911 >>> # - no leading or trailing whitespace on the first part of the value 

1912 >>> list(list_view["Package"]) 

1913 ['foo'] 

1914 >>> with list_view["Architecture"] as arch_list: 

1915 ... orig_arch_list = list(arch_list) 

1916 ... arch_list.replace('i386', 'kfreebsd-amd64') 

1917 >>> orig_arch_list 

1918 ['amd64', 'i386', 'arm64', 'armel'] 

1919 >>> list(list_view["Architecture"]) 

1920 ['amd64', 'kfreebsd-amd64', 'arm64', 'armel'] 

1921 >>> print(paragraph.dump(), end='') 

1922 Package: foo 

1923 # Field comment (because it becomes just before a field) 

1924 Architecture: amd64 

1925 # Inline comment (associated with the next line) 

1926 kfreebsd-amd64 

1927 # We also support arm 

1928 arm64 

1929 armel 

1930 >>> # Format preserved and architecture replaced 

1931 >>> with list_view["Architecture"] as arch_list: 

1932 ... # Prettify the result as sorting will cause awkward whitespace 

1933 ... arch_list.reformat_when_finished() 

1934 ... arch_list.sort() 

1935 >>> print(paragraph.dump(), end='') 

1936 Package: foo 

1937 # Field comment (because it becomes just before a field) 

1938 Architecture: amd64 

1939 # We also support arm 

1940 arm64 

1941 armel 

1942 # Inline comment (associated with the next line) 

1943 kfreebsd-amd64 

1944 >>> list(list_view["Architecture"]) 

1945 ['amd64', 'arm64', 'armel', 'kfreebsd-amd64'] 

1946 >>> # Format preserved and architecture values sorted 

1947 

1948 :param interpretation: Decides how the field values are interpreted. As an example, 

1949 use LIST_SPACE_SEPARATED_INTERPRETATION for fields such as Architecture in the 

1950 debian/control file. 

1951 :param auto_resolve_ambiguous_fields: This parameter is only relevant for paragraphs 

1952 that contain the same field multiple times (these are generally invalid). If the 

1953 caller requests an ambiguous field from an invalid paragraph via a plain field name, 

1954 the return dict-like object will refuse to resolve the field (not knowing which 

1955 version to pick). This parameter (if set to True) instead changes the error into 

1956 assuming the caller wants the *first* variant. 

1957 """ 

1958 return Deb822InterpretingParagraphWrapper( 

1959 self, 

1960 interpretation, 

1961 auto_resolve_ambiguous_fields=auto_resolve_ambiguous_fields, 

1962 ) 

1963 

1964 def configured_view( 

1965 self, 

1966 *, 

1967 discard_comments_on_read=True, # type: bool 

1968 auto_map_initial_line_whitespace=True, # type: bool 

1969 auto_resolve_ambiguous_fields=True, # type: bool 

1970 preserve_field_comments_on_field_updates=True, # type: bool 

1971 auto_map_final_newline_in_multiline_values=True, # type: bool 

1972 ): 

1973 # type: (...) -> Deb822DictishParagraphWrapper 

1974 r"""Provide a Dict[str, str]-like view of this paragraph with non-standard parameters 

1975 

1976 This method returns a dict-like object representing this paragraph that is 

1977 optionally configured differently from the default view. 

1978 

1979 >>> example_deb822_paragraph = ''' 

1980 ... Package: foo 

1981 ... # Field comment (because it becomes just before a field) 

1982 ... Depends: libfoo, 

1983 ... # Inline comment (associated with the next line) 

1984 ... libbar, 

1985 ... ''' 

1986 >>> dfile = parse_deb822_file(example_deb822_paragraph.splitlines()) 

1987 >>> paragraph = next(iter(dfile)) 

1988 >>> # With the defaults, you only deal with the semantic values 

1989 >>> # - no leading or trailing whitespace on the first part of the value 

1990 >>> paragraph["Package"] 

1991 'foo' 

1992 >>> # - no inline comments in multiline values (but whitespace will be present 

1993 >>> # subsequent lines.) 

1994 >>> print(paragraph["Depends"]) 

1995 libfoo, 

1996 libbar, 

1997 >>> paragraph['Foo'] = 'bar' 

1998 >>> paragraph.get('Foo') 

1999 'bar' 

2000 >>> paragraph.get('Unknown-Field') is None 

2001 True 

2002 >>> # But you get asymmetric behaviour with set vs. get 

2003 >>> paragraph['Foo'] = ' bar\n' 

2004 >>> paragraph['Foo'] 

2005 'bar' 

2006 >>> paragraph['Bar'] = ' bar\n#Comment\n another value\n' 

2007 >>> # Note that the whitespace on the first line has been normalized. 

2008 >>> print("Bar: " + paragraph['Bar']) 

2009 Bar: bar 

2010 another value 

2011 >>> # The comment is present (in case you where wondering) 

2012 >>> print(paragraph.get_kvpair_element('Bar').convert_to_text(), end='') 

2013 Bar: bar 

2014 #Comment 

2015 another value 

2016 >>> # On the other hand, you can choose to see the values as they are 

2017 >>> # - We will just reset the paragraph as a "nothing up my sleeve" 

2018 >>> dfile = parse_deb822_file(example_deb822_paragraph.splitlines()) 

2019 >>> paragraph = next(iter(dfile)) 

2020 >>> nonstd_dictview = paragraph.configured_view( 

2021 ... discard_comments_on_read=False, 

2022 ... auto_map_initial_line_whitespace=False, 

2023 ... # For paragraphs with duplicate fields, you can choose to get an error 

2024 ... # rather than the dict picking the first value available. 

2025 ... auto_resolve_ambiguous_fields=False, 

2026 ... auto_map_final_newline_in_multiline_values=False, 

2027 ... ) 

2028 >>> # Because we have reset the state, Foo and Bar are no longer there. 

2029 >>> 'Bar' not in paragraph and 'Foo' not in paragraph 

2030 True 

2031 >>> # We can now see the comments (discard_comments_on_read=False) 

2032 >>> # (The leading whitespace in front of "libfoo" is due to 

2033 >>> # auto_map_initial_line_whitespace=False) 

2034 >>> print(nonstd_dictview["Depends"], end='') 

2035 libfoo, 

2036 # Inline comment (associated with the next line) 

2037 libbar, 

2038 >>> # And all the optional whitespace on the first value line 

2039 >>> # (auto_map_initial_line_whitespace=False) 

2040 >>> nonstd_dictview["Package"] == ' foo\n' 

2041 True 

2042 >>> # ... which will give you symmetric behaviour with set vs. get 

2043 >>> nonstd_dictview['Foo'] = ' bar \n' 

2044 >>> nonstd_dictview['Foo'] 

2045 ' bar \n' 

2046 >>> nonstd_dictview['Bar'] = ' bar \n#Comment\n another value\n' 

2047 >>> nonstd_dictview['Bar'] 

2048 ' bar \n#Comment\n another value\n' 

2049 >>> # But then you get no help either. 

2050 >>> try: 

2051 ... nonstd_dictview["Baz"] = "foo" 

2052 ... except ValueError: 

2053 ... print("Rejected") 

2054 Rejected 

2055 >>> # With auto_map_initial_line_whitespace=False, you have to include minimum a newline 

2056 >>> nonstd_dictview["Baz"] = "foo\n" 

2057 >>> # The absence of leading whitespace gives you the terse variant at the expensive 

2058 >>> # readability 

2059 >>> paragraph.get_kvpair_element('Baz').convert_to_text() 

2060 'Baz:foo\n' 

2061 >>> # But because they are views, changes performed via one view is visible in the other 

2062 >>> paragraph['Foo'] 

2063 'bar' 

2064 >>> # The views show the values according to their own rules. Therefore, there is an 

2065 >>> # asymmetric between paragraph['Foo'] and nonstd_dictview['Foo'] 

2066 >>> # Nevertheless, you can read or write the fields via either - enabling you to use 

2067 >>> # the view that best suit your use-case for the given field. 

2068 >>> 'Baz' in paragraph and nonstd_dictview.get('Baz') is not None 

2069 True 

2070 >>> # Deletion via the view also works 

2071 >>> del nonstd_dictview['Baz'] 

2072 >>> 'Baz' not in paragraph and nonstd_dictview.get('Baz') is None 

2073 True 

2074 

2075 

2076 :param discard_comments_on_read: When getting a field value from the dict, 

2077 this parameter decides how in-line comments are handled. When setting 

2078 the value, inline comments are still allowed and will be retained. 

2079 However, keep in mind that this option makes getter and setter asymmetric 

2080 as a "get" following a "set" with inline comments will omit the comments 

2081 even if they are there (see the code example). 

2082 :param auto_map_initial_line_whitespace: Special-case the first value line 

2083 by trimming unnecessary whitespace leaving only the value. For single-line 

2084 values, all space including newline is pruned. For multi-line values, the 

2085 newline is preserved / needed to distinguish the first line from the 

2086 following lines. When setting a value, this option normalizes the 

2087 whitespace of the initial line of the value field. 

2088 When this option is set to True makes the dictionary behave more like the 

2089 original Deb822 module. 

2090 :param preserve_field_comments_on_field_updates: Whether to preserve the field 

2091 comments when mutating the field. 

2092 :param auto_resolve_ambiguous_fields: This parameter is only relevant for paragraphs 

2093 that contain the same field multiple times (these are generally invalid). If the 

2094 caller requests an ambiguous field from an invalid paragraph via a plain field name, 

2095 the return dict-like object will refuse to resolve the field (not knowing which 

2096 version to pick). This parameter (if set to True) instead changes the error into 

2097 assuming the caller wants the *first* variant. 

2098 :param auto_map_final_newline_in_multiline_values: This parameter controls whether 

2099 a multiline field with have / need a trailing newline. If True, the trailing 

2100 newline is hidden on get and automatically added in set (if missing). 

2101 When this option is set to True makes the dictionary behave more like the 

2102 original Deb822 module. 

2103 """ 

2104 return Deb822DictishParagraphWrapper( 

2105 self, 

2106 discard_comments_on_read=discard_comments_on_read, 

2107 auto_map_initial_line_whitespace=auto_map_initial_line_whitespace, 

2108 auto_resolve_ambiguous_fields=auto_resolve_ambiguous_fields, 

2109 preserve_field_comments_on_field_updates=preserve_field_comments_on_field_updates, 

2110 auto_map_final_newline_in_multiline_values=auto_map_final_newline_in_multiline_values, 

2111 ) 

2112 

2113 @property 

2114 def _paragraph(self): 

2115 # type: () -> Deb822ParagraphElement 

2116 return self 

2117 

2118 def order_last(self, field): 

2119 # type: (ParagraphKey) -> None 

2120 """Re-order the given field so it is "last" in the paragraph""" 

2121 raise NotImplementedError # pragma: no cover 

2122 

2123 def order_first(self, field): 

2124 # type: (ParagraphKey) -> None 

2125 """Re-order the given field so it is "first" in the paragraph""" 

2126 raise NotImplementedError # pragma: no cover 

2127 

2128 def order_before(self, field, reference_field): 

2129 # type: (ParagraphKey, ParagraphKey) -> None 

2130 """Re-order the given field so appears directly after the reference field in the paragraph 

2131 

2132 The reference field must be present.""" 

2133 raise NotImplementedError # pragma: no cover 

2134 

2135 def order_after(self, field, reference_field): 

2136 # type: (ParagraphKey, ParagraphKey) -> None 

2137 """Re-order the given field so appears directly before the reference field in the paragraph 

2138 

2139 The reference field must be present. 

2140 """ 

2141 raise NotImplementedError # pragma: no cover 

2142 

2143 @property 

2144 def kvpair_count(self): 

2145 # type: () -> int 

2146 raise NotImplementedError # pragma: no cover 

2147 

2148 def iter_keys(self): 

2149 # type: () -> Iterable[ParagraphKey] 

2150 raise NotImplementedError # pragma: no cover 

2151 

2152 def contains_kvpair_element(self, item): 

2153 # type: (object) -> bool 

2154 raise NotImplementedError # pragma: no cover 

2155 

2156 def get_kvpair_element( 

2157 self, 

2158 item, # type: ParagraphKey 

2159 use_get=False, # type: bool 

2160 ): 

2161 # type: (...) -> Optional[Deb822KeyValuePairElement] 

2162 raise NotImplementedError # pragma: no cover 

2163 

2164 def set_kvpair_element(self, key, value): 

2165 # type: (ParagraphKey, Deb822KeyValuePairElement) -> None 

2166 raise NotImplementedError # pragma: no cover 

2167 

2168 def remove_kvpair_element(self, key): 

2169 # type: (ParagraphKey) -> None 

2170 raise NotImplementedError # pragma: no cover 

2171 

2172 def sort_fields( 

2173 self, key=None # type: Optional[Callable[[str], Any]] 

2174 ): 

2175 # type: (...) -> None 

2176 """Re-order all fields 

2177 

2178 :param key: Provide a key function (same semantics as for sorted). Keep in mind that 

2179 the module preserve the cases for field names - in generally, callers are recommended 

2180 to use "lower()" to normalize the case. 

2181 """ 

2182 raise NotImplementedError # pragma: no cover 

2183 

2184 def set_field_to_simple_value( 

2185 self, 

2186 item, # type: ParagraphKey 

2187 simple_value, # type: str 

2188 *, 

2189 preserve_original_field_comment=None, # type: Optional[bool] 

2190 field_comment=None, # type: Optional[Commentish] 

2191 ): 

2192 # type: (...) -> None 

2193 r"""Sets a field in this paragraph to a simple "word" or "phrase" 

2194 

2195 In many cases, it is better for callers to just use the paragraph as 

2196 if it was a dictionary. However, this method does enable to you choose 

2197 the field comment (if any), which can be a reason for using it. 

2198 

2199 This is suitable for "simple" fields like "Package". Example: 

2200 

2201 >>> example_deb822_paragraph = ''' 

2202 ... Package: foo 

2203 ... ''' 

2204 >>> dfile = parse_deb822_file(example_deb822_paragraph.splitlines()) 

2205 >>> p = next(iter(dfile)) 

2206 >>> p.set_field_to_simple_value("Package", "mscgen") 

2207 >>> p.set_field_to_simple_value("Architecture", "linux-any kfreebsd-any", 

2208 ... field_comment=['Only ported to linux and kfreebsd']) 

2209 >>> p.set_field_to_simple_value("Priority", "optional") 

2210 >>> print(p.dump(), end='') 

2211 Package: mscgen 

2212 # Only ported to linux and kfreebsd 

2213 Architecture: linux-any kfreebsd-any 

2214 Priority: optional 

2215 >>> # Values are formatted nicely by default, but it does not work with 

2216 >>> # multi-line values 

2217 >>> p.set_field_to_simple_value("Foo", "bar\nbin\n") 

2218 Traceback (most recent call last): 

2219 ... 

2220 ValueError: Cannot use set_field_to_simple_value for values with newlines 

2221 

2222 :param item: Name of the field to set. If the paragraph already 

2223 contains the field, then it will be replaced. If the field exists, 

2224 then it will preserve its order in the paragraph. Otherwise, it is 

2225 added to the end of the paragraph. 

2226 Note this can be a "paragraph key", which enables you to control 

2227 *which* instance of a field is being replaced (in case of duplicate 

2228 fields). 

2229 :param simple_value: The text to use as the value. The value must not 

2230 contain newlines. Leading and trailing will be stripped but space 

2231 within the value is preserved. The value cannot contain comments 

2232 (i.e. if the "#" token appears in the value, then it is considered 

2233 a value rather than "start of a comment) 

2234 :param preserve_original_field_comment: See the description for the 

2235 parameter with the same name in the set_field_from_raw_string method. 

2236 :param field_comment: See the description for the parameter with the same 

2237 name in the set_field_from_raw_string method. 

2238 """ 

2239 if "\n" in simple_value: 

2240 raise ValueError( 

2241 "Cannot use set_field_to_simple_value for values with newlines" 

2242 ) 

2243 

2244 # Reformat it with a leading space and trailing newline. The latter because it is 

2245 # necessary if there are any fields after it and the former because it looks nicer so 

2246 # have single space after the field separator 

2247 stripped = simple_value.strip() 

2248 if stripped: 2248 ↛ 2252line 2248 didn't jump to line 2252, because the condition on line 2248 was never false

2249 raw_value = " " + stripped + "\n" 

2250 else: 

2251 # Special-case for empty values 

2252 raw_value = "\n" 

2253 self.set_field_from_raw_string( 

2254 item, 

2255 raw_value, 

2256 preserve_original_field_comment=preserve_original_field_comment, 

2257 field_comment=field_comment, 

2258 ) 

2259 

2260 def set_field_from_raw_string( 

2261 self, 

2262 item, # type: ParagraphKey 

2263 raw_string_value, # type: str 

2264 *, 

2265 preserve_original_field_comment=None, # type: Optional[bool] 

2266 field_comment=None, # type: Optional[Commentish] 

2267 ): 

2268 # type: (...) -> None 

2269 """Sets a field in this paragraph to a given text value 

2270 

2271 In many cases, it is better for callers to just use the paragraph as 

2272 if it was a dictionary. However, this method does enable to you choose 

2273 the field comment (if any) and lets to have a higher degree of control 

2274 over whitespace (on the first line), which can be a reason for using it. 

2275 

2276 Example usage: 

2277 

2278 >>> example_deb822_paragraph = ''' 

2279 ... Package: foo 

2280 ... ''' 

2281 >>> dfile = parse_deb822_file(example_deb822_paragraph.splitlines()) 

2282 >>> p = next(iter(dfile)) 

2283 >>> raw_value = ''' 

2284 ... Build-Depends: debhelper-compat (= 12), 

2285 ... some-other-bd, 

2286 ... # Comment 

2287 ... another-bd, 

2288 ... '''.lstrip() # Remove leading newline, but *not* the trailing newline 

2289 >>> fname, new_value = raw_value.split(':', 1) 

2290 >>> p.set_field_from_raw_string(fname, new_value) 

2291 >>> print(p.dump(), end='') 

2292 Package: foo 

2293 Build-Depends: debhelper-compat (= 12), 

2294 some-other-bd, 

2295 # Comment 

2296 another-bd, 

2297 >>> # Format preserved 

2298 

2299 :param item: Name of the field to set. If the paragraph already 

2300 contains the field, then it will be replaced. Otherwise, it is 

2301 added to the end of the paragraph. 

2302 Note this can be a "paragraph key", which enables you to control 

2303 *which* instance of a field is being replaced (in case of duplicate 

2304 fields). 

2305 :param raw_string_value: The text to use as the value. The text must 

2306 be valid deb822 syntax and is used *exactly* as it is given. 

2307 Accordingly, multi-line values must include mandatory leading space 

2308 on continuation lines, newlines after the value, etc. On the 

2309 flip-side, any optional space or comments will be included. 

2310 

2311 Note that the first line will *never* be read as a comment (if the 

2312 first line of the value starts with a "#" then it will result 

2313 in "Field-Name:#..." which is parsed as a value starting with "#" 

2314 rather than a comment). 

2315 :param preserve_original_field_comment: If True, then if there is an 

2316 existing field and that has a comment, then the comment will remain 

2317 after this operation. This is the default is the `field_comment` 

2318 parameter is omitted. 

2319 Note that if the parameter is True and the item is ambiguous, this 

2320 will raise an AmbiguousDeb822FieldKeyError. When the parameter is 

2321 omitted, the ambiguity is resolved automatically and if the resolved 

2322 field has a comment then that will be preserved (assuming 

2323 field_comment is None). 

2324 :param field_comment: If not None, add or replace the comment for 

2325 the field. Each string in the list will become one comment 

2326 line (inserted directly before the field name). Will appear in the 

2327 same order as they do in the list. 

2328 

2329 If you want complete control over the formatting of the comments, 

2330 then ensure that each line start with "#" and end with "\\n" before 

2331 the call. Otherwise, leading/trailing whitespace is normalized 

2332 and the missing "#"/"\\n" character is inserted. 

2333 """ 

2334 

2335 new_content = [] # type: List[str] 

2336 if preserve_original_field_comment is not None: 

2337 if field_comment is not None: 2337 ↛ 2338line 2337 didn't jump to line 2338, because the condition on line 2337 was never true

2338 raise ValueError( 

2339 'The "preserve_original_field_comment" conflicts with' 

2340 ' "field_comment" parameter' 

2341 ) 

2342 elif field_comment is not None: 

2343 if not isinstance(field_comment, Deb822CommentElement): 2343 ↛ 2346line 2343 didn't jump to line 2346, because the condition on line 2343 was never false

2344 new_content.extend(_format_comment(x) for x in field_comment) 

2345 field_comment = None 

2346 preserve_original_field_comment = False 

2347 

2348 field_name, _, _ = _unpack_key(item) 

2349 

2350 cased_field_name = field_name 

2351 try: 

2352 original = self.get_kvpair_element(item, use_get=True) 

2353 except AmbiguousDeb822FieldKeyError: 

2354 if preserve_original_field_comment: 

2355 # If we were asked to preserve the original comment, then we 

2356 # require a strict lookup 

2357 raise 

2358 original = self.get_kvpair_element((field_name, 0), use_get=True) 

2359 

2360 if preserve_original_field_comment is None: 

2361 # We simplify preserve_original_field_comment after the lookup of the field. 

2362 # Otherwise, we can get ambiguous key errors when updating an ambiguous field 

2363 # when the caller did not explicitly ask for that behaviour. 

2364 preserve_original_field_comment = True 

2365 

2366 if original: 

2367 # If we already have the field, then preserve the original case 

2368 cased_field_name = original.field_name 

2369 raw = ":".join((cased_field_name, raw_string_value)) 

2370 raw_lines = raw.splitlines(keepends=True) 

2371 for i, line in enumerate(raw_lines, start=1): 

2372 if not line.endswith("\n"): 2372 ↛ 2373line 2372 didn't jump to line 2373, because the condition on line 2372 was never true

2373 raise ValueError( 

2374 "Line {i} in new value was missing trailing newline".format(i=i) 

2375 ) 

2376 if i != 1 and line[0] not in (" ", "\t", "#"): 2376 ↛ 2377line 2376 didn't jump to line 2377

2377 msg = ( 

2378 "Line {i} in new value was invalid. It must either start" 

2379 ' with " " space (continuation line) or "#" (comment line).' 

2380 ' The line started with "{line}"' 

2381 ) 

2382 raise ValueError(msg.format(i=i, line=line[0])) 

2383 if len(raw_lines) > 1 and raw_lines[-1].startswith("#"): 2383 ↛ 2384line 2383 didn't jump to line 2384, because the condition on line 2383 was never true

2384 raise ValueError("The last line in a value field cannot be a comment") 

2385 new_content.extend(raw_lines) 

2386 # As absurd as it might seem, it is easier to just use the parser to 

2387 # construct the AST correctly 

2388 deb822_file = parse_deb822_file(iter(new_content)) 

2389 error_token = deb822_file.find_first_error_element() 

2390 if error_token: 2390 ↛ 2391line 2390 didn't jump to line 2391, because the condition on line 2390 was never true

2391 raise ValueError("Syntax error in new field value for " + field_name) 

2392 paragraph = next(iter(deb822_file)) 

2393 assert isinstance(paragraph, Deb822NoDuplicateFieldsParagraphElement) 

2394 value = paragraph.get_kvpair_element(field_name) 

2395 assert value is not None 

2396 if preserve_original_field_comment: 

2397 if original: 

2398 value.comment_element = original.comment_element 

2399 original.comment_element = None 

2400 elif field_comment is not None: 2400 ↛ 2401line 2400 didn't jump to line 2401, because the condition on line 2400 was never true

2401 value.comment_element = field_comment 

2402 self.set_kvpair_element(item, value) 

2403 

2404 @overload 

2405 def dump( 

2406 self, fd # type: IO[bytes] 

2407 ): 

2408 # type: (...) -> None 

2409 pass 

2410 

2411 @overload 

2412 def dump(self): 

2413 # type: () -> str 

2414 pass 

2415 

2416 def dump( 

2417 self, fd=None # type: Optional[IO[bytes]] 

2418 ): 

2419 # type: (...) -> Optional[str] 

2420 if fd is None: 2420 ↛ 2422line 2420 didn't jump to line 2422, because the condition on line 2420 was never false

2421 return "".join(t.text for t in self.iter_tokens()) 

2422 for token in self.iter_tokens(): 

2423 fd.write(token.text.encode("utf-8")) 

2424 return None 

2425 

2426 

2427class Deb822NoDuplicateFieldsParagraphElement(Deb822ParagraphElement): 

2428 """Paragraph implementation optimized for valid deb822 files 

2429 

2430 When there are no duplicated fields, we can use simpler and faster 

2431 datastructures for common operations. 

2432 """ 

2433 

2434 def __init__( 

2435 self, 

2436 kvpair_elements, # type: List[Deb822KeyValuePairElement] 

2437 kvpair_order, # type: OrderedSet 

2438 ): 

2439 # type: (...) -> None 

2440 super().__init__() 

2441 self._kvpair_elements = {kv.field_name: kv for kv in kvpair_elements} 

2442 self._kvpair_order = kvpair_order 

2443 self._init_parent_of_parts() 

2444 

2445 @property 

2446 def kvpair_count(self): 

2447 # type: () -> int 

2448 return len(self._kvpair_elements) 

2449 

2450 def order_last(self, field): 

2451 # type: (ParagraphKey) -> None 

2452 """Re-order the given field so it is "last" in the paragraph""" 

2453 unpacked_field, _, _ = _unpack_key(field, raise_if_indexed=True) 

2454 self._kvpair_order.order_last(unpacked_field) 

2455 

2456 def order_first(self, field): 

2457 # type: (ParagraphKey) -> None 

2458 """Re-order the given field so it is "first" in the paragraph""" 

2459 unpacked_field, _, _ = _unpack_key(field, raise_if_indexed=True) 

2460 self._kvpair_order.order_first(unpacked_field) 

2461 

2462 def order_before(self, field, reference_field): 

2463 # type: (ParagraphKey, ParagraphKey) -> None 

2464 """Re-order the given field so appears directly after the reference field in the paragraph 

2465 

2466 The reference field must be present.""" 

2467 unpacked_field, _, _ = _unpack_key(field, raise_if_indexed=True) 

2468 unpacked_ref_field, _, _ = _unpack_key(reference_field, raise_if_indexed=True) 

2469 self._kvpair_order.order_before(unpacked_field, unpacked_ref_field) 

2470 

2471 def order_after(self, field, reference_field): 

2472 # type: (ParagraphKey, ParagraphKey) -> None 

2473 """Re-order the given field so appears directly before the reference field in the paragraph 

2474 

2475 The reference field must be present. 

2476 """ 

2477 unpacked_field, _, _ = _unpack_key(field, raise_if_indexed=True) 

2478 unpacked_ref_field, _, _ = _unpack_key(reference_field, raise_if_indexed=True) 

2479 self._kvpair_order.order_after(unpacked_field, unpacked_ref_field) 

2480 

2481 # Overload to narrow the type to just str. 

2482 def __iter__(self): 

2483 # type: () -> Iterator[str] 

2484 return iter(str(k) for k in self._kvpair_order) 

2485 

2486 def iter_keys(self): 

2487 # type: () -> Iterable[str] 

2488 yield from (str(k) for k in self._kvpair_order) 

2489 

2490 def remove_kvpair_element(self, key): 

2491 # type: (ParagraphKey) -> None 

2492 self._full_size_cache = None 

2493 key, _, _ = _unpack_key(key, raise_if_indexed=True) 

2494 del self._kvpair_elements[key] 

2495 self._kvpair_order.remove(key) 

2496 

2497 def contains_kvpair_element(self, item): 

2498 # type: (object) -> bool 

2499 if not isinstance(item, (str, tuple, Deb822FieldNameToken)): 2499 ↛ 2500line 2499 didn't jump to line 2500, because the condition on line 2499 was never true

2500 return False 

2501 item = cast("ParagraphKey", item) 

2502 key, _, _ = _unpack_key(item, raise_if_indexed=True) 

2503 return key in self._kvpair_elements 

2504 

2505 def get_kvpair_element( 

2506 self, 

2507 item, # type: ParagraphKey 

2508 use_get=False, # type: bool 

2509 ): 

2510 # type: (...) -> Optional[Deb822KeyValuePairElement] 

2511 item, _, _ = _unpack_key(item, raise_if_indexed=True) 

2512 if use_get: 

2513 return self._kvpair_elements.get(item) 

2514 return self._kvpair_elements[item] 

2515 

2516 def set_kvpair_element(self, key, value): 

2517 # type: (ParagraphKey, Deb822KeyValuePairElement) -> None 

2518 key, _, _ = _unpack_key(key, raise_if_indexed=True) 

2519 if isinstance(key, Deb822FieldNameToken): 2519 ↛ 2520line 2519 didn't jump to line 2520, because the condition on line 2519 was never true

2520 if key is not value.field_token: 

2521 raise ValueError( 

2522 "Key is a Deb822FieldNameToken, but not *the* Deb822FieldNameToken" 

2523 " for the value" 

2524 ) 

2525 key = value.field_name 

2526 else: 

2527 if key != value.field_name: 2527 ↛ 2528line 2527 didn't jump to line 2528, because the condition on line 2527 was never true

2528 raise ValueError( 

2529 "Cannot insert value under a different field value than field name" 

2530 " from its Deb822FieldNameToken implies" 

2531 ) 

2532 # Use the string from the Deb822FieldNameToken as we need to keep that in memory either 

2533 # way 

2534 key = value.field_name 

2535 original_value = self._kvpair_elements.get(key) 

2536 self._full_size_cache = None 

2537 self._kvpair_elements[key] = value 

2538 self._kvpair_order.append(key) 

2539 if original_value is not None: 

2540 original_value.parent_element = None 

2541 value.parent_element = self 

2542 

2543 def sort_fields(self, key=None): 

2544 # type: (Optional[Callable[[str], Any]]) -> None 

2545 """Re-order all fields 

2546 

2547 :param key: Provide a key function (same semantics as for sorted). Keep in mind that 

2548 the module preserve the cases for field names - in generally, callers are recommended 

2549 to use "lower()" to normalize the case. 

2550 """ 

2551 for last_field_name in reversed(self._kvpair_order): 

2552 last_kvpair = self._kvpair_elements[cast("_strI", last_field_name)] 

2553 if last_kvpair.value_element.add_final_newline_if_missing(): 

2554 self._full_size_cache = None 

2555 break 

2556 

2557 if key is None: 

2558 key = default_field_sort_key 

2559 

2560 self._kvpair_order = OrderedSet(sorted(self._kvpair_order, key=key)) 

2561 

2562 def iter_parts(self): 

2563 # type: () -> Iterable[TokenOrElement] 

2564 yield from ( 

2565 self._kvpair_elements[x] 

2566 for x in cast("Iterable[_strI]", self._kvpair_order) 

2567 ) 

2568 

2569 

2570class Deb822DuplicateFieldsParagraphElement(Deb822ParagraphElement): 

2571 

2572 def __init__(self, kvpair_elements): 

2573 # type: (List[Deb822KeyValuePairElement]) -> None 

2574 super().__init__() 

2575 self._kvpair_order = LinkedList() # type: LinkedList[Deb822KeyValuePairElement] 

2576 self._kvpair_elements = {} # type: Dict[_strI, List[KVPNode]] 

2577 self._init_kvpair_fields(kvpair_elements) 

2578 self._init_parent_of_parts() 

2579 

2580 @property 

2581 def has_duplicate_fields(self): 

2582 # type: () -> bool 

2583 # Most likely, the answer is "True" but if the caller "fixes" the problem 

2584 # then this can return "False" 

2585 return len(self._kvpair_order) > len(self._kvpair_elements) 

2586 

2587 def _init_kvpair_fields(self, kvpairs): 

2588 # type: (Iterable[Deb822KeyValuePairElement]) -> None 

2589 assert not self._kvpair_order 

2590 assert not self._kvpair_elements 

2591 for kv in kvpairs: 

2592 field_name = kv.field_name 

2593 node = self._kvpair_order.append(kv) 

2594 if field_name not in self._kvpair_elements: 

2595 self._kvpair_elements[field_name] = [node] 

2596 else: 

2597 self._kvpair_elements[field_name].append(node) 

2598 

2599 def _nodes_being_relocated(self, field): 

2600 # type: (ParagraphKey) -> Tuple[List[KVPNode], List[KVPNode]] 

2601 key, index, name_token = _unpack_key(field) 

2602 nodes = self._kvpair_elements[key] 

2603 nodes_being_relocated = [] 

2604 

2605 if name_token is not None or index is not None: 

2606 single_node = self._resolve_to_single_node(nodes, key, index, name_token) 

2607 assert single_node is not None 

2608 nodes_being_relocated.append(single_node) 

2609 else: 

2610 nodes_being_relocated = nodes 

2611 return nodes, nodes_being_relocated 

2612 

2613 def order_last(self, field): 

2614 # type: (ParagraphKey) -> None 

2615 """Re-order the given field so it is "last" in the paragraph""" 

2616 nodes, nodes_being_relocated = self._nodes_being_relocated(field) 

2617 assert len(nodes_being_relocated) == 1 or len(nodes) == len( 

2618 nodes_being_relocated 

2619 ) 

2620 

2621 kvpair_order = self._kvpair_order 

2622 for node in nodes_being_relocated: 

2623 if kvpair_order.tail_node is node: 

2624 # Special case for relocating a single node that happens to be the last. 

2625 continue 

2626 kvpair_order.remove_node(node) 

2627 # assertion for mypy 

2628 assert kvpair_order.tail_node is not None 

2629 kvpair_order.insert_node_after(node, kvpair_order.tail_node) 

2630 

2631 if ( 

2632 len(nodes_being_relocated) == 1 

2633 and nodes_being_relocated[0] is not nodes[-1] 

2634 ): 

2635 single_node = nodes_being_relocated[0] 

2636 nodes.remove(single_node) 

2637 nodes.append(single_node) 

2638 

2639 def order_first(self, field): 

2640 # type: (ParagraphKey) -> None 

2641 """Re-order the given field so it is "first" in the paragraph""" 

2642 nodes, nodes_being_relocated = self._nodes_being_relocated(field) 

2643 assert len(nodes_being_relocated) == 1 or len(nodes) == len( 

2644 nodes_being_relocated 

2645 ) 

2646 

2647 kvpair_order = self._kvpair_order 

2648 for node in nodes_being_relocated: 

2649 if kvpair_order.head_node is node: 

2650 # Special case for relocating a single node that happens to be the first. 

2651 continue 

2652 kvpair_order.remove_node(node) 

2653 # assertion for mypy 

2654 assert kvpair_order.head_node is not None 

2655 kvpair_order.insert_node_before(node, kvpair_order.head_node) 

2656 

2657 if len(nodes_being_relocated) == 1 and nodes_being_relocated[0] is not nodes[0]: 

2658 single_node = nodes_being_relocated[0] 

2659 nodes.remove(single_node) 

2660 nodes.insert(0, single_node) 

2661 

2662 def order_before(self, field, reference_field): 

2663 # type: (ParagraphKey, ParagraphKey) -> None 

2664 """Re-order the given field so appears directly after the reference field in the paragraph 

2665 

2666 The reference field must be present.""" 

2667 nodes, nodes_being_relocated = self._nodes_being_relocated(field) 

2668 assert len(nodes_being_relocated) == 1 or len(nodes) == len( 

2669 nodes_being_relocated 

2670 ) 

2671 # For "before" we always use the "first" variant as reference in case of doubt 

2672 _, reference_nodes = self._nodes_being_relocated(reference_field) 

2673 reference_node = reference_nodes[0] 

2674 if reference_node in nodes_being_relocated: 

2675 raise ValueError("Cannot re-order a field relative to itself") 

2676 

2677 kvpair_order = self._kvpair_order 

2678 for node in nodes_being_relocated: 

2679 kvpair_order.remove_node(node) 

2680 kvpair_order.insert_node_before(node, reference_node) 

2681 

2682 if len(nodes_being_relocated) == 1 and len(nodes) > 1: 

2683 # Regenerate the (new) relative field order. 

2684 field_name = nodes_being_relocated[0].value.field_name 

2685 self._regenerate_relative_kvapir_order(field_name) 

2686 

2687 def order_after(self, field, reference_field): 

2688 # type: (ParagraphKey, ParagraphKey) -> None 

2689 """Re-order the given field so appears directly before the reference field in the paragraph 

2690 

2691 The reference field must be present. 

2692 """ 

2693 nodes, nodes_being_relocated = self._nodes_being_relocated(field) 

2694 assert len(nodes_being_relocated) == 1 or len(nodes) == len( 

2695 nodes_being_relocated 

2696 ) 

2697 _, reference_nodes = self._nodes_being_relocated(reference_field) 

2698 # For "after" we always use the "last" variant as reference in case of doubt 

2699 reference_node = reference_nodes[-1] 

2700 if reference_node in nodes_being_relocated: 

2701 raise ValueError("Cannot re-order a field relative to itself") 

2702 

2703 kvpair_order = self._kvpair_order 

2704 # Use "reversed" to preserve the relative order of the nodes assuming a bulk reorder 

2705 for node in reversed(nodes_being_relocated): 

2706 kvpair_order.remove_node(node) 

2707 kvpair_order.insert_node_after(node, reference_node) 

2708 

2709 if len(nodes_being_relocated) == 1 and len(nodes) > 1: 

2710 # Regenerate the (new) relative field order. 

2711 field_name = nodes_being_relocated[0].value.field_name 

2712 self._regenerate_relative_kvapir_order(field_name) 

2713 

2714 def _regenerate_relative_kvapir_order(self, field_name): 

2715 # type: (_strI) -> None 

2716 nodes = [] 

2717 for node in self._kvpair_order.iter_nodes(): 

2718 if node.value.field_name == field_name: 

2719 nodes.append(node) 

2720 self._kvpair_elements[field_name] = nodes 

2721 

2722 def iter_parts(self): 

2723 # type: () -> Iterable[TokenOrElement] 

2724 yield from self._kvpair_order 

2725 

2726 @property 

2727 def kvpair_count(self): 

2728 # type: () -> int 

2729 return len(self._kvpair_order) 

2730 

2731 def iter_keys(self): 

2732 # type: () -> Iterable[ParagraphKey] 

2733 yield from (kv.field_name for kv in self._kvpair_order) 

2734 

2735 def _resolve_to_single_node( 

2736 self, 

2737 nodes, # type: List[KVPNode] 

2738 key, # type: str 

2739 index, # type: Optional[int] 

2740 name_token, # type: Optional[Deb822FieldNameToken] 

2741 use_get=False, # type: bool 

2742 ): 

2743 # type: (...) -> Optional[KVPNode] 

2744 if index is None: 

2745 if len(nodes) != 1: 

2746 if name_token is not None: 

2747 node = self._find_node_via_name_token(name_token, nodes) 

2748 if node is not None: 

2749 return node 

2750 msg = ( 

2751 "Ambiguous key {key} - the field appears {res_len} times. Use" 

2752 " ({key}, index) to denote which instance of the field you want. (Index" 

2753 " can be 0..{res_len_1} or e.g. -1 to denote the last field)" 

2754 ) 

2755 raise AmbiguousDeb822FieldKeyError( 

2756 msg.format(key=key, res_len=len(nodes), res_len_1=len(nodes) - 1) 

2757 ) 

2758 index = 0 

2759 try: 

2760 return nodes[index] 

2761 except IndexError: 

2762 if use_get: 

2763 return None 

2764 msg = 'Field "{key}" was present but the index "{index}" was invalid.' 

2765 raise KeyError(msg.format(key=key, index=index)) 

2766 

2767 def get_kvpair_element( 

2768 self, 

2769 item, # type: ParagraphKey 

2770 use_get=False, # type: bool 

2771 ): 

2772 # type: (...) -> Optional[Deb822KeyValuePairElement] 

2773 key, index, name_token = _unpack_key(item) 

2774 if use_get: 

2775 nodes = self._kvpair_elements.get(key) 

2776 if nodes is None: 

2777 return None 

2778 else: 

2779 nodes = self._kvpair_elements[key] 

2780 node = self._resolve_to_single_node( 

2781 nodes, key, index, name_token, use_get=use_get 

2782 ) 

2783 if node is not None: 

2784 return node.value 

2785 return None 

2786 

2787 @staticmethod 

2788 def _find_node_via_name_token( 

2789 name_token, # type: Deb822FieldNameToken 

2790 elements, # type: Iterable[KVPNode] 

2791 ): 

2792 # type: (...) -> Optional[KVPNode] 

2793 # if we are given a name token, then it is non-ambiguous if we have exactly 

2794 # that name token in our list of nodes. It will be an O(n) lookup but we 

2795 # probably do not have that many duplicate fields (and even if do, it is not 

2796 # exactly a valid file, so there little reason to optimize for it) 

2797 for node in elements: 

2798 if name_token is node.value.field_token: 

2799 return node 

2800 return None 

2801 

2802 def contains_kvpair_element(self, item): 

2803 # type: (object) -> bool 

2804 if not isinstance(item, (str, tuple, Deb822FieldNameToken)): 

2805 return False 

2806 item = cast("ParagraphKey", item) 

2807 try: 

2808 return self.get_kvpair_element(item, use_get=True) is not None 

2809 except AmbiguousDeb822FieldKeyError: 

2810 return True 

2811 

2812 def set_kvpair_element(self, key, value): 

2813 # type: (ParagraphKey, Deb822KeyValuePairElement) -> None 

2814 key, index, name_token = _unpack_key(key) 

2815 if name_token: 

2816 if name_token is not value.field_token: 

2817 original_nodes = self._kvpair_elements.get(value.field_name) 

2818 original_node = None 

2819 if original_nodes is not None: 

2820 original_node = self._find_node_via_name_token( 

2821 name_token, original_nodes 

2822 ) 

2823 

2824 if original_node is None: 

2825 raise ValueError( 

2826 "Key is a Deb822FieldNameToken, but not *the*" 

2827 " Deb822FieldNameToken for the value nor the" 

2828 " Deb822FieldNameToken for an existing field in the paragraph" 

2829 ) 

2830 # Primarily for mypy's sake 

2831 assert original_nodes is not None 

2832 # Rely on the index-based code below to handle update. 

2833 index = original_nodes.index(original_node) 

2834 key = value.field_name 

2835 else: 

2836 if key != value.field_name: 

2837 raise ValueError( 

2838 "Cannot insert value under a different field value than field name" 

2839 " from its Deb822FieldNameToken implies" 

2840 ) 

2841 # Use the string from the Deb822FieldNameToken as it is a _strI and has the same value 

2842 # (memory optimization) 

2843 key = value.field_name 

2844 self._full_size_cache = None 

2845 original_nodes = self._kvpair_elements.get(key) 

2846 if original_nodes is None or not original_nodes: 

2847 if index is not None and index != 0: 

2848 msg = ( 

2849 "Cannot replace field ({key}, {index}) as the field does not exist" 

2850 " in the first place. Please index-less key or ({key}, 0) if you" 

2851 " want to add the field." 

2852 ) 

2853 raise KeyError(msg.format(key=key, index=index)) 

2854 node = self._kvpair_order.append(value) 

2855 if key not in self._kvpair_elements: 

2856 self._kvpair_elements[key] = [node] 

2857 else: 

2858 self._kvpair_elements[key].append(node) 

2859 return 

2860 

2861 replace_all = False 

2862 if index is None: 

2863 replace_all = True 

2864 node = original_nodes[0] 

2865 if len(original_nodes) != 1: 

2866 self._kvpair_elements[key] = [node] 

2867 else: 

2868 # We insist on there being an original node, which as a side effect ensures 

2869 # you cannot add additional copies of the field. This means that you cannot 

2870 # make the problem worse. 

2871 node = original_nodes[index] 

2872 

2873 # Replace the value of the existing node plus do a little dance 

2874 # for the parent element part. 

2875 node.value.parent_element = None 

2876 value.parent_element = self 

2877 node.value = value 

2878 

2879 if replace_all and len(original_nodes) != 1: 

2880 # If we were in a replace-all mode, discard any remaining nodes 

2881 for n in original_nodes[1:]: 

2882 n.value.parent_element = None 

2883 self._kvpair_order.remove_node(n) 

2884 

2885 def remove_kvpair_element(self, key): 

2886 # type: (ParagraphKey) -> None 

2887 key, idx, name_token = _unpack_key(key) 

2888 field_list = self._kvpair_elements[key] 

2889 

2890 if name_token is None and idx is None: 

2891 self._full_size_cache = None 

2892 # Remove all case 

2893 for node in field_list: 

2894 node.value.parent_element = None 

2895 self._kvpair_order.remove_node(node) 

2896 del self._kvpair_elements[key] 

2897 return 

2898 

2899 if name_token is not None: 

2900 # Indirection between original_node and node for mypy's sake 

2901 original_node = self._find_node_via_name_token(name_token, field_list) 

2902 if original_node is None: 

2903 msg = 'The field "{key}" is present but key used to access it is not.' 

2904 raise KeyError(msg.format(key=key)) 

2905 node = original_node 

2906 else: 

2907 assert idx is not None 

2908 try: 

2909 node = field_list[idx] 

2910 except KeyError: 

2911 msg = 'The field "{key}" is present, but the index "{idx}" was invalid.' 

2912 raise KeyError(msg.format(key=key, idx=idx)) 

2913 

2914 self._full_size_cache = None 

2915 if len(field_list) == 1: 

2916 del self._kvpair_elements[key] 

2917 else: 

2918 field_list.remove(node) 

2919 node.value.parent_element = None 

2920 self._kvpair_order.remove_node(node) 

2921 

2922 def sort_fields(self, key=None): 

2923 # type: (Optional[Callable[[str], Any]]) -> None 

2924 """Re-order all fields 

2925 

2926 :param key: Provide a key function (same semantics as for sorted). Keep in mind that 

2927 the module preserve the cases for field names - in generally, callers are recommended 

2928 to use "lower()" to normalize the case. 

2929 """ 

2930 

2931 if key is None: 

2932 key = default_field_sort_key 

2933 

2934 # Work around mypy that cannot seem to shred the Optional notion 

2935 # without this little indirection 

2936 key_impl = key 

2937 

2938 def _actual_key(kvpair): 

2939 # type: (Deb822KeyValuePairElement) -> Any 

2940 return key_impl(kvpair.field_name) 

2941 

2942 for last_kvpair in reversed(self._kvpair_order): 

2943 if last_kvpair.value_element.add_final_newline_if_missing(): 

2944 self._full_size_cache = None 

2945 break 

2946 

2947 sorted_kvpair_list = sorted(self._kvpair_order, key=_actual_key) 

2948 self._kvpair_order = LinkedList() 

2949 self._kvpair_elements = {} 

2950 self._init_kvpair_fields(sorted_kvpair_list) 

2951 

2952 

2953class Deb822FileElement(Deb822Element): 

2954 """Represents the entire deb822 file""" 

2955 

2956 def __init__(self, token_and_elements): 

2957 # type: (LinkedList[TokenOrElement]) -> None 

2958 super().__init__() 

2959 self._token_and_elements = token_and_elements 

2960 self._init_parent_of_parts() 

2961 

2962 @classmethod 

2963 def new_empty_file(cls): 

2964 # type: () -> Deb822FileElement 

2965 """Creates a new Deb822FileElement with no contents 

2966 

2967 Note that a deb822 file must be non-empty to be considered valid 

2968 """ 

2969 return cls(LinkedList()) 

2970 

2971 @property 

2972 def is_valid_file(self): 

2973 # type: () -> bool 

2974 """Returns true if the file is valid 

2975 

2976 Invalid elements include error elements (Deb822ErrorElement) but also 

2977 issues such as paragraphs with duplicate fields or "empty" files 

2978 (a valid deb822 file contains at least one paragraph). 

2979 """ 

2980 had_paragraph = False 

2981 for paragraph in self: 

2982 had_paragraph = True 

2983 if not paragraph or paragraph.has_duplicate_fields: 

2984 return False 

2985 

2986 if not had_paragraph: 

2987 return False 

2988 

2989 return self.find_first_error_element() is None 

2990 

2991 def find_first_error_element(self): 

2992 # type: () -> Optional[Deb822ErrorElement] 

2993 """Returns the first Deb822ErrorElement (or None) in the file""" 

2994 return next( 

2995 iter(self.iter_recurse(only_element_or_token_type=Deb822ErrorElement)), None 

2996 ) 

2997 

2998 def __iter__(self): 

2999 # type: () -> Iterator[Deb822ParagraphElement] 

3000 return iter(self.iter_parts_of_type(Deb822ParagraphElement)) 

3001 

3002 def iter_parts(self): 

3003 # type: () -> Iterable[TokenOrElement] 

3004 yield from self._token_and_elements 

3005 

3006 def insert(self, idx, para): 

3007 # type: (int, Deb822ParagraphElement) -> None 

3008 """Inserts a paragraph into the file at the given "index" of paragraphs 

3009 

3010 Note that if the index is between two paragraphs containing a "free 

3011 floating" comment (e.g. paragraph/start-of-file, empty line, comment, 

3012 empty line, paragraph) then it is unspecified which "side" of the 

3013 comment the new paragraph will appear and this may change between 

3014 versions of python-debian. 

3015 

3016 

3017 >>> original = ''' 

3018 ... Package: libfoo-dev 

3019 ... Depends: libfoo1 (= ${binary:Version}), ${shlib:Depends}, ${misc:Depends} 

3020 ... '''.lstrip() 

3021 >>> deb822_file = parse_deb822_file(original.splitlines()) 

3022 >>> para1 = Deb822ParagraphElement.new_empty_paragraph() 

3023 >>> para1["Source"] = "foo" 

3024 >>> para1["Build-Depends"] = "debhelper-compat (= 13)" 

3025 >>> para2 = Deb822ParagraphElement.new_empty_paragraph() 

3026 >>> para2["Package"] = "libfoo1" 

3027 >>> para2["Depends"] = "${shlib:Depends}, ${misc:Depends}" 

3028 >>> deb822_file.insert(0, para1) 

3029 >>> deb822_file.insert(1, para2) 

3030 >>> expected = ''' 

3031 ... Source: foo 

3032 ... Build-Depends: debhelper-compat (= 13) 

3033 ... 

3034 ... Package: libfoo1 

3035 ... Depends: ${shlib:Depends}, ${misc:Depends} 

3036 ... 

3037 ... Package: libfoo-dev 

3038 ... Depends: libfoo1 (= ${binary:Version}), ${shlib:Depends}, ${misc:Depends} 

3039 ... '''.lstrip() 

3040 >>> deb822_file.dump() == expected 

3041 True 

3042 """ 

3043 

3044 anchor_node = None 

3045 needs_newline = True 

3046 self._full_size_cache = None 

3047 if idx == 0: 

3048 # Special-case, if idx is 0, then we insert it before everything else. 

3049 # This is mostly a cosmetic choice for corner cases involving free-floating 

3050 # comments in the file. 

3051 if not self._token_and_elements: 3051 ↛ 3052line 3051 didn't jump to line 3052, because the condition on line 3051 was never true

3052 self.append(para) 

3053 return 

3054 anchor_node = self._token_and_elements.head_node 

3055 needs_newline = bool(self._token_and_elements) 

3056 else: 

3057 i = 0 

3058 for node in self._token_and_elements.iter_nodes(): 3058 ↛ 3066line 3058 didn't jump to line 3066, because the loop on line 3058 didn't complete

3059 entry = node.value 

3060 if isinstance(entry, Deb822ParagraphElement): 

3061 i += 1 

3062 if idx == i - 1: 

3063 anchor_node = node 

3064 break 

3065 

3066 if anchor_node is None: 3066 ↛ 3068line 3066 didn't jump to line 3068, because the condition on line 3066 was never true

3067 # Empty list or idx after the last paragraph both degenerate into append 

3068 self.append(para) 

3069 else: 

3070 if needs_newline: 3070 ↛ 3076line 3070 didn't jump to line 3076, because the condition on line 3070 was never false

3071 # Remember to inject the "separating" newline between two paragraphs 

3072 nl_token = self._set_parent(Deb822WhitespaceToken("\n")) 

3073 anchor_node = self._token_and_elements.insert_before( 

3074 nl_token, anchor_node 

3075 ) 

3076 self._token_and_elements.insert_before(self._set_parent(para), anchor_node) 

3077 

3078 def append(self, paragraph): 

3079 # type: (Deb822ParagraphElement) -> None 

3080 """Appends a paragraph to the file 

3081 

3082 >>> deb822_file = Deb822FileElement.new_empty_file() 

3083 >>> para1 = Deb822ParagraphElement.new_empty_paragraph() 

3084 >>> para1["Source"] = "foo" 

3085 >>> para1["Build-Depends"] = "debhelper-compat (= 13)" 

3086 >>> para2 = Deb822ParagraphElement.new_empty_paragraph() 

3087 >>> para2["Package"] = "foo" 

3088 >>> para2["Depends"] = "${shlib:Depends}, ${misc:Depends}" 

3089 >>> deb822_file.append(para1) 

3090 >>> deb822_file.append(para2) 

3091 >>> expected = ''' 

3092 ... Source: foo 

3093 ... Build-Depends: debhelper-compat (= 13) 

3094 ... 

3095 ... Package: foo 

3096 ... Depends: ${shlib:Depends}, ${misc:Depends} 

3097 ... '''.lstrip() 

3098 >>> deb822_file.dump() == expected 

3099 True 

3100 """ 

3101 tail_element = self._token_and_elements.tail 

3102 if paragraph.parent_element is not None: 3102 ↛ 3103line 3102 didn't jump to line 3103, because the condition on line 3102 was never true

3103 if paragraph.parent_element is self: 

3104 raise ValueError("Paragraph is already a part of this file") 

3105 raise ValueError("Paragraph is already part of another Deb822File") 

3106 

3107 self._full_size_cache = None 

3108 # We need a separating newline if there is not a whitespace token at the end of the file. 

3109 # Note the special case where the file ends on a comment; here we insert a whitespace too 

3110 # to be sure. Otherwise, we would have to check that there is an empty line before that 

3111 # comment and that is too much effort. 

3112 if tail_element and not isinstance(tail_element, Deb822WhitespaceToken): 

3113 self._token_and_elements.append( 

3114 self._set_parent(Deb822WhitespaceToken("\n")) 

3115 ) 

3116 self._token_and_elements.append(self._set_parent(paragraph)) 

3117 

3118 def remove(self, paragraph): 

3119 # type: (Deb822ParagraphElement) -> None 

3120 if paragraph.parent_element is not self: 

3121 raise ValueError("Paragraph is part of a different file") 

3122 node = None 

3123 for node in self._token_and_elements.iter_nodes(): 

3124 if node.value is paragraph: 

3125 break 

3126 if node is None: 

3127 raise RuntimeError("unable to find paragraph") 

3128 self._full_size_cache = None 

3129 previous_node = node.previous_node 

3130 next_node = node.next_node 

3131 self._token_and_elements.remove_node(node) 

3132 if next_node is None: 

3133 if previous_node and isinstance(previous_node.value, Deb822WhitespaceToken): 

3134 self._token_and_elements.remove_node(previous_node) 

3135 else: 

3136 if isinstance(next_node.value, Deb822WhitespaceToken): 

3137 self._token_and_elements.remove_node(next_node) 

3138 paragraph.parent_element = None 

3139 

3140 def _set_parent(self, t): 

3141 # type: (TE) -> TE 

3142 t.parent_element = self 

3143 return t 

3144 

3145 def position_in_parent(self, *, skip_leading_comments: bool = True) -> Position: 

3146 # Recursive base-case 

3147 return START_POSITION 

3148 

3149 def position_in_file(self, *, skip_leading_comments: bool = True) -> Position: 

3150 # By definition 

3151 return START_POSITION 

3152 

3153 @overload 

3154 def dump( 

3155 self, fd # type: IO[bytes] 

3156 ): 

3157 # type: (...) -> None 

3158 pass 

3159 

3160 @overload 

3161 def dump(self): 

3162 # type: () -> str 

3163 pass 

3164 

3165 def dump( 

3166 self, fd=None # type: Optional[IO[bytes]] 

3167 ): 

3168 # type: (...) -> Optional[str] 

3169 if fd is None: 3169 ↛ 3171line 3169 didn't jump to line 3171, because the condition on line 3169 was never false

3170 return "".join(t.text for t in self.iter_tokens()) 

3171 for token in self.iter_tokens(): 

3172 fd.write(token.text.encode("utf-8")) 

3173 return None 

3174 

3175 

3176_combine_error_tokens_into_elements = combine_into_replacement( 

3177 Deb822ErrorToken, Deb822ErrorElement 

3178) 

3179_combine_comment_tokens_into_elements = combine_into_replacement( 

3180 Deb822CommentToken, Deb822CommentElement 

3181) 

3182_combine_vl_elements_into_value_elements = combine_into_replacement( 

3183 Deb822ValueLineElement, Deb822ValueElement 

3184) 

3185_combine_kvp_elements_into_paragraphs = combine_into_replacement( 

3186 Deb822KeyValuePairElement, 

3187 Deb822ParagraphElement, 

3188 constructor=Deb822ParagraphElement.from_kvpairs, 

3189) 

3190 

3191 

3192def _parsed_value_render_factory(discard_comments): 

3193 # type: (bool) -> Callable[[Deb822ParsedValueElement], str] 

3194 return ( 

3195 Deb822ParsedValueElement.convert_to_text_without_comments 

3196 if discard_comments 

3197 else Deb822ParsedValueElement.convert_to_text 

3198 ) 

3199 

3200 

3201LIST_SPACE_SEPARATED_INTERPRETATION = ListInterpretation( 

3202 whitespace_split_tokenizer, 

3203 _parse_whitespace_list_value, 

3204 Deb822ParsedValueElement, 

3205 Deb822SemanticallySignificantWhiteSpace, 

3206 lambda: Deb822SpaceSeparatorToken(" "), 

3207 _parsed_value_render_factory, 

3208) 

3209LIST_COMMA_SEPARATED_INTERPRETATION = ListInterpretation( 

3210 comma_split_tokenizer, 

3211 _parse_comma_list_value, 

3212 Deb822ParsedValueElement, 

3213 Deb822CommaToken, 

3214 Deb822CommaToken, 

3215 _parsed_value_render_factory, 

3216) 

3217LIST_UPLOADERS_INTERPRETATION = ListInterpretation( 

3218 comma_split_tokenizer, 

3219 _parse_uploaders_list_value, 

3220 Deb822ParsedValueElement, 

3221 Deb822CommaToken, 

3222 Deb822CommaToken, 

3223 _parsed_value_render_factory, 

3224) 

3225 

3226 

3227def _non_end_of_line_token(v): 

3228 # type: (TokenOrElement) -> bool 

3229 # Consume tokens until the newline 

3230 return not isinstance(v, Deb822WhitespaceToken) or v.text != "\n" 

3231 

3232 

3233def _build_value_line( 

3234 token_stream, # type: Iterable[Union[TokenOrElement, Deb822CommentElement]] 

3235): 

3236 # type: (...) -> Iterable[Union[TokenOrElement, Deb822ValueLineElement]] 

3237 """Parser helper - consumes tokens part of a Deb822ValueEntryElement and turns them into one""" 

3238 buffered_stream = BufferingIterator(token_stream) 

3239 

3240 # Deb822ValueLineElement is a bit tricky because of how we handle whitespace 

3241 # and comments. 

3242 # 

3243 # In relation to comments, then only continuation lines can have comments. 

3244 # If there is a comment before a "K: V" line, then the comment is associated 

3245 # with the field rather than the value. 

3246 # 

3247 # On the whitespace front, then we separate syntactical mandatory whitespace 

3248 # from optional whitespace. As an example: 

3249 # 

3250 # """ 

3251 # # some comment associated with the Depends field 

3252 # Depends:_foo_$ 

3253 # # some comment associated with the line containing "bar" 

3254 # !________bar_$ 

3255 # """ 

3256 # 

3257 # Where "$" and "!" represents mandatory whitespace (the newline and the first 

3258 # space are required for the file to be parsed correctly), where as "_" is 

3259 # "optional" whitespace (from a syntactical point of view). 

3260 # 

3261 # This distinction enable us to facilitate APIs for easy removal/normalization 

3262 # of redundant whitespaces without having programmers worry about trashing 

3263 # the file. 

3264 # 

3265 # 

3266 

3267 comment_element = None 

3268 continuation_line_token = None 

3269 token = None # type: Optional[TokenOrElement] 

3270 

3271 for token in buffered_stream: 

3272 start_of_value_entry = False 

3273 if isinstance(token, Deb822ValueContinuationToken): 

3274 continuation_line_token = token 

3275 start_of_value_entry = True 

3276 token = None 

3277 elif isinstance(token, Deb822FieldSeparatorToken): 

3278 start_of_value_entry = True 

3279 elif isinstance(token, Deb822CommentElement): 

3280 next_token = buffered_stream.peek() 

3281 # If the next token is a continuation line token, then this comment 

3282 # belong to a value and we might as well just start the value 

3283 # parsing now. 

3284 # 

3285 # Note that we rely on this behaviour to avoid emitting the comment 

3286 # token (failing to do so would cause the comment to appear twice 

3287 # in the file). 

3288 if isinstance(next_token, Deb822ValueContinuationToken): 

3289 start_of_value_entry = True 

3290 comment_element = token 

3291 token = None 

3292 # Use next with None to avoid raising StopIteration inside a generator 

3293 # It won't happen, but pylint cannot see that, so we do this instead. 

3294 continuation_line_token = cast( 

3295 "Deb822ValueContinuationToken", next(buffered_stream, None) 

3296 ) 

3297 assert continuation_line_token is not None 

3298 

3299 if token is not None: 

3300 yield token 

3301 if start_of_value_entry: 

3302 tokens_in_value = list(buffered_stream.takewhile(_non_end_of_line_token)) 

3303 eol_token = cast("Deb822WhitespaceToken", next(buffered_stream, None)) 

3304 assert eol_token is None or eol_token.text == "\n" 

3305 leading_whitespace = None 

3306 trailing_whitespace = None 

3307 # "Depends:\n foo" would cause tokens_in_value to be empty for the 

3308 # first "value line" (the empty part between ":" and "\n") 

3309 if tokens_in_value: 3309 ↛ 3323line 3309 didn't jump to line 3323, because the condition on line 3309 was never false

3310 # Another special-case, "Depends: \n foo" (i.e. space after colon) 

3311 # should not introduce an IndexError 

3312 if isinstance(tokens_in_value[-1], Deb822WhitespaceToken): 

3313 trailing_whitespace = cast( 

3314 "Deb822WhitespaceToken", tokens_in_value.pop() 

3315 ) 

3316 if tokens_in_value and isinstance( 3316 ↛ 3319line 3316 didn't jump to line 3319, because the condition on line 3316 was never true

3317 tokens_in_value[-1], Deb822WhitespaceToken 

3318 ): 

3319 leading_whitespace = cast( 

3320 "Deb822WhitespaceToken", tokens_in_value[0] 

3321 ) 

3322 tokens_in_value = tokens_in_value[1:] 

3323 yield Deb822ValueLineElement( 

3324 comment_element, 

3325 continuation_line_token, 

3326 leading_whitespace, 

3327 tokens_in_value, 

3328 trailing_whitespace, 

3329 eol_token, 

3330 ) 

3331 comment_element = None 

3332 continuation_line_token = None 

3333 

3334 

3335def _build_field_with_value( 

3336 token_stream, # type: Iterable[Union[TokenOrElement, Deb822ValueElement]] 

3337): 

3338 # type: (...) -> Iterable[Union[TokenOrElement, Deb822KeyValuePairElement]] 

3339 buffered_stream = BufferingIterator(token_stream) 

3340 for token_or_element in buffered_stream: 

3341 start_of_field = False 

3342 comment_element = None 

3343 if isinstance(token_or_element, Deb822FieldNameToken): 

3344 start_of_field = True 

3345 elif isinstance(token_or_element, Deb822CommentElement): 

3346 comment_element = token_or_element 

3347 next_token = buffered_stream.peek() 

3348 start_of_field = isinstance(next_token, Deb822FieldNameToken) 

3349 if start_of_field: 3349 ↛ 3356line 3349 didn't jump to line 3356, because the condition on line 3349 was never false

3350 # Remember to consume the field token 

3351 try: 

3352 token_or_element = next(buffered_stream) 

3353 except StopIteration: # pragma: no cover 

3354 raise AssertionError 

3355 

3356 if start_of_field: 

3357 field_name = token_or_element 

3358 separator = next(buffered_stream, None) 

3359 value_element = next(buffered_stream, None) 

3360 if separator is None or value_element is None: 3360 ↛ 3363line 3360 didn't jump to line 3363, because the condition on line 3360 was never true

3361 # Early EOF - should not be possible with how the tokenizer works 

3362 # right now, but now it is future-proof. 

3363 if comment_element: 

3364 yield comment_element 

3365 error_elements = [field_name] 

3366 if separator is not None: 

3367 error_elements.append(separator) 

3368 yield Deb822ErrorElement(error_elements) 

3369 return 

3370 

3371 if isinstance(separator, Deb822FieldSeparatorToken) and isinstance( 3371 ↛ 3382line 3371 didn't jump to line 3382, because the condition on line 3371 was never false

3372 value_element, Deb822ValueElement 

3373 ): 

3374 yield Deb822KeyValuePairElement( 

3375 comment_element, 

3376 cast("Deb822FieldNameToken", field_name), 

3377 separator, 

3378 value_element, 

3379 ) 

3380 else: 

3381 # We had a parse error, consume until the newline. 

3382 error_tokens = [token_or_element] # type: List[TokenOrElement] 

3383 error_tokens.extend(buffered_stream.takewhile(_non_end_of_line_token)) 

3384 nl = buffered_stream.peek() 

3385 # Take the newline as well if present 

3386 if nl and isinstance(nl, Deb822NewlineAfterValueToken): 

3387 next(buffered_stream, None) 

3388 error_tokens.append(nl) 

3389 yield Deb822ErrorElement(error_tokens) 

3390 else: 

3391 # Token is not part of a field, emit it as-is 

3392 yield token_or_element 

3393 

3394 

3395def _abort_on_error_tokens(sequence): 

3396 # type: (Iterable[TokenOrElement]) -> Iterable[TokenOrElement] 

3397 line_no = 1 

3398 for token in sequence: 

3399 # We are always called while the sequence consists entirely of tokens 

3400 if token.is_error: 3400 ↛ 3401line 3400 didn't jump to line 3401, because the condition on line 3400 was never true

3401 error_as_text = token.convert_to_text().replace("\n", "\\n") 

3402 raise SyntaxOrParseError( 

3403 'Syntax or Parse error on or near line {line_no}: "{error_as_text}"'.format( 

3404 error_as_text=error_as_text, line_no=line_no 

3405 ) 

3406 ) 

3407 line_no += token.convert_to_text().count("\n") 

3408 yield token 

3409 

3410 

3411def parse_deb822_file( 

3412 sequence, # type: Union[Iterable[Union[str, bytes]], str] 

3413 *, 

3414 accept_files_with_error_tokens=False, # type: bool 

3415 accept_files_with_duplicated_fields=False, # type: bool 

3416 encoding="utf-8", # type: str 

3417): 

3418 # type: (...) -> Deb822FileElement 

3419 """ 

3420 

3421 :param sequence: An iterable over lines of str or bytes (an open file for 

3422 reading will do). If line endings are provided in the input, then they 

3423 must be present on every line (except the last) will be preserved as-is. 

3424 If omitted and the content is at least 2 lines, then parser will assume 

3425 implicit newlines. 

3426 :param accept_files_with_error_tokens: If True, files with critical syntax 

3427 or parse errors will be returned as "successfully" parsed. Usually, 

3428 working on files with this kind of errors are not desirable as it is 

3429 hard to make sense of such files (and they might in fact not be a deb822 

3430 file at all). When set to False (the default) a ValueError is raised if 

3431 there is a critical syntax or parse error. 

3432 Note that duplicated fields in a paragraph is not considered a critical 

3433 parse error by this parser as the implementation can gracefully cope 

3434 with these. Use accept_files_with_duplicated_fields to determine if 

3435 such files should be accepted. 

3436 :param accept_files_with_duplicated_fields: If True, then 

3437 files containing paragraphs with duplicated fields will be returned as 

3438 "successfully" parsed even though they are invalid according to the 

3439 specification. The paragraphs will prefer the first appearance of the 

3440 field unless caller explicitly requests otherwise (e.g., via 

3441 Deb822ParagraphElement.configured_view). If False, then this method 

3442 will raise a ValueError if any duplicated fields are seen inside any 

3443 paragraph. 

3444 :param encoding: The encoding to use (this is here to support Deb822-like 

3445 APIs, new code should not use this parameter). 

3446 """ 

3447 

3448 if isinstance(sequence, (str, bytes)): 3448 ↛ 3450line 3448 didn't jump to line 3450, because the condition on line 3448 was never true

3449 # Match the deb822 API. 

3450 sequence = sequence.splitlines(True) 

3451 

3452 # The order of operations are important here. As an example, 

3453 # _build_value_line assumes that all comment tokens have been merged 

3454 # into comment elements. Likewise, _build_field_and_value assumes 

3455 # that value tokens (along with their comments) have been combined 

3456 # into elements. 

3457 tokens = tokenize_deb822_file( 

3458 sequence, encoding=encoding 

3459 ) # type: Iterable[TokenOrElement] 

3460 if not accept_files_with_error_tokens: 

3461 tokens = _abort_on_error_tokens(tokens) 

3462 tokens = _combine_comment_tokens_into_elements(tokens) 

3463 tokens = _build_value_line(tokens) 

3464 tokens = _combine_vl_elements_into_value_elements(tokens) 

3465 tokens = _build_field_with_value(tokens) 

3466 tokens = _combine_kvp_elements_into_paragraphs(tokens) 

3467 # Combine any free-floating error tokens into error elements. We do 

3468 # this last as it enables other parts of the parser to include error 

3469 # tokens in their error elements if they discover something is wrong. 

3470 tokens = _combine_error_tokens_into_elements(tokens) 

3471 

3472 deb822_file = Deb822FileElement(LinkedList(tokens)) 

3473 

3474 if not accept_files_with_duplicated_fields: 

3475 for no, paragraph in enumerate(deb822_file): 

3476 if isinstance(paragraph, Deb822DuplicateFieldsParagraphElement): 3476 ↛ 3477line 3476 didn't jump to line 3477, because the condition on line 3476 was never true

3477 field_names = set() 

3478 dup_field = None 

3479 for field in paragraph.keys(): 

3480 field_name, _, _ = _unpack_key(field) 

3481 # assert for mypy 

3482 assert isinstance(field_name, str) 

3483 if field_name in field_names: 

3484 dup_field = field_name 

3485 break 

3486 field_names.add(field_name) 

3487 if dup_field is not None: 

3488 msg = 'Duplicate field "{dup_field}" in paragraph number {no}' 

3489 raise ValueError(msg.format(dup_field=dup_field, no=no)) 

3490 

3491 return deb822_file 

3492 

3493 

3494if __name__ == "__main__": # pragma: no cover 

3495 import doctest 

3496 

3497 doctest.testmod()