Coverage for src/debputy/lsp/maint_prefs.py: 82%
273 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-04-19 20:37 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-04-19 20:37 +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 Self,
14 Dict,
15 Any,
16 Tuple,
17)
18from collections.abc import Callable, Mapping, Iterable
20import debputy.lsp.data as data_dir
21from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES
22from debian._deb822_repro.types import FormatterCallback
23from debputy.lsp.vendoring.wrap_and_sort import wrap_and_sort_formatter
24from debputy.packages import SourcePackage
25from debputy.util import _error
26from debputy.yaml import MANIFEST_YAML
27from debputy.yaml.compat import CommentedMap
29PT = TypeVar("PT", bool, str, int)
32BUILTIN_STYLES = "maint-preferences.yaml"
34_NORMALISE_FIELD_CONTENT_KEY = ["deb822", "normalize-field-content"]
35_UPLOADER_SPLIT_RE = re.compile(r"(?<=>)\s*,")
37_WAS_OPTIONS = {
38 "-a": ("deb822_always_wrap", True),
39 "--always-wrap": ("deb822_always_wrap", True),
40 "-s": ("deb822_short_indent", True),
41 "--short-indent": ("deb822_short_indent", True),
42 "-t": ("deb822_trailing_separator", True),
43 "--trailing-separator": ("deb822_trailing_separator", True),
44 # Noise option for us; we do not accept `--no-keep-first` though
45 "-k": (None, True),
46 "--keep-first": (None, True),
47 "--no-keep-first": ("DISABLE_NORMALIZE_STANZA_ORDER", True),
48 "-b": ("deb822_normalize_stanza_order", True),
49 "--sort-binary-packages": ("deb822_normalize_stanza_order", True),
50}
52_WAS_DEFAULTS = {
53 "deb822_always_wrap": False,
54 "deb822_short_indent": False,
55 "deb822_trailing_separator": False,
56 "deb822_normalize_stanza_order": False,
57 "deb822_normalize_field_content": True,
58 "deb822_auto_canonical_size_field_names": False,
59}
62@dataclasses.dataclass(slots=True, frozen=True, kw_only=True)
63class PreferenceOption(Generic[PT]):
64 key: str | list[str]
65 expected_type: type[PT] | Callable[[Any], str | None]
66 description: str
67 default_value: PT | Callable[[CommentedMap], PT | None] | None = None
69 @property
70 def name(self) -> str:
71 if isinstance(self.key, str):
72 return self.key
73 return ".".join(self.key)
75 @property
76 def attribute_name(self) -> str:
77 return self.name.replace("-", "_").replace(".", "_")
79 def extract_value(
80 self,
81 filename: str,
82 key: str,
83 data: CommentedMap,
84 ) -> PT | None:
85 v = data.mlget(self.key, list_ok=True)
86 if v is None:
87 default_value = self.default_value
88 if callable(default_value):
89 return default_value(data)
90 return default_value
91 val_issue: str | None = None
92 expected_type = self.expected_type
93 if not isinstance(expected_type, type) and callable(self.expected_type):
94 val_issue = self.expected_type(v)
95 elif not isinstance(v, self.expected_type): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 val_issue = f"It should have been a {self.expected_type} but it was not"
98 if val_issue is None: 98 ↛ 100line 98 didn't jump to line 100 because the condition on line 98 was always true
99 return v
100 raise ValueError(
101 f'The value "{self.name}" for key {key} in file "{filename}" was incorrect: {val_issue}'
102 )
105def _is_packaging_team_default(m: CommentedMap) -> bool:
106 v = m.get("canonical-name")
107 if not isinstance(v, str):
108 return False
109 v = v.lower()
110 return v.endswith((" maintainer", " maintainers", " team"))
113def _false_when_formatting_content(m: CommentedMap) -> bool | None:
114 return m.mlget(_NORMALISE_FIELD_CONTENT_KEY, list_ok=True, default=False) is True
117MAINT_OPTIONS: list[PreferenceOption] = [
118 PreferenceOption(
119 key="canonical-name",
120 expected_type=str,
121 description=textwrap.dedent("""\
122 Canonical spelling/case of the maintainer name.
124 The `debputy` linter will emit a diagnostic if the name is not spelled exactly as provided here.
125 Can be useful to ensure your name is updated after a change of name.
126 """),
127 ),
128 PreferenceOption(
129 key="is-packaging-team",
130 expected_type=bool,
131 default_value=_is_packaging_team_default,
132 description=textwrap.dedent("""\
133 Whether this entry is for a packaging team
135 This affects how styles are applied when multiple maintainers (`Maintainer` + `Uploaders`) are listed
136 in `debian/control`. For package teams, the team preference prevails when the team is in the `Maintainer`
137 field. For non-packaging teams, generally the rules do not apply as soon as there are co-maintainers.
139 The default is derived from the canonical name. If said name ends with phrases like "Team" or "Maintainer"
140 then the email is assumed to be for a team by default.
141 """),
142 ),
143 PreferenceOption(
144 key="formatting",
145 expected_type=lambda x: (
146 None
147 if isinstance(x, EffectiveFormattingPreference)
148 else "It should have been a EffectiveFormattingPreference but it was not"
149 ),
150 default_value=None,
151 description=textwrap.dedent("""\
152 The formatting preference of the maintainer. Can either be a string for a named style or an inline
153 style.
154 """),
155 ),
156]
158FORMATTING_OPTIONS = [
159 PreferenceOption(
160 key=["deb822", "short-indent"],
161 expected_type=bool,
162 description=textwrap.dedent("""\
163 Whether to use "short" indents for relationship fields (such as `Depends`).
165 This roughly corresponds to `wrap-and-sort`'s `-s` option.
167 **Example**:
169 When `true`, the following:
170 ```
171 Depends: foo,
172 bar
173 ```
175 would be reformatted as:
177 ```
178 Depends:
179 foo,
180 bar
181 ```
183 (Assuming `formatting.deb822.short-indent` is `false`)
185 Note that defaults to `false` *if* (and only if) other formatting options will trigger reformat of
186 the field and this option has not been set. Setting this option can trigger reformatting of fields
187 that span multiple lines.
189 Additionally, this only triggers when a field is being reformatted. Generally that requires
190 another option such as `formatting.deb822.normalize-field-content` for that to happen.
191 """),
192 ),
193 PreferenceOption(
194 key=["deb822", "always-wrap"],
195 expected_type=bool,
196 description=textwrap.dedent("""\
197 Whether to always wrap fields (such as `Depends`).
199 This roughly corresponds to `wrap-and-sort`'s `-a` option.
201 **Example**:
203 When `true`, the following:
204 ```
205 Depends: foo, bar
206 ```
208 would be reformatted as:
210 ```
211 Depends: foo,
212 bar
213 ```
215 (Assuming `formatting.deb822.short-indent` is `false`)
217 This option only applies to fields where formatting is a pure style preference. As an
218 example, `Description` (`debian/control`) or `License` (`debian/copyright`) will not
219 be affected by this option.
221 Note: When `true`, this option overrules `formatting.deb822.max-line-length` when they interact.
222 Additionally, this only triggers when a field is being reformatted. Generally that requires
223 another option such as `formatting.deb822.normalize-field-content` for that to happen.
224 """),
225 ),
226 PreferenceOption(
227 key=["deb822", "trailing-separator"],
228 expected_type=bool,
229 default_value=False,
230 description=textwrap.dedent("""\
231 Whether to always end relationship fields (such as `Depends`) with a trailing separator.
233 This roughly corresponds to `wrap-and-sort`'s `-t` option.
235 **Example**:
237 When `true`, the following:
238 ```
239 Depends: foo,
240 bar
241 ```
243 would be reformatted as:
245 ```
246 Depends: foo,
247 bar,
248 ```
250 Note: The trailing separator is only applied if the field is reformatted. This means this option
251 generally requires another option to trigger reformatting (like
252 `formatting.deb822.normalize-field-content`).
253 """),
254 ),
255 PreferenceOption(
256 key=["deb822", "max-line-length"],
257 expected_type=int,
258 default_value=79,
259 description=textwrap.dedent("""\
260 How long a value line can be before it should be line wrapped.
262 This roughly corresponds to `wrap-and-sort`'s `--max-line-length` option.
264 This option only applies to fields where formatting is a pure style preference. As an
265 example, `Description` (`debian/control`) or `License` (`debian/copyright`) will not
266 be affected by this option.
268 This setting may affect style-related diagnostics. Notably, many tools with linting or
269 diagnostics capabilities (`debputy` included) will generally flag lines of 80 or more
270 characters as suboptimal. The diagnostics are because some fields are displayed to end
271 users in settings where 80 character-wide displays are or were the norm with no or
272 only awkward horizontal scrolling options available. Using a value higher than the
273 default may cause undesired diagnostics.
275 Note: When `formatting.deb822.always-wrap` is `true`, then this option will be overruled.
276 Additionally, this only triggers when a field is being reformatted. Generally that requires
277 another option such as `formatting.deb822.normalize-field-content` for that to happen.
278 """),
279 ),
280 PreferenceOption(
281 key=_NORMALISE_FIELD_CONTENT_KEY,
282 expected_type=bool,
283 default_value=False,
284 description=textwrap.dedent("""\
285 Whether to normalize field content.
287 This roughly corresponds to the subset of `wrap-and-sort` that normalizes field content
288 like sorting and normalizing relations or sorting the architecture field.
290 **Example**:
292 When `true`, the following:
293 ```
294 Depends: foo,
295 bar|baz
296 ```
298 would be reformatted as:
300 ```
301 Depends: bar | baz,
302 foo,
303 ```
305 This causes affected fields to always be rewritten and therefore be sure that other options
306 such as `formatting.deb822.short-indent` or `formatting.deb822.always-wrap` is set according
307 to taste.
309 Note: The field may be rewritten without this being set to `true`. As an example, the `always-wrap`
310 option can trigger a field rewrite. However, in that case, the values (including any internal whitespace)
311 are left as-is while the whitespace normalization between the values is still applied.
312 """),
313 ),
314 PreferenceOption(
315 key=["deb822", "normalize-field-order"],
316 expected_type=bool,
317 default_value=False,
318 description=textwrap.dedent("""\
319 Whether to normalize field order in a stanza.
321 There is no `wrap-and-sort` feature matching this.
323 **Example**:
325 When `true`, the following:
326 ```
327 Depends: bar
328 Package: foo
329 ```
331 would be reformatted as:
333 ```
334 Depends: foo
335 Package: bar
336 ```
338 The field order is not by field name but by a logic order defined in `debputy` based on existing
339 conventions. The `deb822` format does not dictate any field order inside stanzas in general, so
340 reordering of fields is generally safe.
342 If a field of the first stanza is known to be a format discriminator such as the `Format' in
343 `debian/copyright`, then it will be put first. Generally that matches existing convention plus
344 it maximizes the odds that existing tools will correctly identify the file format.
345 """),
346 ),
347 PreferenceOption(
348 key=["deb822", "normalize-stanza-order"],
349 expected_type=bool,
350 default_value=False,
351 description=textwrap.dedent("""\
352 Whether to normalize stanza order in a file.
354 This roughly corresponds to `wrap-and-sort`'s `-kb` feature except this may apply to other deb822
355 files.
357 **Example**:
359 When `true`, the following:
360 ```
361 Source: zzbar
363 Package: zzbar
365 Package: zzbar-util
367 Package: libzzbar-dev
369 Package: libzzbar2
370 ```
372 would be reformatted as:
374 ```
375 Source: zzbar
377 Package: zzbar
379 Package: libzzbar2
381 Package: libzzbar-dev
383 Package: zzbar-util
384 ```
386 Reordering will only performed when:
387 1) There is a convention for a normalized order
388 2) The normalization can be performed without changing semantics
390 Note: This option only guards style/preference related re-ordering. It does not influence
391 warnings about the order being semantic incorrect (which will still be emitted regardless
392 of this setting).
393 """),
394 ),
395 PreferenceOption(
396 key=["deb822", "auto-canonical-size-field-names"],
397 expected_type=bool,
398 default_value=False,
399 description=textwrap.dedent("""\
400 Whether to canonicalize field names of known `deb822` fields.
402 This causes formatting to align known fields in `deb822` files with their
403 canonical spelling. As an examples:
405 ```
406 source: foo
407 RULES-REQUIRES-ROOT: no
408 ```
410 Would be reformatted as:
412 ```
413 Source: foo
414 Rules-Requires-Root: no
415 ```
417 The formatting only applies when the canonical spelling of the field is known
418 to `debputy`. Unknown fields retain their original casing/formatting.
420 This setting may affect style-related diagnostics. Notably, many tools with linting or
421 diagnostics capabilities (`debputy` included) will generally flag non-canonical spellings
422 of field names as suboptimal. Note that while `debputy` will only flag and correct
423 non-canonical casing of fields, some tooling may be more opinionated and flag even
424 fields they do not know using an algorithm to guess the canonical casing. Therefore,
425 even with this enabled, you can still canonical spelling related diagnostics from
426 other tooling.
427 """),
428 ),
429]
432@dataclasses.dataclass(slots=True, frozen=True)
433class EffectiveFormattingPreference:
434 deb822_short_indent: bool | None = None
435 deb822_always_wrap: bool | None = None
436 deb822_trailing_separator: bool = False
437 deb822_normalize_field_content: bool = False
438 deb822_normalize_field_order: bool = False
439 deb822_normalize_stanza_order: bool = False
440 deb822_max_line_length: int = 79
441 deb822_auto_canonical_size_field_names: bool = False
443 @classmethod
444 def from_file(
445 cls,
446 filename: str,
447 key: str,
448 styles: CommentedMap,
449 ) -> Self:
450 attr = {}
452 for option in FORMATTING_OPTIONS:
453 if not hasattr(cls, option.attribute_name): 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true
454 continue
455 value = option.extract_value(filename, key, styles)
456 attr[option.attribute_name] = value
457 return cls(**attr) # type: ignore
459 @classmethod
460 def aligned_preference(
461 cls,
462 a: Optional["EffectiveFormattingPreference"],
463 b: Optional["EffectiveFormattingPreference"],
464 ) -> Optional["EffectiveFormattingPreference"]:
465 if a is None or b is None:
466 return None
468 for option in MAINT_OPTIONS:
469 attr_name = option.attribute_name
470 if not hasattr(EffectiveFormattingPreference, attr_name): 470 ↛ 472line 470 didn't jump to line 472 because the condition on line 470 was always true
471 continue
472 a_value = getattr(a, attr_name)
473 b_value = getattr(b, attr_name)
474 if a_value != b_value:
475 return None
476 return a
478 def deb822_formatter(self) -> FormatterCallback:
479 line_length = self.deb822_max_line_length
480 return wrap_and_sort_formatter(
481 1 if self.deb822_short_indent else "FIELD_NAME_LENGTH",
482 trailing_separator=self.deb822_trailing_separator,
483 immediate_empty_line=self.deb822_short_indent or False,
484 max_line_length_one_liner=(0 if self.deb822_always_wrap else line_length),
485 )
487 def replace(self, /, **changes: Any) -> Self:
488 return dataclasses.replace(self, **changes)
491@dataclasses.dataclass(slots=True, frozen=True)
492class MaintainerPreference:
493 canonical_name: str | None = None
494 is_packaging_team: bool = False
495 formatting: EffectiveFormattingPreference | None = None
497 @classmethod
498 def from_file(
499 cls,
500 filename: str,
501 key: str,
502 styles: CommentedMap,
503 ) -> Self:
504 attr = {}
506 for option in MAINT_OPTIONS:
507 if not hasattr(cls, option.attribute_name): 507 ↛ 508line 507 didn't jump to line 508 because the condition on line 507 was never true
508 continue
509 value = option.extract_value(filename, key, styles)
510 attr[option.attribute_name] = value
511 return cls(**attr) # type: ignore
514class MaintainerPreferenceTable:
516 def __init__(
517 self,
518 named_styles: Mapping[str, EffectiveFormattingPreference],
519 maintainer_preferences: Mapping[str, MaintainerPreference],
520 ) -> None:
521 self._named_styles = named_styles
522 self._maintainer_preferences = maintainer_preferences
524 @classmethod
525 def load_preferences(cls) -> Self:
526 named_styles: dict[str, EffectiveFormattingPreference] = {}
527 maintainer_preferences: dict[str, MaintainerPreference] = {}
529 path = importlib.resources.files(data_dir).joinpath(BUILTIN_STYLES)
531 with path.open("r", encoding="utf-8") as fd:
532 parse_file(named_styles, maintainer_preferences, str(path), fd)
534 missing_keys = set(named_styles.keys()).difference(
535 ALL_PUBLIC_NAMED_STYLES.keys()
536 )
537 if missing_keys: 537 ↛ 538line 537 didn't jump to line 538 because the condition on line 537 was never true
538 missing_styles = ", ".join(sorted(missing_keys))
539 _error(
540 f"The following named styles are public API but not present in the config file: {missing_styles}"
541 )
543 # TODO: Support fetching styles online to pull them in faster than waiting for a stable release.
544 return cls(named_styles, maintainer_preferences)
546 @property
547 def named_styles(self) -> Mapping[str, EffectiveFormattingPreference]:
548 return self._named_styles
550 @property
551 def maintainer_preferences(self) -> Mapping[str, MaintainerPreference]:
552 return self._maintainer_preferences
555def parse_file(
556 named_styles: dict[str, EffectiveFormattingPreference],
557 maintainer_preferences: dict[str, MaintainerPreference],
558 filename: str,
559 fd,
560) -> None:
561 content = MANIFEST_YAML.load(fd)
562 if not isinstance(content, CommentedMap): 562 ↛ 563line 562 didn't jump to line 563 because the condition on line 562 was never true
563 raise ValueError(
564 f'The file "{filename}" should be a YAML file with a single mapping at the root'
565 )
566 try:
567 maintainer_rules = content["maintainer-rules"]
568 if not isinstance(maintainer_rules, CommentedMap): 568 ↛ 569line 568 didn't jump to line 569 because the condition on line 568 was never true
569 raise KeyError("maintainer-rules") from None
570 except KeyError:
571 raise ValueError(
572 f'The file "{filename}" should have a "maintainer-rules" key which must be a mapping.'
573 )
574 named_styles_raw = content.get("formatting")
575 if named_styles_raw is None or not isinstance(named_styles_raw, CommentedMap): 575 ↛ 576line 575 didn't jump to line 576 because the condition on line 575 was never true
576 named_styles_raw = {}
578 for style_name, content in named_styles_raw.items():
579 style = EffectiveFormattingPreference.from_file(
580 filename,
581 style_name,
582 content,
583 )
584 named_styles[style_name] = style
586 for maintainer_email, maintainer_pref in maintainer_rules.items():
587 if not isinstance(maintainer_pref, CommentedMap): 587 ↛ 588line 587 didn't jump to line 588 because the condition on line 587 was never true
588 line_no = maintainer_rules.lc.key(maintainer_email).line
589 raise ValueError(
590 f'The value for maintainer "{maintainer_email}" should have been a mapping,'
591 f' but it is not. The problem entry is at line {line_no} in "{filename}"'
592 )
593 formatting = maintainer_pref.get("formatting")
594 if isinstance(formatting, str):
595 try:
596 style = named_styles[formatting]
597 except KeyError:
598 line_no = maintainer_rules.lc.key(maintainer_email).line
599 raise ValueError(
600 f'The maintainer "{maintainer_email}" requested the named style "{formatting}",'
601 f' but said style was not defined {filename}. The problem entry is at line {line_no} in "{filename}"'
602 ) from None
603 maintainer_pref["formatting"] = style
604 elif formatting is not None: 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true
605 maintainer_pref["formatting"] = EffectiveFormattingPreference.from_file(
606 filename,
607 "formatting",
608 formatting,
609 )
610 mp = MaintainerPreference.from_file(
611 filename,
612 maintainer_email,
613 maintainer_pref,
614 )
616 maintainer_preferences[maintainer_email] = mp
619@functools.lru_cache(64)
620def extract_maint_email(maint: str) -> str:
621 if not maint.endswith(">"): 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true
622 return ""
624 try:
625 idx = maint.index("<")
626 except ValueError:
627 return ""
628 return maint[idx + 1 : -1]
631def _parse_salsa_ci_boolean(value: str | int | bool) -> bool:
632 if isinstance(value, str):
633 return value in ("yes", "1", "true")
634 elif not isinstance(value, (int, bool)): 634 ↛ 635line 634 didn't jump to line 635 because the condition on line 634 was never true
635 raise TypeError("Unsupported value")
636 else:
637 return value is True or value == 1
640def _read_salsa_ci_wrap_and_sort_enabled(salsa_ci: CommentedMap | None) -> bool:
641 sentinel = object()
642 disable_wrap_and_sort_raw = salsa_ci.mlget(
643 ["variables", "SALSA_CI_DISABLE_WRAP_AND_SORT"],
644 list_ok=True,
645 default=sentinel,
646 )
648 if disable_wrap_and_sort_raw is sentinel:
649 enable_wrap_and_sort_raw = salsa_ci.mlget(
650 ["variables", "SALSA_CI_ENABLE_WRAP_AND_SORT"],
651 list_ok=True,
652 default=None,
653 )
654 if enable_wrap_and_sort_raw is None or not isinstance(
655 enable_wrap_and_sort_raw, (str, int, bool)
656 ):
657 return False
659 return _parse_salsa_ci_boolean(enable_wrap_and_sort_raw)
660 if not isinstance(disable_wrap_and_sort_raw, (str, int, bool)):
661 return False
663 disable_wrap_and_sort = _parse_salsa_ci_boolean(disable_wrap_and_sort_raw)
664 return not disable_wrap_and_sort
667def determine_effective_preference(
668 maint_preference_table: MaintainerPreferenceTable,
669 source_package: SourcePackage | None,
670 salsa_ci: CommentedMap | None,
671) -> tuple[EffectiveFormattingPreference | None, str | None, str | None]:
672 style = source_package.fields.get("X-Style") if source_package is not None else None
673 if style is not None:
674 if style not in ALL_PUBLIC_NAMED_STYLES: 674 ↛ 675line 674 didn't jump to line 675 because the condition on line 674 was never true
675 return None, None, "X-Style contained an unknown/unsupported style"
676 return maint_preference_table.named_styles.get(style), "debputy reformat", None
678 if salsa_ci and _read_salsa_ci_wrap_and_sort_enabled(salsa_ci):
679 wrap_and_sort_options = salsa_ci.mlget(
680 ["variables", "SALSA_CI_WRAP_AND_SORT_ARGS"],
681 list_ok=True,
682 default=None,
683 )
684 if wrap_and_sort_options is None:
685 wrap_and_sort_options = ""
686 elif not isinstance(wrap_and_sort_options, str): 686 ↛ 687line 686 didn't jump to line 687 because the condition on line 686 was never true
687 return (
688 None,
689 None,
690 "The salsa-ci had a non-string option for wrap-and-sort",
691 )
692 detected_style = parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options)
693 tool_w_args = f"wrap-and-sort {wrap_and_sort_options}".strip()
694 if detected_style is None: 694 ↛ 695line 694 didn't jump to line 695 because the condition on line 694 was never true
695 msg = "One or more of the wrap-and-sort options in the salsa-ci file was not supported"
696 else:
697 msg = None
698 return detected_style, tool_w_args, msg
699 if source_package is None:
700 return None, None, None
702 maint = source_package.fields.get("Maintainer")
703 if maint is None: 703 ↛ 704line 703 didn't jump to line 704 because the condition on line 703 was never true
704 return None, None, None
705 maint_email = extract_maint_email(maint)
706 maint_pref = maint_preference_table.maintainer_preferences.get(maint_email)
707 # Special-case "@packages.debian.org" when missing, since they are likely to be "ad-hoc"
708 # teams that will not be registered. In that case, we fall back to looking at the uploader
709 # preferences as-if the maintainer had not been listed at all.
710 if maint_pref is None and not maint_email.endswith("@packages.debian.org"):
711 return None, None, None
712 if maint_pref is not None and maint_pref.is_packaging_team: 712 ↛ 715line 712 didn't jump to line 715 because the condition on line 712 was never true
713 # When the maintainer is registered as a packaging team, then we assume the packaging
714 # team's style applies unconditionally.
715 effective = maint_pref.formatting
716 tool_w_args = _guess_tool_from_style(maint_preference_table, effective)
717 return effective, tool_w_args, None
718 uploaders = source_package.fields.get("Uploaders")
719 if uploaders is None: 719 ↛ 720line 719 didn't jump to line 720 because the condition on line 719 was never true
720 detected_style = maint_pref.formatting if maint_pref is not None else None
721 tool_w_args = _guess_tool_from_style(maint_preference_table, detected_style)
722 return detected_style, tool_w_args, None
723 all_styles: list[EffectiveFormattingPreference | None] = []
724 if maint_pref is not None: 724 ↛ 725line 724 didn't jump to line 725 because the condition on line 724 was never true
725 all_styles.append(maint_pref.formatting)
726 for uploader in _UPLOADER_SPLIT_RE.split(uploaders):
727 uploader_email = extract_maint_email(uploader)
728 uploader_pref = maint_preference_table.maintainer_preferences.get(
729 uploader_email
730 )
731 all_styles.append(uploader_pref.formatting if uploader_pref else None)
733 if not all_styles: 733 ↛ 734line 733 didn't jump to line 734 because the condition on line 733 was never true
734 return None, None, None
735 r = functools.reduce(EffectiveFormattingPreference.aligned_preference, all_styles)
736 assert not isinstance(r, MaintainerPreference)
737 tool_w_args = _guess_tool_from_style(maint_preference_table, r)
738 return r, tool_w_args, None
741def _guess_tool_from_style(
742 maint_preference_table: MaintainerPreferenceTable,
743 pref: EffectiveFormattingPreference | None,
744) -> str | None:
745 if pref is None:
746 return None
747 if maint_preference_table.named_styles["black"] == pref: 747 ↛ 749line 747 didn't jump to line 749 because the condition on line 747 was always true
748 return "debputy reformat"
749 return None
752def _split_options(args: Iterable[str]) -> Iterable[str]:
753 for arg in args:
754 if arg.startswith("--"):
755 yield arg
756 continue
757 if not arg.startswith("-") or len(arg) < 2: 757 ↛ 758line 757 didn't jump to line 758 because the condition on line 757 was never true
758 yield arg
759 continue
760 for sarg in arg[1:]:
761 yield f"-{sarg}"
764@functools.lru_cache
765def parse_salsa_ci_wrap_and_sort_args(
766 args: str,
767) -> EffectiveFormattingPreference | None:
768 options = dict(_WAS_DEFAULTS)
769 for arg in _split_options(args.split()):
770 v = _WAS_OPTIONS.get(arg)
771 if v is None: 771 ↛ 772line 771 didn't jump to line 772 because the condition on line 771 was never true
772 return None
773 varname, value = v
774 if varname is None:
775 continue
776 options[varname] = value
777 if "DISABLE_NORMALIZE_STANZA_ORDER" in options:
778 del options["DISABLE_NORMALIZE_STANZA_ORDER"]
779 options["deb822_normalize_stanza_order"] = False
781 return EffectiveFormattingPreference(**options) # type: ignore