From 03a1bd448be99d872d663a57a1cf4492882e090d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 25 Apr 2024 04:59:47 +0200 Subject: Adding upstream version 0.1.29. Signed-off-by: Daniel Baumann --- coverage-report/d_e9c451f4ae334f76_parsing_py.html | 3596 -------------------- 1 file changed, 3596 deletions(-) delete mode 100644 coverage-report/d_e9c451f4ae334f76_parsing_py.html (limited to 'coverage-report/d_e9c451f4ae334f76_parsing_py.html') diff --git a/coverage-report/d_e9c451f4ae334f76_parsing_py.html b/coverage-report/d_e9c451f4ae334f76_parsing_py.html deleted file mode 100644 index 40a0c05..0000000 --- a/coverage-report/d_e9c451f4ae334f76_parsing_py.html +++ /dev/null @@ -1,3596 +0,0 @@ - - - - - Coverage for src/debputy/lsp/vendoring/_deb822_repro/parsing.py: 59% - - - - - -
-
-

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

-
- - - -- cgit v1.2.3