Coverage for src/debputy/lsp/maint_prefs.py: 82%

271 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import dataclasses 

2import functools 

3import importlib.resources 

4import re 

5import textwrap 

6from typing import ( 

7 Type, 

8 TypeVar, 

9 Generic, 

10 Optional, 

11 List, 

12 Union, 

13 Callable, 

14 Mapping, 

15 Self, 

16 Dict, 

17 Iterable, 

18 Any, 

19 Tuple, 

20) 

21 

22import debputy.lsp.data as data_dir 

23from debputy.lsp.lsp_reference_keyword import ALL_PUBLIC_NAMED_STYLES 

24from debputy.lsp.vendoring._deb822_repro.types import FormatterCallback 

25from debputy.lsp.vendoring.wrap_and_sort import wrap_and_sort_formatter 

26from debputy.packages import SourcePackage 

27from debputy.util import _error 

28from debputy.yaml import MANIFEST_YAML 

29from debputy.yaml.compat import CommentedMap 

30 

31PT = TypeVar("PT", bool, str, int) 

32 

33 

34BUILTIN_STYLES = "maint-preferences.yaml" 

35 

36_NORMALISE_FIELD_CONTENT_KEY = ["deb822", "normalize-field-content"] 

37_UPLOADER_SPLIT_RE = re.compile(r"(?<=>)\s*,") 

38 

39_WAS_OPTIONS = { 

40 "-a": ("deb822_always_wrap", True), 

41 "--always-wrap": ("deb822_always_wrap", True), 

42 "-s": ("deb822_short_indent", True), 

43 "--short-indent": ("deb822_short_indent", True), 

44 "-t": ("deb822_trailing_separator", True), 

45 "--trailing-separator": ("deb822_trailing_separator", True), 

46 # Noise option for us; we do not accept `--no-keep-first` though 

47 "-k": (None, True), 

48 "--keep-first": (None, True), 

49 "--no-keep-first": ("DISABLE_NORMALIZE_STANZA_ORDER", True), 

50 "-b": ("deb822_normalize_stanza_order", True), 

51 "--sort-binary-packages": ("deb822_normalize_stanza_order", True), 

52} 

53 

54_WAS_DEFAULTS = { 

55 "deb822_always_wrap": False, 

56 "deb822_short_indent": False, 

57 "deb822_trailing_separator": False, 

58 "deb822_normalize_stanza_order": False, 

59 "deb822_normalize_field_content": True, 

60} 

61 

62 

63@dataclasses.dataclass(slots=True, frozen=True, kw_only=True) 

64class PreferenceOption(Generic[PT]): 

65 key: Union[str, List[str]] 

66 expected_type: Union[Type[PT], Callable[[Any], Optional[str]]] 

67 description: str 

68 default_value: Optional[Union[PT, Callable[[CommentedMap], Optional[PT]]]] = None 

69 

70 @property 

71 def name(self) -> str: 

72 if isinstance(self.key, str): 

73 return self.key 

74 return ".".join(self.key) 

75 

76 @property 

77 def attribute_name(self) -> str: 

78 return self.name.replace("-", "_").replace(".", "_") 

79 

80 def extract_value( 

81 self, 

82 filename: str, 

83 key: str, 

84 data: CommentedMap, 

85 ) -> Optional[PT]: 

86 v = data.mlget(self.key, list_ok=True) 

87 if v is None: 

88 default_value = self.default_value 

89 if callable(default_value): 

90 return default_value(data) 

91 return default_value 

92 val_issue: Optional[str] = None 

93 expected_type = self.expected_type 

94 if not isinstance(expected_type, type) and callable(self.expected_type): 

95 val_issue = self.expected_type(v) 

96 elif not isinstance(v, self.expected_type): 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 val_issue = f"It should have been a {self.expected_type} but it was not" 

98 

99 if val_issue is None: 99 ↛ 101line 99 didn't jump to line 101 because the condition on line 99 was always true

100 return v 

101 raise ValueError( 

102 f'The value "{self.name}" for key {key} in file "{filename}" was incorrect: {val_issue}' 

103 ) 

104 

105 

106def _is_packaging_team_default(m: CommentedMap) -> bool: 

107 v = m.get("canonical-name") 

108 if not isinstance(v, str): 

109 return False 

110 v = v.lower() 

111 return v.endswith((" maintainer", " maintainers", " team")) 

112 

113 

114def _false_when_formatting_content(m: CommentedMap) -> Optional[bool]: 

115 return m.mlget(_NORMALISE_FIELD_CONTENT_KEY, list_ok=True, default=False) is True 

116 

117 

118MAINT_OPTIONS: List[PreferenceOption] = [ 

119 PreferenceOption( 

120 key="canonical-name", 

121 expected_type=str, 

122 description=textwrap.dedent( 

123 """\ 

124 Canonical spelling/case of the maintainer name. 

125 

126 The `debputy` linter will emit a diagnostic if the name is not spelled exactly as provided here. 

127 Can be useful to ensure your name is updated after a change of name. 

128 """ 

129 ), 

130 ), 

131 PreferenceOption( 

132 key="is-packaging-team", 

133 expected_type=bool, 

134 default_value=_is_packaging_team_default, 

135 description=textwrap.dedent( 

136 """\ 

137 Whether this entry is for a packaging team 

138 

139 This affects how styles are applied when multiple maintainers (`Maintainer` + `Uploaders`) are listed 

140 in `debian/control`. For package teams, the team preference prevails when the team is in the `Maintainer` 

141 field. For non-packaging teams, generally the rules do not apply as soon as there are co-maintainers. 

142 

143 The default is derived from the canonical name. If said name ends with phrases like "Team" or "Maintainer" 

144 then the email is assumed to be for a team by default. 

145 """ 

146 ), 

147 ), 

148 PreferenceOption( 

149 key="formatting", 

150 expected_type=lambda x: ( 

151 None 

152 if isinstance(x, EffectiveFormattingPreference) 

153 else "It should have been a EffectiveFormattingPreference but it was not" 

154 ), 

155 default_value=None, 

156 description=textwrap.dedent( 

157 """\ 

158 The formatting preference of the maintainer. Can either be a string for a named style or an inline 

159 style. 

160 """ 

161 ), 

162 ), 

163] 

164 

165FORMATTING_OPTIONS = [ 

166 PreferenceOption( 

167 key=["deb822", "short-indent"], 

168 expected_type=bool, 

169 description=textwrap.dedent( 

170 """\ 

171 Whether to use "short" indents for relationship fields (such as `Depends`). 

172 

173 This roughly corresponds to `wrap-and-sort`'s `-s` option. 

174 

175 **Example**: 

176 

177 When `true`, the following: 

178 ``` 

179 Depends: foo, 

180 bar 

181 ``` 

182 

183 would be reformatted as: 

184 

185 ``` 

186 Depends: 

187 foo, 

188 bar 

189 ``` 

190 

191 (Assuming `formatting.deb822.short-indent` is `false`) 

192 

193 Note that defaults to `false` *if* (and only if) other formatting options will trigger reformat of 

194 the field and this option has not been set. Setting this option can trigger reformatting of fields 

195 that span multiple lines. 

196 

197 Additionally, this only triggers when a field is being reformatted. Generally that requires 

198 another option such as `formatting.deb822.normalize-field-content` for that to happen. 

199 """ 

200 ), 

201 ), 

202 PreferenceOption( 

203 key=["deb822", "always-wrap"], 

204 expected_type=bool, 

205 description=textwrap.dedent( 

206 """\ 

207 Whether to always wrap fields (such as `Depends`). 

208 

209 This roughly corresponds to `wrap-and-sort`'s `-a` option. 

210 

211 **Example**: 

212 

213 When `true`, the following: 

214 ``` 

215 Depends: foo, bar 

216 ``` 

217 

218 would be reformatted as: 

219 

220 ``` 

221 Depends: foo, 

222 bar 

223 ``` 

224 

225 (Assuming `formatting.deb822.short-indent` is `false`) 

226 

227 This option only applies to fields where formatting is a pure style preference. As an 

228 example, `Description` (`debian/control`) or `License` (`debian/copyright`) will not 

229 be affected by this option. 

230 

231 Note: When `true`, this option overrules `formatting.deb822.max-line-length` when they interact.  

232 Additionally, this only triggers when a field is being reformatted. Generally that requires 

233 another option such as `formatting.deb822.normalize-field-content` for that to happen. 

234 """ 

235 ), 

236 ), 

237 PreferenceOption( 

238 key=["deb822", "trailing-separator"], 

239 expected_type=bool, 

240 default_value=False, 

241 description=textwrap.dedent( 

242 """\ 

243 Whether to always end relationship fields (such as `Depends`) with a trailing separator. 

244 

245 This roughly corresponds to `wrap-and-sort`'s `-t` option. 

246 

247 **Example**: 

248 

249 When `true`, the following: 

250 ``` 

251 Depends: foo, 

252 bar 

253 ``` 

254 

255 would be reformatted as: 

256 

257 ``` 

258 Depends: foo, 

259 bar, 

260 ``` 

261 

262 Note: The trailing separator is only applied if the field is reformatted. This means this option 

263 generally requires another option to trigger reformatting (like 

264 `formatting.deb822.normalize-field-content`). 

265 """ 

266 ), 

267 ), 

268 PreferenceOption( 

269 key=["deb822", "max-line-length"], 

270 expected_type=int, 

271 default_value=79, 

272 description=textwrap.dedent( 

273 """\ 

274 How long a value line can be before it should be line wrapped. 

275 

276 This roughly corresponds to `wrap-and-sort`'s `--max-line-length` option. 

277 

278 This option only applies to fields where formatting is a pure style preference. As an 

279 example, `Description` (`debian/control`) or `License` (`debian/copyright`) will not 

280 be affected by this option. 

281 

282 Note: When `formatting.deb822.always-wrap` is `true`, then this option will be overruled. 

283 Additionally, this only triggers when a field is being reformatted. Generally that requires 

284 another option such as `formatting.deb822.normalize-field-content` for that to happen. 

285 """ 

286 ), 

287 ), 

288 PreferenceOption( 

289 key=_NORMALISE_FIELD_CONTENT_KEY, 

290 expected_type=bool, 

291 default_value=False, 

292 description=textwrap.dedent( 

293 """\ 

294 Whether to normalize field content. 

295 

296 This roughly corresponds to the subset of `wrap-and-sort` that normalizes field content 

297 like sorting and normalizing relations or sorting the architecture field. 

298 

299 **Example**: 

300 

301 When `true`, the following: 

302 ``` 

303 Depends: foo, 

304 bar|baz 

305 ``` 

306 

307 would be reformatted as: 

308 

309 ``` 

310 Depends: bar | baz, 

311 foo, 

312 ``` 

313 

314 This causes affected fields to always be rewritten and therefore be sure that other options 

315 such as `formatting.deb822.short-indent` or `formatting.deb822.always-wrap` is set according 

316 to taste. 

317 

318 Note: The field may be rewritten without this being set to `true`. As an example, the `always-wrap` 

319 option can trigger a field rewrite. However, in that case, the values (including any internal whitespace) 

320 are left as-is while the whitespace normalization between the values is still applied. 

321 """ 

322 ), 

323 ), 

324 PreferenceOption( 

325 key=["deb822", "normalize-field-order"], 

326 expected_type=bool, 

327 default_value=False, 

328 description=textwrap.dedent( 

329 """\ 

330 Whether to normalize field order in a stanza. 

331 

332 There is no `wrap-and-sort` feature matching this. 

333 

334 **Example**: 

335 

336 When `true`, the following: 

337 ``` 

338 Depends: bar 

339 Package: foo 

340 ``` 

341 

342 would be reformatted as: 

343 

344 ``` 

345 Depends: foo 

346 Package: bar 

347 ``` 

348 

349 The field order is not by field name but by a logic order defined in `debputy` based on existing 

350 conventions. The `deb822` format does not dictate any field order inside stanzas in general, so 

351 reordering of fields is generally safe. 

352 

353 If a field of the first stanza is known to be a format discriminator such as the `Format' in 

354 `debian/copyright`, then it will be put first. Generally that matches existing convention plus 

355 it maximizes the odds that existing tools will correctly identify the file format. 

356 """ 

357 ), 

358 ), 

359 PreferenceOption( 

360 key=["deb822", "normalize-stanza-order"], 

361 expected_type=bool, 

362 default_value=False, 

363 description=textwrap.dedent( 

364 """\ 

365 Whether to normalize stanza order in a file. 

366 

367 This roughly corresponds to `wrap-and-sort`'s `-kb` feature except this may apply to other deb822 

368 files. 

369 

370 **Example**: 

371 

372 When `true`, the following: 

373 ``` 

374 Source: zzbar 

375 

376 Package: zzbar 

377 

378 Package: zzbar-util 

379 

380 Package: libzzbar-dev 

381 

382 Package: libzzbar2 

383 ``` 

384 

385 would be reformatted as: 

386 

387 ``` 

388 Source: zzbar 

389 

390 Package: zzbar 

391 

392 Package: libzzbar2 

393 

394 Package: libzzbar-dev 

395 

396 Package: zzbar-util 

397 ``` 

398 

399 Reordering will only performed when: 

400 1) There is a convention for a normalized order 

401 2) The normalization can be performed without changing semantics 

402 

403 Note: This option only guards style/preference related re-ordering. It does not influence 

404 warnings about the order being semantic incorrect (which will still be emitted regardless 

405 of this setting). 

406 """ 

407 ), 

408 ), 

409] 

410 

411 

412@dataclasses.dataclass(slots=True, frozen=True) 

413class EffectiveFormattingPreference: 

414 deb822_short_indent: Optional[bool] = None 

415 deb822_always_wrap: Optional[bool] = None 

416 deb822_trailing_separator: bool = False 

417 deb822_normalize_field_content: bool = False 

418 deb822_normalize_field_order: bool = False 

419 deb822_normalize_stanza_order: bool = False 

420 deb822_max_line_length: int = 79 

421 

422 @classmethod 

423 def from_file( 

424 cls, 

425 filename: str, 

426 key: str, 

427 styles: CommentedMap, 

428 ) -> Self: 

429 attr = {} 

430 

431 for option in FORMATTING_OPTIONS: 

432 if not hasattr(cls, option.attribute_name): 432 ↛ 433line 432 didn't jump to line 433 because the condition on line 432 was never true

433 continue 

434 value = option.extract_value(filename, key, styles) 

435 attr[option.attribute_name] = value 

436 return cls(**attr) # type: ignore 

437 

438 @classmethod 

439 def aligned_preference( 

440 cls, 

441 a: Optional["EffectiveFormattingPreference"], 

442 b: Optional["EffectiveFormattingPreference"], 

443 ) -> Optional["EffectiveFormattingPreference"]: 

444 if a is None or b is None: 

445 return None 

446 

447 for option in MAINT_OPTIONS: 

448 attr_name = option.attribute_name 

449 if not hasattr(EffectiveFormattingPreference, attr_name): 449 ↛ 451line 449 didn't jump to line 451 because the condition on line 449 was always true

450 continue 

451 a_value = getattr(a, attr_name) 

452 b_value = getattr(b, attr_name) 

453 if a_value != b_value: 

454 return None 

455 return a 

456 

457 def deb822_formatter(self) -> FormatterCallback: 

458 line_length = self.deb822_max_line_length 

459 return wrap_and_sort_formatter( 

460 1 if self.deb822_short_indent else "FIELD_NAME_LENGTH", 

461 trailing_separator=self.deb822_trailing_separator, 

462 immediate_empty_line=self.deb822_short_indent or False, 

463 max_line_length_one_liner=(0 if self.deb822_always_wrap else line_length), 

464 ) 

465 

466 def replace(self, /, **changes: Any) -> Self: 

467 return dataclasses.replace(self, **changes) 

468 

469 

470@dataclasses.dataclass(slots=True, frozen=True) 

471class MaintainerPreference: 

472 canonical_name: Optional[str] = None 

473 is_packaging_team: bool = False 

474 formatting: Optional[EffectiveFormattingPreference] = None 

475 

476 @classmethod 

477 def from_file( 

478 cls, 

479 filename: str, 

480 key: str, 

481 styles: CommentedMap, 

482 ) -> Self: 

483 attr = {} 

484 

485 for option in MAINT_OPTIONS: 

486 if not hasattr(cls, option.attribute_name): 486 ↛ 487line 486 didn't jump to line 487 because the condition on line 486 was never true

487 continue 

488 value = option.extract_value(filename, key, styles) 

489 attr[option.attribute_name] = value 

490 return cls(**attr) # type: ignore 

491 

492 

493class MaintainerPreferenceTable: 

494 

495 def __init__( 

496 self, 

497 named_styles: Mapping[str, EffectiveFormattingPreference], 

498 maintainer_preferences: Mapping[str, MaintainerPreference], 

499 ) -> None: 

500 self._named_styles = named_styles 

501 self._maintainer_preferences = maintainer_preferences 

502 

503 @classmethod 

504 def load_preferences(cls) -> Self: 

505 named_styles: Dict[str, EffectiveFormattingPreference] = {} 

506 maintainer_preferences: Dict[str, MaintainerPreference] = {} 

507 

508 path = importlib.resources.files(data_dir).joinpath(BUILTIN_STYLES) 

509 

510 with path.open("r", encoding="utf-8") as fd: 

511 parse_file(named_styles, maintainer_preferences, str(path), fd) 

512 

513 missing_keys = set(named_styles.keys()).difference( 

514 ALL_PUBLIC_NAMED_STYLES.keys() 

515 ) 

516 if missing_keys: 516 ↛ 517line 516 didn't jump to line 517 because the condition on line 516 was never true

517 missing_styles = ", ".join(sorted(missing_keys)) 

518 _error( 

519 f"The following named styles are public API but not present in the config file: {missing_styles}" 

520 ) 

521 

522 # TODO: Support fetching styles online to pull them in faster than waiting for a stable release. 

523 return cls(named_styles, maintainer_preferences) 

524 

525 @property 

526 def named_styles(self) -> Mapping[str, EffectiveFormattingPreference]: 

527 return self._named_styles 

528 

529 @property 

530 def maintainer_preferences(self) -> Mapping[str, MaintainerPreference]: 

531 return self._maintainer_preferences 

532 

533 

534def parse_file( 

535 named_styles: Dict[str, EffectiveFormattingPreference], 

536 maintainer_preferences: Dict[str, MaintainerPreference], 

537 filename: str, 

538 fd, 

539) -> None: 

540 content = MANIFEST_YAML.load(fd) 

541 if not isinstance(content, CommentedMap): 541 ↛ 542line 541 didn't jump to line 542 because the condition on line 541 was never true

542 raise ValueError( 

543 f'The file "{filename}" should be a YAML file with a single mapping at the root' 

544 ) 

545 try: 

546 maintainer_rules = content["maintainer-rules"] 

547 if not isinstance(maintainer_rules, CommentedMap): 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true

548 raise KeyError("maintainer-rules") from None 

549 except KeyError: 

550 raise ValueError( 

551 f'The file "{filename}" should have a "maintainer-rules" key which must be a mapping.' 

552 ) 

553 named_styles_raw = content.get("formatting") 

554 if named_styles_raw is None or not isinstance(named_styles_raw, CommentedMap): 554 ↛ 555line 554 didn't jump to line 555 because the condition on line 554 was never true

555 named_styles_raw = {} 

556 

557 for style_name, content in named_styles_raw.items(): 

558 style = EffectiveFormattingPreference.from_file( 

559 filename, 

560 style_name, 

561 content, 

562 ) 

563 named_styles[style_name] = style 

564 

565 for maintainer_email, maintainer_pref in maintainer_rules.items(): 

566 if not isinstance(maintainer_pref, CommentedMap): 566 ↛ 567line 566 didn't jump to line 567 because the condition on line 566 was never true

567 line_no = maintainer_rules.lc.key(maintainer_email).line 

568 raise ValueError( 

569 f'The value for maintainer "{maintainer_email}" should have been a mapping,' 

570 f' but it is not. The problem entry is at line {line_no} in "{filename}"' 

571 ) 

572 formatting = maintainer_pref.get("formatting") 

573 if isinstance(formatting, str): 

574 try: 

575 style = named_styles[formatting] 

576 except KeyError: 

577 line_no = maintainer_rules.lc.key(maintainer_email).line 

578 raise ValueError( 

579 f'The maintainer "{maintainer_email}" requested the named style "{formatting}",' 

580 f' but said style was not defined {filename}. The problem entry is at line {line_no} in "{filename}"' 

581 ) from None 

582 maintainer_pref["formatting"] = style 

583 elif formatting is not None: 583 ↛ 584line 583 didn't jump to line 584 because the condition on line 583 was never true

584 maintainer_pref["formatting"] = EffectiveFormattingPreference.from_file( 

585 filename, 

586 "formatting", 

587 formatting, 

588 ) 

589 mp = MaintainerPreference.from_file( 

590 filename, 

591 maintainer_email, 

592 maintainer_pref, 

593 ) 

594 

595 maintainer_preferences[maintainer_email] = mp 

596 

597 

598@functools.lru_cache(64) 

599def extract_maint_email(maint: str) -> str: 

600 if not maint.endswith(">"): 600 ↛ 601line 600 didn't jump to line 601 because the condition on line 600 was never true

601 return "" 

602 

603 try: 

604 idx = maint.index("<") 

605 except ValueError: 

606 return "" 

607 return maint[idx + 1 : -1] 

608 

609 

610def _parse_salsa_ci_boolean(value: Union[str, int, bool]) -> bool: 

611 if isinstance(value, str): 

612 return value in ("yes", "1", "true") 

613 elif not isinstance(value, (int, bool)): 613 ↛ 614line 613 didn't jump to line 614 because the condition on line 613 was never true

614 raise TypeError("Unsupported value") 

615 else: 

616 return value is True or value == 1 

617 

618 

619def _read_salsa_ci_wrap_and_sort_enabled(salsa_ci: Optional[CommentedMap]) -> bool: 

620 sentinel = object() 

621 disable_wrap_and_sort_raw = salsa_ci.mlget( 

622 ["variables", "SALSA_CI_DISABLE_WRAP_AND_SORT"], 

623 list_ok=True, 

624 default=sentinel, 

625 ) 

626 

627 if disable_wrap_and_sort_raw is sentinel: 

628 enable_wrap_and_sort_raw = salsa_ci.mlget( 

629 ["variables", "SALSA_CI_ENABLE_WRAP_AND_SORT"], 

630 list_ok=True, 

631 default=None, 

632 ) 

633 if enable_wrap_and_sort_raw is None or not isinstance( 

634 enable_wrap_and_sort_raw, (str, int, bool) 

635 ): 

636 return False 

637 

638 return _parse_salsa_ci_boolean(enable_wrap_and_sort_raw) 

639 if not isinstance(disable_wrap_and_sort_raw, (str, int, bool)): 

640 return False 

641 

642 disable_wrap_and_sort = _parse_salsa_ci_boolean(disable_wrap_and_sort_raw) 

643 return not disable_wrap_and_sort 

644 

645 

646def determine_effective_preference( 

647 maint_preference_table: MaintainerPreferenceTable, 

648 source_package: Optional[SourcePackage], 

649 salsa_ci: Optional[CommentedMap], 

650) -> Tuple[Optional[EffectiveFormattingPreference], Optional[str], Optional[str]]: 

651 style = source_package.fields.get("X-Style") if source_package is not None else None 

652 if style is not None: 

653 if style not in ALL_PUBLIC_NAMED_STYLES: 653 ↛ 654line 653 didn't jump to line 654 because the condition on line 653 was never true

654 return None, None, "X-Style contained an unknown/unsupported style" 

655 return maint_preference_table.named_styles.get(style), "debputy reformat", None 

656 

657 if salsa_ci and _read_salsa_ci_wrap_and_sort_enabled(salsa_ci): 

658 wrap_and_sort_options = salsa_ci.mlget( 

659 ["variables", "SALSA_CI_WRAP_AND_SORT_ARGS"], 

660 list_ok=True, 

661 default=None, 

662 ) 

663 if wrap_and_sort_options is None: 

664 wrap_and_sort_options = "" 

665 elif not isinstance(wrap_and_sort_options, str): 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true

666 return ( 

667 None, 

668 None, 

669 "The salsa-ci had a non-string option for wrap-and-sort", 

670 ) 

671 detected_style = parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options) 

672 tool_w_args = f"wrap-and-sort {wrap_and_sort_options}".strip() 

673 if detected_style is None: 673 ↛ 674line 673 didn't jump to line 674 because the condition on line 673 was never true

674 msg = "One or more of the wrap-and-sort options in the salsa-ci file was not supported" 

675 else: 

676 msg = None 

677 return detected_style, tool_w_args, msg 

678 if source_package is None: 678 ↛ 679line 678 didn't jump to line 679 because the condition on line 678 was never true

679 return None, None, None 

680 

681 maint = source_package.fields.get("Maintainer") 

682 if maint is None: 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true

683 return None, None, None 

684 maint_email = extract_maint_email(maint) 

685 maint_pref = maint_preference_table.maintainer_preferences.get(maint_email) 

686 # Special-case "@packages.debian.org" when missing, since they are likely to be "ad-hoc" 

687 # teams that will not be registered. In that case, we fall back to looking at the uploader 

688 # preferences as-if the maintainer had not been listed at all. 

689 if maint_pref is None and not maint_email.endswith("@packages.debian.org"): 

690 return None, None, None 

691 if maint_pref is not None and maint_pref.is_packaging_team: 691 ↛ 694line 691 didn't jump to line 694 because the condition on line 691 was never true

692 # When the maintainer is registered as a packaging team, then we assume the packaging 

693 # team's style applies unconditionally. 

694 effective = maint_pref.formatting 

695 tool_w_args = _guess_tool_from_style(maint_preference_table, effective) 

696 return effective, tool_w_args, None 

697 uploaders = source_package.fields.get("Uploaders") 

698 if uploaders is None: 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true

699 detected_style = maint_pref.formatting if maint_pref is not None else None 

700 tool_w_args = _guess_tool_from_style(maint_preference_table, detected_style) 

701 return detected_style, tool_w_args, None 

702 all_styles: List[Optional[EffectiveFormattingPreference]] = [] 

703 if maint_pref is not None: 703 ↛ 704line 703 didn't jump to line 704 because the condition on line 703 was never true

704 all_styles.append(maint_pref.formatting) 

705 for uploader in _UPLOADER_SPLIT_RE.split(uploaders): 

706 uploader_email = extract_maint_email(uploader) 

707 uploader_pref = maint_preference_table.maintainer_preferences.get( 

708 uploader_email 

709 ) 

710 all_styles.append(uploader_pref.formatting if uploader_pref else None) 

711 

712 if not all_styles: 712 ↛ 713line 712 didn't jump to line 713 because the condition on line 712 was never true

713 return None, None, None 

714 r = functools.reduce(EffectiveFormattingPreference.aligned_preference, all_styles) 

715 assert not isinstance(r, MaintainerPreference) 

716 tool_w_args = _guess_tool_from_style(maint_preference_table, r) 

717 return r, tool_w_args, None 

718 

719 

720def _guess_tool_from_style( 

721 maint_preference_table: MaintainerPreferenceTable, 

722 pref: Optional[EffectiveFormattingPreference], 

723) -> Optional[str]: 

724 if pref is None: 

725 return None 

726 if maint_preference_table.named_styles["black"] == pref: 726 ↛ 728line 726 didn't jump to line 728 because the condition on line 726 was always true

727 return "debputy reformat" 

728 return None 

729 

730 

731def _split_options(args: Iterable[str]) -> Iterable[str]: 

732 for arg in args: 

733 if arg.startswith("--"): 

734 yield arg 

735 continue 

736 if not arg.startswith("-") or len(arg) < 2: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true

737 yield arg 

738 continue 

739 for sarg in arg[1:]: 

740 yield f"-{sarg}" 

741 

742 

743@functools.lru_cache 

744def parse_salsa_ci_wrap_and_sort_args( 

745 args: str, 

746) -> Optional[EffectiveFormattingPreference]: 

747 options = dict(_WAS_DEFAULTS) 

748 for arg in _split_options(args.split()): 

749 v = _WAS_OPTIONS.get(arg) 

750 if v is None: 750 ↛ 751line 750 didn't jump to line 751 because the condition on line 750 was never true

751 return None 

752 varname, value = v 

753 if varname is None: 

754 continue 

755 options[varname] = value 

756 if "DISABLE_NORMALIZE_STANZA_ORDER" in options: 

757 del options["DISABLE_NORMALIZE_STANZA_ORDER"] 

758 options["deb822_normalize_stanza_order"] = False 

759 

760 return EffectiveFormattingPreference(**options) # type: ignore