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

272 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +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.named_styles 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 "deb822_auto_canonical_size_field_names": False, 

61} 

62 

63 

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

65class PreferenceOption(Generic[PT]): 

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

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

68 description: str 

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

70 

71 @property 

72 def name(self) -> str: 

73 if isinstance(self.key, str): 

74 return self.key 

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

76 

77 @property 

78 def attribute_name(self) -> str: 

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

80 

81 def extract_value( 

82 self, 

83 filename: str, 

84 key: str, 

85 data: CommentedMap, 

86 ) -> Optional[PT]: 

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

88 if v is None: 

89 default_value = self.default_value 

90 if callable(default_value): 

91 return default_value(data) 

92 return default_value 

93 val_issue: Optional[str] = None 

94 expected_type = self.expected_type 

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

96 val_issue = self.expected_type(v) 

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

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

99 

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

101 return v 

102 raise ValueError( 

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

104 ) 

105 

106 

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

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

109 if not isinstance(v, str): 

110 return False 

111 v = v.lower() 

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

113 

114 

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

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

117 

118 

119MAINT_OPTIONS: List[PreferenceOption] = [ 

120 PreferenceOption( 

121 key="canonical-name", 

122 expected_type=str, 

123 description=textwrap.dedent( 

124 """\ 

125 Canonical spelling/case of the maintainer name. 

126 

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

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

129 """ 

130 ), 

131 ), 

132 PreferenceOption( 

133 key="is-packaging-team", 

134 expected_type=bool, 

135 default_value=_is_packaging_team_default, 

136 description=textwrap.dedent( 

137 """\ 

138 Whether this entry is for a packaging team 

139 

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

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

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

143 

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

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

146 """ 

147 ), 

148 ), 

149 PreferenceOption( 

150 key="formatting", 

151 expected_type=lambda x: ( 

152 None 

153 if isinstance(x, EffectiveFormattingPreference) 

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

155 ), 

156 default_value=None, 

157 description=textwrap.dedent( 

158 """\ 

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

160 style. 

161 """ 

162 ), 

163 ), 

164] 

165 

166FORMATTING_OPTIONS = [ 

167 PreferenceOption( 

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

169 expected_type=bool, 

170 description=textwrap.dedent( 

171 """\ 

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

173 

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

175 

176 **Example**: 

177 

178 When `true`, the following: 

179 ``` 

180 Depends: foo, 

181 bar 

182 ``` 

183 

184 would be reformatted as: 

185 

186 ``` 

187 Depends: 

188 foo, 

189 bar 

190 ``` 

191 

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

193 

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

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

196 that span multiple lines. 

197 

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

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

200 """ 

201 ), 

202 ), 

203 PreferenceOption( 

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

205 expected_type=bool, 

206 description=textwrap.dedent( 

207 """\ 

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

209 

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

211 

212 **Example**: 

213 

214 When `true`, the following: 

215 ``` 

216 Depends: foo, bar 

217 ``` 

218 

219 would be reformatted as: 

220 

221 ``` 

222 Depends: foo, 

223 bar 

224 ``` 

225 

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

227 

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

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

230 be affected by this option. 

231 

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

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

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

235 """ 

236 ), 

237 ), 

238 PreferenceOption( 

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

240 expected_type=bool, 

241 default_value=False, 

242 description=textwrap.dedent( 

243 """\ 

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

245 

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

247 

248 **Example**: 

249 

250 When `true`, the following: 

251 ``` 

252 Depends: foo, 

253 bar 

254 ``` 

255 

256 would be reformatted as: 

257 

258 ``` 

259 Depends: foo, 

260 bar, 

261 ``` 

262 

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

264 generally requires another option to trigger reformatting (like 

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

266 """ 

267 ), 

268 ), 

269 PreferenceOption( 

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

271 expected_type=int, 

272 default_value=79, 

273 description=textwrap.dedent( 

274 """\ 

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

276 

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

278 

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

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

281 be affected by this option. 

282 

283 This setting may affect style-related diagnostics. Notably, many tools with linting or 

284 diagnostics capabilities (`debputy` included) will generally flag lines of 80 or more 

285 characters as suboptimal. The diagnostics are because some fields are displayed to end 

286 users in settings where 80 character-wide displays are or were the norm with no or 

287 only awkward horizontal scrolling options available. Using a value higher than the 

288 default may cause undesired diagnostics. 

289 

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

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

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

293 """ 

294 ), 

295 ), 

296 PreferenceOption( 

297 key=_NORMALISE_FIELD_CONTENT_KEY, 

298 expected_type=bool, 

299 default_value=False, 

300 description=textwrap.dedent( 

301 """\ 

302 Whether to normalize field content. 

303 

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

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

306 

307 **Example**: 

308 

309 When `true`, the following: 

310 ``` 

311 Depends: foo, 

312 bar|baz 

313 ``` 

314 

315 would be reformatted as: 

316 

317 ``` 

318 Depends: bar | baz, 

319 foo, 

320 ``` 

321 

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

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

324 to taste. 

325 

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

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

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

329 """ 

330 ), 

331 ), 

332 PreferenceOption( 

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

334 expected_type=bool, 

335 default_value=False, 

336 description=textwrap.dedent( 

337 """\ 

338 Whether to normalize field order in a stanza. 

339 

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

341 

342 **Example**: 

343 

344 When `true`, the following: 

345 ``` 

346 Depends: bar 

347 Package: foo 

348 ``` 

349 

350 would be reformatted as: 

351 

352 ``` 

353 Depends: foo 

354 Package: bar 

355 ``` 

356 

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

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

359 reordering of fields is generally safe. 

360 

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

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

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

364 """ 

365 ), 

366 ), 

367 PreferenceOption( 

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

369 expected_type=bool, 

370 default_value=False, 

371 description=textwrap.dedent( 

372 """\ 

373 Whether to normalize stanza order in a file. 

374 

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

376 files. 

377 

378 **Example**: 

379 

380 When `true`, the following: 

381 ``` 

382 Source: zzbar 

383 

384 Package: zzbar 

385 

386 Package: zzbar-util 

387 

388 Package: libzzbar-dev 

389 

390 Package: libzzbar2 

391 ``` 

392 

393 would be reformatted as: 

394 

395 ``` 

396 Source: zzbar 

397 

398 Package: zzbar 

399 

400 Package: libzzbar2 

401 

402 Package: libzzbar-dev 

403 

404 Package: zzbar-util 

405 ``` 

406 

407 Reordering will only performed when: 

408 1) There is a convention for a normalized order 

409 2) The normalization can be performed without changing semantics 

410 

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

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

413 of this setting). 

414 """ 

415 ), 

416 ), 

417 PreferenceOption( 

418 key=["deb822", "auto-canonical-size-field-names"], 

419 expected_type=bool, 

420 default_value=False, 

421 description=textwrap.dedent( 

422 """\ 

423 Whether to canonical field names of known `deb822` fields. 

424 

425 This causes formatting to align known fields in `deb822` files with their 

426 canonical spelling. As an examples: 

427 

428 ``` 

429 source: foo 

430 RULES-REQUIRES-ROOT: no 

431 ``` 

432 

433 Would be reformatted as: 

434 

435 ``` 

436 Source: foo 

437 Rules-Requires-Root: no 

438 ``` 

439 

440 The formatting only applies when the canonical spelling of the field is known 

441 to `debputy`. Unknown fields retain their original casing/formatting. 

442 

443 This setting may affect style-related diagnostics. Notably, many tools with linting or 

444 diagnostics capabilities (`debputy` included) will generally flag non-canonical spellings 

445 of field names as suboptimal. Note that while `debputy` will only flag and correct 

446 non-canonical casing of fields, some tooling may be more opinionated and flag even 

447 fields they do not know using an algorithm to guess the canonical casing. Therefore, 

448 even with this enabled, you can still canonical spelling related diagnostics from 

449 other tooling. 

450 """ 

451 ), 

452 ), 

453] 

454 

455 

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

457class EffectiveFormattingPreference: 

458 deb822_short_indent: Optional[bool] = None 

459 deb822_always_wrap: Optional[bool] = None 

460 deb822_trailing_separator: bool = False 

461 deb822_normalize_field_content: bool = False 

462 deb822_normalize_field_order: bool = False 

463 deb822_normalize_stanza_order: bool = False 

464 deb822_max_line_length: int = 79 

465 deb822_auto_canonical_size_field_names: bool = False 

466 

467 @classmethod 

468 def from_file( 

469 cls, 

470 filename: str, 

471 key: str, 

472 styles: CommentedMap, 

473 ) -> Self: 

474 attr = {} 

475 

476 for option in FORMATTING_OPTIONS: 

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

478 continue 

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

480 attr[option.attribute_name] = value 

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

482 

483 @classmethod 

484 def aligned_preference( 

485 cls, 

486 a: Optional["EffectiveFormattingPreference"], 

487 b: Optional["EffectiveFormattingPreference"], 

488 ) -> Optional["EffectiveFormattingPreference"]: 

489 if a is None or b is None: 

490 return None 

491 

492 for option in MAINT_OPTIONS: 

493 attr_name = option.attribute_name 

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

495 continue 

496 a_value = getattr(a, attr_name) 

497 b_value = getattr(b, attr_name) 

498 if a_value != b_value: 

499 return None 

500 return a 

501 

502 def deb822_formatter(self) -> FormatterCallback: 

503 line_length = self.deb822_max_line_length 

504 return wrap_and_sort_formatter( 

505 1 if self.deb822_short_indent else "FIELD_NAME_LENGTH", 

506 trailing_separator=self.deb822_trailing_separator, 

507 immediate_empty_line=self.deb822_short_indent or False, 

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

509 ) 

510 

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

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

513 

514 

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

516class MaintainerPreference: 

517 canonical_name: Optional[str] = None 

518 is_packaging_team: bool = False 

519 formatting: Optional[EffectiveFormattingPreference] = None 

520 

521 @classmethod 

522 def from_file( 

523 cls, 

524 filename: str, 

525 key: str, 

526 styles: CommentedMap, 

527 ) -> Self: 

528 attr = {} 

529 

530 for option in MAINT_OPTIONS: 

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

532 continue 

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

534 attr[option.attribute_name] = value 

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

536 

537 

538class MaintainerPreferenceTable: 

539 

540 def __init__( 

541 self, 

542 named_styles: Mapping[str, EffectiveFormattingPreference], 

543 maintainer_preferences: Mapping[str, MaintainerPreference], 

544 ) -> None: 

545 self._named_styles = named_styles 

546 self._maintainer_preferences = maintainer_preferences 

547 

548 @classmethod 

549 def load_preferences(cls) -> Self: 

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

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

552 

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

554 

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

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

557 

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

559 ALL_PUBLIC_NAMED_STYLES.keys() 

560 ) 

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

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

563 _error( 

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

565 ) 

566 

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

568 return cls(named_styles, maintainer_preferences) 

569 

570 @property 

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

572 return self._named_styles 

573 

574 @property 

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

576 return self._maintainer_preferences 

577 

578 

579def parse_file( 

580 named_styles: Dict[str, EffectiveFormattingPreference], 

581 maintainer_preferences: Dict[str, MaintainerPreference], 

582 filename: str, 

583 fd, 

584) -> None: 

585 content = MANIFEST_YAML.load(fd) 

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

587 raise ValueError( 

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

589 ) 

590 try: 

591 maintainer_rules = content["maintainer-rules"] 

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

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

594 except KeyError: 

595 raise ValueError( 

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

597 ) 

598 named_styles_raw = content.get("formatting") 

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

600 named_styles_raw = {} 

601 

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

603 style = EffectiveFormattingPreference.from_file( 

604 filename, 

605 style_name, 

606 content, 

607 ) 

608 named_styles[style_name] = style 

609 

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

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

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

613 raise ValueError( 

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

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

616 ) 

617 formatting = maintainer_pref.get("formatting") 

618 if isinstance(formatting, str): 

619 try: 

620 style = named_styles[formatting] 

621 except KeyError: 

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

623 raise ValueError( 

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

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

626 ) from None 

627 maintainer_pref["formatting"] = style 

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

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

630 filename, 

631 "formatting", 

632 formatting, 

633 ) 

634 mp = MaintainerPreference.from_file( 

635 filename, 

636 maintainer_email, 

637 maintainer_pref, 

638 ) 

639 

640 maintainer_preferences[maintainer_email] = mp 

641 

642 

643@functools.lru_cache(64) 

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

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

646 return "" 

647 

648 try: 

649 idx = maint.index("<") 

650 except ValueError: 

651 return "" 

652 return maint[idx + 1 : -1] 

653 

654 

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

656 if isinstance(value, str): 

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

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

659 raise TypeError("Unsupported value") 

660 else: 

661 return value is True or value == 1 

662 

663 

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

665 sentinel = object() 

666 disable_wrap_and_sort_raw = salsa_ci.mlget( 

667 ["variables", "SALSA_CI_DISABLE_WRAP_AND_SORT"], 

668 list_ok=True, 

669 default=sentinel, 

670 ) 

671 

672 if disable_wrap_and_sort_raw is sentinel: 

673 enable_wrap_and_sort_raw = salsa_ci.mlget( 

674 ["variables", "SALSA_CI_ENABLE_WRAP_AND_SORT"], 

675 list_ok=True, 

676 default=None, 

677 ) 

678 if enable_wrap_and_sort_raw is None or not isinstance( 

679 enable_wrap_and_sort_raw, (str, int, bool) 

680 ): 

681 return False 

682 

683 return _parse_salsa_ci_boolean(enable_wrap_and_sort_raw) 

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

685 return False 

686 

687 disable_wrap_and_sort = _parse_salsa_ci_boolean(disable_wrap_and_sort_raw) 

688 return not disable_wrap_and_sort 

689 

690 

691def determine_effective_preference( 

692 maint_preference_table: MaintainerPreferenceTable, 

693 source_package: Optional[SourcePackage], 

694 salsa_ci: Optional[CommentedMap], 

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

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

697 if style is not None: 

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

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

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

701 

702 if salsa_ci and _read_salsa_ci_wrap_and_sort_enabled(salsa_ci): 

703 wrap_and_sort_options = salsa_ci.mlget( 

704 ["variables", "SALSA_CI_WRAP_AND_SORT_ARGS"], 

705 list_ok=True, 

706 default=None, 

707 ) 

708 if wrap_and_sort_options is None: 

709 wrap_and_sort_options = "" 

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

711 return ( 

712 None, 

713 None, 

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

715 ) 

716 detected_style = parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options) 

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

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

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

720 else: 

721 msg = None 

722 return detected_style, tool_w_args, msg 

723 if source_package is None: 

724 return None, None, None 

725 

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

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

728 return None, None, None 

729 maint_email = extract_maint_email(maint) 

730 maint_pref = maint_preference_table.maintainer_preferences.get(maint_email) 

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

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

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

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

735 return None, None, None 

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

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

738 # team's style applies unconditionally. 

739 effective = maint_pref.formatting 

740 tool_w_args = _guess_tool_from_style(maint_preference_table, effective) 

741 return effective, tool_w_args, None 

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

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

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

745 tool_w_args = _guess_tool_from_style(maint_preference_table, detected_style) 

746 return detected_style, tool_w_args, None 

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

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

749 all_styles.append(maint_pref.formatting) 

750 for uploader in _UPLOADER_SPLIT_RE.split(uploaders): 

751 uploader_email = extract_maint_email(uploader) 

752 uploader_pref = maint_preference_table.maintainer_preferences.get( 

753 uploader_email 

754 ) 

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

756 

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

758 return None, None, None 

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

760 assert not isinstance(r, MaintainerPreference) 

761 tool_w_args = _guess_tool_from_style(maint_preference_table, r) 

762 return r, tool_w_args, None 

763 

764 

765def _guess_tool_from_style( 

766 maint_preference_table: MaintainerPreferenceTable, 

767 pref: Optional[EffectiveFormattingPreference], 

768) -> Optional[str]: 

769 if pref is None: 

770 return None 

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

772 return "debputy reformat" 

773 return None 

774 

775 

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

777 for arg in args: 

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

779 yield arg 

780 continue 

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

782 yield arg 

783 continue 

784 for sarg in arg[1:]: 

785 yield f"-{sarg}" 

786 

787 

788@functools.lru_cache 

789def parse_salsa_ci_wrap_and_sort_args( 

790 args: str, 

791) -> Optional[EffectiveFormattingPreference]: 

792 options = dict(_WAS_DEFAULTS) 

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

794 v = _WAS_OPTIONS.get(arg) 

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

796 return None 

797 varname, value = v 

798 if varname is None: 

799 continue 

800 options[varname] = value 

801 if "DISABLE_NORMALIZE_STANZA_ORDER" in options: 

802 del options["DISABLE_NORMALIZE_STANZA_ORDER"] 

803 options["deb822_normalize_stanza_order"] = False 

804 

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