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
« 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)
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
31PT = TypeVar("PT", bool, str, int)
34BUILTIN_STYLES = "maint-preferences.yaml"
36_NORMALISE_FIELD_CONTENT_KEY = ["deb822", "normalize-field-content"]
37_UPLOADER_SPLIT_RE = re.compile(r"(?<=>)\s*,")
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}
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}
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
71 @property
72 def name(self) -> str:
73 if isinstance(self.key, str):
74 return self.key
75 return ".".join(self.key)
77 @property
78 def attribute_name(self) -> str:
79 return self.name.replace("-", "_").replace(".", "_")
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"
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 )
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"))
115def _false_when_formatting_content(m: CommentedMap) -> Optional[bool]:
116 return m.mlget(_NORMALISE_FIELD_CONTENT_KEY, list_ok=True, default=False) is True
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.
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
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.
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]
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`).
174 This roughly corresponds to `wrap-and-sort`'s `-s` option.
176 **Example**:
178 When `true`, the following:
179 ```
180 Depends: foo,
181 bar
182 ```
184 would be reformatted as:
186 ```
187 Depends:
188 foo,
189 bar
190 ```
192 (Assuming `formatting.deb822.short-indent` is `false`)
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.
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`).
210 This roughly corresponds to `wrap-and-sort`'s `-a` option.
212 **Example**:
214 When `true`, the following:
215 ```
216 Depends: foo, bar
217 ```
219 would be reformatted as:
221 ```
222 Depends: foo,
223 bar
224 ```
226 (Assuming `formatting.deb822.short-indent` is `false`)
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.
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.
246 This roughly corresponds to `wrap-and-sort`'s `-t` option.
248 **Example**:
250 When `true`, the following:
251 ```
252 Depends: foo,
253 bar
254 ```
256 would be reformatted as:
258 ```
259 Depends: foo,
260 bar,
261 ```
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.
277 This roughly corresponds to `wrap-and-sort`'s `--max-line-length` option.
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.
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.
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.
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.
307 **Example**:
309 When `true`, the following:
310 ```
311 Depends: foo,
312 bar|baz
313 ```
315 would be reformatted as:
317 ```
318 Depends: bar | baz,
319 foo,
320 ```
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.
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.
340 There is no `wrap-and-sort` feature matching this.
342 **Example**:
344 When `true`, the following:
345 ```
346 Depends: bar
347 Package: foo
348 ```
350 would be reformatted as:
352 ```
353 Depends: foo
354 Package: bar
355 ```
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.
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.
375 This roughly corresponds to `wrap-and-sort`'s `-kb` feature except this may apply to other deb822
376 files.
378 **Example**:
380 When `true`, the following:
381 ```
382 Source: zzbar
384 Package: zzbar
386 Package: zzbar-util
388 Package: libzzbar-dev
390 Package: libzzbar2
391 ```
393 would be reformatted as:
395 ```
396 Source: zzbar
398 Package: zzbar
400 Package: libzzbar2
402 Package: libzzbar-dev
404 Package: zzbar-util
405 ```
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
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.
425 This causes formatting to align known fields in `deb822` files with their
426 canonical spelling. As an examples:
428 ```
429 source: foo
430 RULES-REQUIRES-ROOT: no
431 ```
433 Would be reformatted as:
435 ```
436 Source: foo
437 Rules-Requires-Root: no
438 ```
440 The formatting only applies when the canonical spelling of the field is known
441 to `debputy`. Unknown fields retain their original casing/formatting.
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]
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
467 @classmethod
468 def from_file(
469 cls,
470 filename: str,
471 key: str,
472 styles: CommentedMap,
473 ) -> Self:
474 attr = {}
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
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
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
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 )
511 def replace(self, /, **changes: Any) -> Self:
512 return dataclasses.replace(self, **changes)
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
521 @classmethod
522 def from_file(
523 cls,
524 filename: str,
525 key: str,
526 styles: CommentedMap,
527 ) -> Self:
528 attr = {}
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
538class MaintainerPreferenceTable:
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
548 @classmethod
549 def load_preferences(cls) -> Self:
550 named_styles: Dict[str, EffectiveFormattingPreference] = {}
551 maintainer_preferences: Dict[str, MaintainerPreference] = {}
553 path = importlib.resources.files(data_dir).joinpath(BUILTIN_STYLES)
555 with path.open("r", encoding="utf-8") as fd:
556 parse_file(named_styles, maintainer_preferences, str(path), fd)
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 )
567 # TODO: Support fetching styles online to pull them in faster than waiting for a stable release.
568 return cls(named_styles, maintainer_preferences)
570 @property
571 def named_styles(self) -> Mapping[str, EffectiveFormattingPreference]:
572 return self._named_styles
574 @property
575 def maintainer_preferences(self) -> Mapping[str, MaintainerPreference]:
576 return self._maintainer_preferences
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 = {}
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
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 )
640 maintainer_preferences[maintainer_email] = mp
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 ""
648 try:
649 idx = maint.index("<")
650 except ValueError:
651 return ""
652 return maint[idx + 1 : -1]
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
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 )
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
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
687 disable_wrap_and_sort = _parse_salsa_ci_boolean(disable_wrap_and_sort_raw)
688 return not disable_wrap_and_sort
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
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
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)
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
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
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}"
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
805 return EffectiveFormattingPreference(**options) # type: ignore