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
« 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)
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
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}
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
70 @property
71 def name(self) -> str:
72 if isinstance(self.key, str):
73 return self.key
74 return ".".join(self.key)
76 @property
77 def attribute_name(self) -> str:
78 return self.name.replace("-", "_").replace(".", "_")
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"
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 )
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"))
114def _false_when_formatting_content(m: CommentedMap) -> Optional[bool]:
115 return m.mlget(_NORMALISE_FIELD_CONTENT_KEY, list_ok=True, default=False) is True
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.
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
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.
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]
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`).
173 This roughly corresponds to `wrap-and-sort`'s `-s` option.
175 **Example**:
177 When `true`, the following:
178 ```
179 Depends: foo,
180 bar
181 ```
183 would be reformatted as:
185 ```
186 Depends:
187 foo,
188 bar
189 ```
191 (Assuming `formatting.deb822.short-indent` is `false`)
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.
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`).
209 This roughly corresponds to `wrap-and-sort`'s `-a` option.
211 **Example**:
213 When `true`, the following:
214 ```
215 Depends: foo, bar
216 ```
218 would be reformatted as:
220 ```
221 Depends: foo,
222 bar
223 ```
225 (Assuming `formatting.deb822.short-indent` is `false`)
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.
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.
245 This roughly corresponds to `wrap-and-sort`'s `-t` option.
247 **Example**:
249 When `true`, the following:
250 ```
251 Depends: foo,
252 bar
253 ```
255 would be reformatted as:
257 ```
258 Depends: foo,
259 bar,
260 ```
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.
276 This roughly corresponds to `wrap-and-sort`'s `--max-line-length` option.
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.
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.
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.
299 **Example**:
301 When `true`, the following:
302 ```
303 Depends: foo,
304 bar|baz
305 ```
307 would be reformatted as:
309 ```
310 Depends: bar | baz,
311 foo,
312 ```
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.
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.
332 There is no `wrap-and-sort` feature matching this.
334 **Example**:
336 When `true`, the following:
337 ```
338 Depends: bar
339 Package: foo
340 ```
342 would be reformatted as:
344 ```
345 Depends: foo
346 Package: bar
347 ```
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.
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.
367 This roughly corresponds to `wrap-and-sort`'s `-kb` feature except this may apply to other deb822
368 files.
370 **Example**:
372 When `true`, the following:
373 ```
374 Source: zzbar
376 Package: zzbar
378 Package: zzbar-util
380 Package: libzzbar-dev
382 Package: libzzbar2
383 ```
385 would be reformatted as:
387 ```
388 Source: zzbar
390 Package: zzbar
392 Package: libzzbar2
394 Package: libzzbar-dev
396 Package: zzbar-util
397 ```
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
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]
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
422 @classmethod
423 def from_file(
424 cls,
425 filename: str,
426 key: str,
427 styles: CommentedMap,
428 ) -> Self:
429 attr = {}
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
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
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
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 )
466 def replace(self, /, **changes: Any) -> Self:
467 return dataclasses.replace(self, **changes)
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
476 @classmethod
477 def from_file(
478 cls,
479 filename: str,
480 key: str,
481 styles: CommentedMap,
482 ) -> Self:
483 attr = {}
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
493class MaintainerPreferenceTable:
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
503 @classmethod
504 def load_preferences(cls) -> Self:
505 named_styles: Dict[str, EffectiveFormattingPreference] = {}
506 maintainer_preferences: Dict[str, MaintainerPreference] = {}
508 path = importlib.resources.files(data_dir).joinpath(BUILTIN_STYLES)
510 with path.open("r", encoding="utf-8") as fd:
511 parse_file(named_styles, maintainer_preferences, str(path), fd)
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 )
522 # TODO: Support fetching styles online to pull them in faster than waiting for a stable release.
523 return cls(named_styles, maintainer_preferences)
525 @property
526 def named_styles(self) -> Mapping[str, EffectiveFormattingPreference]:
527 return self._named_styles
529 @property
530 def maintainer_preferences(self) -> Mapping[str, MaintainerPreference]:
531 return self._maintainer_preferences
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 = {}
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
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 )
595 maintainer_preferences[maintainer_email] = mp
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 ""
603 try:
604 idx = maint.index("<")
605 except ValueError:
606 return ""
607 return maint[idx + 1 : -1]
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
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 )
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
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
642 disable_wrap_and_sort = _parse_salsa_ci_boolean(disable_wrap_and_sort_raw)
643 return not disable_wrap_and_sort
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
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
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)
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
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
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}"
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
760 return EffectiveFormattingPreference(**options) # type: ignore