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