Coverage for src/debputy/commands/debputy_cmd/plugin_cmds.py: 12%
492 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 argparse
2import dataclasses
3import operator
4import os
5import sys
6import textwrap
7from itertools import chain
8from typing import (
9 Sequence,
10 Union,
11 Tuple,
12 Iterable,
13 Any,
14 Optional,
15 Type,
16 Callable,
17)
19from debputy.analysis.analysis_util import flatten_ppfs
20from debputy.commands.debputy_cmd.context import (
21 CommandContext,
22 add_arg,
23 ROOT_COMMAND,
24)
25from debputy.commands.debputy_cmd.output import (
26 _stream_to_pager,
27 _output_styling,
28 IOBasedOutputStyling,
29)
30from debputy.exceptions import DebputySubstitutionError
31from debputy.filesystem_scan import build_virtual_fs
32from debputy.manifest_parser.declarative_parser import (
33 BASIC_SIMPLE_TYPES,
34)
35from debputy.manifest_parser.parser_doc import (
36 render_rule,
37 render_multiline_documentation,
38 render_type_mapping,
39 render_source_type,
40)
41from debputy.manifest_parser.util import unpack_type
42from debputy.packager_provided_files import detect_all_packager_provided_files
43from debputy.plugin.api.doc_parsing import parser_type_name
44from debputy.plugin.api.example_processing import (
45 process_discard_rule_example,
46 DiscardVerdict,
47)
48from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
49from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin
50from debputy.plugin.api.impl_types import (
51 PackagerProvidedFileClassSpec,
52 PluginProvidedManifestVariable,
53 DispatchingParserBase,
54 PluginProvidedDiscardRule,
55 AutomaticDiscardRuleExample,
56 MetadataOrMaintscriptDetector,
57 DebputyPluginMetadata,
58 DeclarativeInputParser,
59)
60from debputy.plugin.api.parser_tables import (
61 SUPPORTED_DISPATCHABLE_TABLE_PARSERS,
62 OPARSER_MANIFEST_ROOT,
63)
64from debputy.substitution import Substitution
65from debputy.util import _error, _warn, manifest_format_doc
67plugin_dispatcher = ROOT_COMMAND.add_dispatching_subcommand(
68 "plugin",
69 "plugin_subcommand",
70 default_subcommand="--help",
71 help_description="Interact with debputy plugins",
72 metavar="command",
73)
75plugin_list_cmds = plugin_dispatcher.add_dispatching_subcommand(
76 "list",
77 "plugin_subcommand_list",
78 metavar="topic",
79 default_subcommand="plugins",
80 help_description="List plugins or things provided by plugins (unstable format)."
81 " Pass `--help` *after* `list` get a topic listing",
82)
84plugin_show_cmds = plugin_dispatcher.add_dispatching_subcommand(
85 "show",
86 "plugin_subcommand_show",
87 metavar="topic",
88 help_description="Show details about a plugin or things provided by plugins (unstable format)."
89 " Pass `--help` *after* `show` get a topic listing",
90)
93def format_output_arg(
94 default_format: str,
95 allowed_formats: Sequence[str],
96 help_text: str,
97) -> Callable[[argparse.ArgumentParser], None]:
98 if default_format not in allowed_formats: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true
99 raise ValueError("The default format must be in the allowed_formats...")
101 def _configurator(argparser: argparse.ArgumentParser) -> None:
102 argparser.add_argument(
103 "--output-format",
104 dest="output_format",
105 default=default_format,
106 choices=allowed_formats,
107 help=help_text,
108 )
110 return _configurator
113# To let --output-format=... "always" work
114TEXT_ONLY_FORMAT = format_output_arg(
115 "text",
116 ["text"],
117 "Select a given output format (options and output are not stable between releases)",
118)
121TEXT_CSV_FORMAT_NO_STABILITY_PROMISE = format_output_arg(
122 "text",
123 ["text", "csv"],
124 "Select a given output format (options and output are not stable between releases)",
125)
128@plugin_list_cmds.register_subcommand(
129 "plugins",
130 help_description="List known plugins",
131 argparser=TEXT_CSV_FORMAT_NO_STABILITY_PROMISE,
132)
133def _plugin_cmd_list_plugins(context: CommandContext) -> None:
134 plugin_metadata_entries = context.load_plugins().plugin_data.values()
135 # Because the "plugins" part is optional, we are not guaranteed that TEXT_CSV_FORMAT applies
136 output_format = getattr(context.parsed_args, "output_format", "text")
137 assert output_format in {"text", "csv"}
138 with _stream_to_pager(context.parsed_args) as (fd, fo):
139 fo.print_list_table(
140 ["Plugin Name", "Plugin Path"],
141 [(p.plugin_name, p.plugin_path) for p in plugin_metadata_entries],
142 )
145def _path(path: str) -> str:
146 if path.startswith("./"):
147 return path[1:]
148 return path
151def _ppf_flags(ppf: PackagerProvidedFileClassSpec) -> str:
152 flags = []
153 if ppf.allow_name_segment:
154 flags.append("named")
155 if ppf.allow_architecture_segment:
156 flags.append("arch")
157 if ppf.supports_priority:
158 flags.append(f"priority={ppf.default_priority}")
159 if ppf.packageless_is_fallback_for_all_packages:
160 flags.append("main-all-fallback")
161 if ppf.post_formatting_rewrite:
162 flags.append("post-format-hook")
163 return ",".join(flags)
166@plugin_list_cmds.register_subcommand(
167 ["used-packager-provided-files", "uppf", "u-p-p-f"],
168 help_description="List packager provided files used by this package (debian/pkg.foo)",
169 argparser=TEXT_ONLY_FORMAT,
170)
171def _plugin_cmd_list_uppf(context: CommandContext) -> None:
172 plugin_feature_set = context.load_plugins()
173 all_ppfs = detect_all_packager_provided_files(
174 plugin_feature_set,
175 context.debian_dir,
176 context.binary_packages(),
177 )
178 requested_plugins = set(context.requested_plugins())
179 requested_plugins.add("debputy")
180 all_detected_ppfs = list(flatten_ppfs(all_ppfs))
182 used_ppfs = [
183 p
184 for p in all_detected_ppfs
185 if p.definition.debputy_plugin_metadata.plugin_name in requested_plugins
186 ]
187 inactive_ppfs = [
188 p
189 for p in all_detected_ppfs
190 if p.definition.debputy_plugin_metadata.plugin_name not in requested_plugins
191 ]
193 if not used_ppfs and not inactive_ppfs:
194 print("No packager provided files detected; not even a changelog... ?")
195 return
197 with _stream_to_pager(context.parsed_args) as (fd, fo):
198 if used_ppfs:
199 headers: Sequence[Union[str, Tuple[str, str]]] = [
200 "File",
201 "Matched Stem",
202 "Installed Into",
203 "Installed As",
204 ]
205 fo.print_list_table(
206 headers,
207 [
208 (
209 ppf.path.path,
210 ppf.definition.stem,
211 ppf.package_name,
212 "/".join(ppf.compute_dest()).lstrip("."),
213 )
214 for ppf in sorted(
215 used_ppfs, key=operator.attrgetter("package_name")
216 )
217 ],
218 )
220 if inactive_ppfs:
221 headers: Sequence[Union[str, Tuple[str, str]]] = [
222 "UNUSED FILE",
223 "Matched Stem",
224 "Installed Into",
225 "Could Be Installed As",
226 "If B-D Had",
227 ]
228 fo.print_list_table(
229 headers,
230 [
231 (
232 f"~{ppf.path.path}~",
233 ppf.definition.stem,
234 f"~{ppf.package_name}~",
235 "/".join(ppf.compute_dest()).lstrip("."),
236 f"debputy-plugin-{ppf.definition.debputy_plugin_metadata.plugin_name}",
237 )
238 for ppf in sorted(
239 inactive_ppfs, key=operator.attrgetter("package_name")
240 )
241 ],
242 )
245@plugin_list_cmds.register_subcommand(
246 ["packager-provided-files", "ppf", "p-p-f"],
247 help_description="List packager provided file definitions (debian/pkg.foo)",
248 argparser=TEXT_CSV_FORMAT_NO_STABILITY_PROMISE,
249)
250def _plugin_cmd_list_ppf(context: CommandContext) -> None:
251 ppfs: Iterable[PackagerProvidedFileClassSpec]
252 ppfs = context.load_plugins().packager_provided_files.values()
253 with _stream_to_pager(context.parsed_args) as (fd, fo):
254 headers: Sequence[Union[str, Tuple[str, str]]] = [
255 "Stem",
256 "Installed As",
257 ("Mode", ">"),
258 "Features",
259 "Provided by",
260 ]
261 fo.print_list_table(
262 headers,
263 [
264 (
265 ppf.stem,
266 _path(ppf.installed_as_format),
267 "0" + oct(ppf.default_mode)[2:],
268 _ppf_flags(ppf),
269 ppf.debputy_plugin_metadata.plugin_name,
270 )
271 for ppf in sorted(ppfs, key=operator.attrgetter("stem"))
272 ],
273 )
275 if os.path.isdir("debian/") and fo.output_format == "text":
276 fo.print()
277 fo.print(
278 "Hint: You can use `debputy plugin list used-packager-provided-files` to have `debputy`",
279 )
280 fo.print("list all the files in debian/ that matches these definitions.")
283@plugin_list_cmds.register_subcommand(
284 ["metadata-detectors"],
285 help_description="List metadata detectors",
286 argparser=TEXT_CSV_FORMAT_NO_STABILITY_PROMISE,
287)
288def _plugin_cmd_list_metadata_detectors(context: CommandContext) -> None:
289 mds = list(
290 chain.from_iterable(
291 context.load_plugins().metadata_maintscript_detectors.values()
292 )
293 )
295 def _sort_key(md: "MetadataOrMaintscriptDetector") -> Any:
296 return md.plugin_metadata.plugin_name, md.detector_id
298 with _stream_to_pager(context.parsed_args) as (fd, fo):
299 fo.print_list_table(
300 ["Provided by", "Detector Id"],
301 [
302 (md.plugin_metadata.plugin_name, md.detector_id)
303 for md in sorted(mds, key=_sort_key)
304 ],
305 )
308def _resolve_variable_for_list(
309 substitution: Substitution,
310 variable: PluginProvidedManifestVariable,
311) -> str:
312 var = "{{" + variable.variable_name + "}}"
313 try:
314 value = substitution.substitute(var, "CLI request")
315 except DebputySubstitutionError:
316 value = None
317 return _render_manifest_variable_value(value)
320def _render_manifest_variable_flag(variable: PluginProvidedManifestVariable) -> str:
321 flags = []
322 if variable.is_for_special_case:
323 flags.append("special-use-case")
324 if variable.is_internal:
325 flags.append("internal")
326 return ",".join(flags)
329def _render_list_filter(v: Optional[bool]) -> str:
330 if v is None:
331 return "N/A"
332 return "shown" if v else "hidden"
335@plugin_list_cmds.register_subcommand(
336 ["manifest-variables"],
337 help_description="List plugin provided manifest variables (such as `{{path:FOO}}`)",
338)
339def plugin_cmd_list_manifest_variables(context: CommandContext) -> None:
340 variables = context.load_plugins().manifest_variables
341 substitution = context.substitution.with_extra_substitutions(
342 PACKAGE="<package-name>"
343 )
344 parsed_args = context.parsed_args
345 show_special_case_vars = parsed_args.show_special_use_variables
346 show_token_vars = parsed_args.show_token_variables
347 show_all_vars = parsed_args.show_all_variables
349 def _include_var(var: PluginProvidedManifestVariable) -> bool:
350 if show_all_vars:
351 return True
352 if var.is_internal:
353 return False
354 if var.is_for_special_case and not show_special_case_vars:
355 return False
356 if var.is_token and not show_token_vars:
357 return False
358 return True
360 with _stream_to_pager(context.parsed_args) as (fd, fo):
361 fo.print_list_table(
362 ["Variable (use via: `{{ NAME }}`)", "Value", "Flag", "Provided by"],
363 [
364 (
365 k,
366 _resolve_variable_for_list(substitution, var),
367 _render_manifest_variable_flag(var),
368 var.plugin_metadata.plugin_name,
369 )
370 for k, var in sorted(variables.items())
371 if _include_var(var)
372 ],
373 )
375 fo.print()
377 filters = [
378 (
379 "Token variables",
380 show_token_vars if not show_all_vars else None,
381 "--show-token-variables",
382 ),
383 (
384 "Special use variables",
385 show_special_case_vars if not show_all_vars else None,
386 "--show-special-case-variables",
387 ),
388 ]
390 fo.print_list_table(
391 ["Variable type", "Value", "Option"],
392 [
393 (
394 fname,
395 _render_list_filter(value or show_all_vars),
396 f"{option} OR --show-all-variables",
397 )
398 for fname, value, option in filters
399 ],
400 )
403@plugin_cmd_list_manifest_variables.configure_handler
404def list_manifest_variable_arg_parser(
405 plugin_list_manifest_variables_parser: argparse.ArgumentParser,
406) -> None:
407 plugin_list_manifest_variables_parser.add_argument(
408 "--show-special-case-variables",
409 dest="show_special_use_variables",
410 default=False,
411 action="store_true",
412 help="Show variables that are only used in special / niche cases",
413 )
414 plugin_list_manifest_variables_parser.add_argument(
415 "--show-token-variables",
416 dest="show_token_variables",
417 default=False,
418 action="store_true",
419 help="Show token (syntactical) variables like {{token:TAB}}",
420 )
421 plugin_list_manifest_variables_parser.add_argument(
422 "--show-all-variables",
423 dest="show_all_variables",
424 default=False,
425 action="store_true",
426 help="Show all variables regardless of type/kind (overrules other filter settings)",
427 )
428 TEXT_ONLY_FORMAT(plugin_list_manifest_variables_parser)
431@plugin_list_cmds.register_subcommand(
432 ["pluggable-manifest-rules", "p-m-r", "pmr"],
433 help_description="Pluggable manifest rules (such as install rules)",
434 argparser=TEXT_CSV_FORMAT_NO_STABILITY_PROMISE,
435)
436def _plugin_cmd_list_manifest_rules(context: CommandContext) -> None:
437 feature_set = context.load_plugins()
439 # Type hint to make the chain call easier for the type checker, which does not seem
440 # to derive to this common base type on its own.
441 base_type = Iterable[Tuple[Union[str, Type[Any]], DispatchingParserBase[Any]]]
443 parser_generator = feature_set.manifest_parser_generator
444 table_parsers: base_type = parser_generator.dispatchable_table_parsers.items()
445 object_parsers: base_type = parser_generator.dispatchable_object_parsers.items()
447 parsers = chain(
448 table_parsers,
449 object_parsers,
450 )
452 with _stream_to_pager(context.parsed_args) as (fd, fo):
453 fo.print_list_table(
454 ["Rule Name", "Rule Type", "Provided By"],
455 [
456 (
457 rn,
458 parser_type_name(rt),
459 pt.parser_for(rn).plugin_metadata.plugin_name,
460 )
461 for rt, pt in parsers
462 for rn in pt.registered_keywords()
463 ],
464 )
467@plugin_list_cmds.register_subcommand(
468 ["automatic-discard-rules", "a-d-r"],
469 help_description="List automatic discard rules",
470 argparser=TEXT_CSV_FORMAT_NO_STABILITY_PROMISE,
471)
472def _plugin_cmd_list_automatic_discard_rules(context: CommandContext) -> None:
473 auto_discard_rules = context.load_plugins().auto_discard_rules
475 with _stream_to_pager(context.parsed_args) as (fd, fo):
476 fo.print_list_table(
477 ["Name", "Provided By"],
478 [
479 (
480 name,
481 ppdr.plugin_metadata.plugin_name,
482 )
483 for name, ppdr in auto_discard_rules.items()
484 ],
485 )
488def _render_manifest_variable_value(v: Optional[str]) -> str:
489 if v is None:
490 return "(N/A: Cannot resolve the variable)"
491 v = v.replace("\n", "\\n").replace("\t", "\\t")
492 return v
495@plugin_show_cmds.register_subcommand(
496 ["manifest-variables"],
497 help_description="Plugin provided manifest variables (such as `{{path:FOO}}`)",
498 argparser=add_arg(
499 "manifest_variable",
500 metavar="manifest-variable",
501 help="Name of the variable (such as `path:FOO` or `{{path:FOO}}`) to display details about",
502 ),
503)
504def _plugin_cmd_show_manifest_variables(context: CommandContext) -> None:
505 plugin_feature_set = context.load_plugins()
506 variables = plugin_feature_set.manifest_variables
507 substitution = context.substitution
508 parsed_args = context.parsed_args
509 variable_name = parsed_args.manifest_variable
510 fo = _output_styling(context.parsed_args, sys.stdout)
511 if variable_name.startswith("{{") and variable_name.endswith("}}"):
512 variable_name = variable_name[2:-2]
513 variable: Optional[PluginProvidedManifestVariable]
514 if variable_name.startswith("env:") and len(variable_name) > 4:
515 env_var = variable_name[4:]
516 variable = PluginProvidedManifestVariable(
517 plugin_feature_set.plugin_data["debputy"],
518 variable_name,
519 variable_value=None,
520 is_context_specific_variable=False,
521 is_documentation_placeholder=True,
522 variable_reference_documentation=textwrap.dedent(
523 f"""\
524 Environment variable "{env_var}"
526 Note that uses beneath `builds:` may use the environment variable defined by
527 `build-environment:` (depends on whether the rule uses eager or lazy
528 substitution) while uses outside `builds:` will generally not use a definition
529 from `build-environment:`.
530 """
531 ),
532 )
533 else:
534 variable = variables.get(variable_name)
535 if variable is None:
536 _error(
537 f'Cannot resolve "{variable_name}" as a known variable from any of the available'
538 f" plugins. Please use `debputy plugin list manifest-variables` to list all known"
539 f" provided variables."
540 )
542 var_with_braces = "{{" + variable_name + "}}"
543 try:
544 source_value = substitution.substitute(var_with_braces, "CLI request")
545 except DebputySubstitutionError:
546 source_value = None
547 binary_value = source_value
548 print(f"Variable: {variable_name}")
549 fo.print_visual_formatting(f"=========={'=' * len(variable_name)}")
550 print()
552 if variable.is_context_specific_variable:
553 try:
554 binary_value = substitution.with_extra_substitutions(
555 PACKAGE="<package-name>",
556 ).substitute(var_with_braces, "CLI request")
557 except DebputySubstitutionError:
558 binary_value = None
560 doc = variable.variable_reference_documentation or "No documentation provided"
561 for line in render_multiline_documentation(doc):
562 print(line)
564 if source_value == binary_value:
565 print(f"Resolved: {_render_manifest_variable_value(source_value)}")
566 else:
567 print("Resolved:")
568 print(f" [source context]: {_render_manifest_variable_value(source_value)}")
569 print(f" [binary context]: {_render_manifest_variable_value(binary_value)}")
571 if variable.is_for_special_case:
572 print(
573 'Special-case: The variable has been marked as a "special-case"-only variable.'
574 )
576 if not variable.is_documentation_placeholder:
577 print(f"Plugin: {variable.plugin_metadata.plugin_name}")
579 if variable.is_internal:
580 print()
581 # I knew everything I felt was showing on my face, and I hate that. I grated out,
582 print("That was private.")
585def _determine_ppf(
586 context: CommandContext,
587) -> Tuple[PackagerProvidedFileClassSpec, bool]:
588 feature_set = context.load_plugins()
589 ppf_name = context.parsed_args.ppf_name
590 try:
591 return feature_set.packager_provided_files[ppf_name], False
592 except KeyError:
593 pass
595 orig_ppf_name = ppf_name
596 if (
597 ppf_name.startswith("d/")
598 and not os.path.lexists(ppf_name)
599 and os.path.lexists("debian/" + ppf_name[2:])
600 ):
601 ppf_name = "debian/" + ppf_name[2:]
603 if ppf_name in ("debian/control", "debian/debputy.manifest", "debian/rules"):
604 if ppf_name == "debian/debputy.manifest":
605 doc = manifest_format_doc("")
606 else:
607 doc = "Debian Policy Manual or a packaging tutorial"
608 _error(
609 f"Sorry. While {orig_ppf_name} is a well-defined packaging file, it does not match the definition of"
610 f" a packager provided file. Please see {doc} for more information about this file"
611 )
613 if context.has_dctrl_file and os.path.lexists(ppf_name):
614 basename = ppf_name[7:]
615 if "/" not in basename:
616 debian_dir = build_virtual_fs([basename])
617 all_ppfs = detect_all_packager_provided_files(
618 feature_set,
619 debian_dir,
620 context.binary_packages(),
621 )
622 if all_ppfs:
623 matched = next(iter(all_ppfs.values()))
624 if len(matched.auto_installable) == 1 and not matched.reserved_only:
625 return matched.auto_installable[0].definition, True
626 if not matched.auto_installable and len(matched.reserved_only) == 1:
627 reserved = next(iter(matched.reserved_only.values()))
628 if len(reserved) == 1:
629 return reserved[0].definition, True
631 _error(
632 f'Unknown packager provided file "{orig_ppf_name}". Please use'
633 f" `debputy plugin list packager-provided-files` to see them all."
634 )
637@plugin_show_cmds.register_subcommand(
638 ["packager-provided-files", "ppf", "p-p-f"],
639 help_description="Show details about a given packager provided file (debian/pkg.foo)",
640 argparser=add_arg(
641 "ppf_name",
642 metavar="name",
643 help="Name of the packager provided file (such as `changelog`) to display details about",
644 ),
645)
646def _plugin_cmd_show_ppf(context: CommandContext) -> None:
647 ppf, matched_file = _determine_ppf(context)
649 fo = _output_styling(context.parsed_args, sys.stdout)
651 fo.print(f"Packager Provided File: {ppf.stem}")
652 fo.print_visual_formatting(f"========================{'=' * len(ppf.stem)}")
653 fo.print()
654 ref_doc = ppf.reference_documentation
655 description = ref_doc.description if ref_doc else None
656 doc_uris = ref_doc.format_documentation_uris if ref_doc else tuple()
657 if description is None:
658 fo.print(
659 f"Sorry, no description provided by the plugin {ppf.debputy_plugin_metadata.plugin_name}."
660 )
661 else:
662 for line in description.splitlines(keepends=False):
663 fo.print(line)
665 fo.print()
666 fo.print("Features:")
667 if ppf.packageless_is_fallback_for_all_packages:
668 fo.print(f" * debian/{ppf.stem} is used for *ALL* packages")
669 else:
670 fo.print(f' * debian/{ppf.stem} is used for only for the "main" package')
671 if ppf.allow_name_segment:
672 fo.print(" * Supports naming segment (multiple files and custom naming).")
673 else:
674 fo.print(
675 " * No naming support; at most one per package and it is named after the package."
676 )
677 if ppf.allow_architecture_segment:
678 fo.print(" * Supports architecture specific variants.")
679 else:
680 fo.print(" * No architecture specific variants.")
681 if ppf.supports_priority:
682 fo.print(
683 f" * Has a priority system (default priority: {ppf.default_priority})."
684 )
686 fo.print()
687 fo.print("Examples matches:")
689 if context.has_dctrl_file:
690 first_pkg = next(iter(context.binary_packages()))
691 else:
692 first_pkg = "example-package"
693 example_files = [
694 (f"debian/{ppf.stem}", first_pkg),
695 (f"debian/{first_pkg}.{ppf.stem}", first_pkg),
696 ]
697 if ppf.allow_name_segment:
698 example_files.append(
699 (f"debian/{first_pkg}.my.custom.name.{ppf.stem}", "my.custom.name")
700 )
701 if ppf.allow_architecture_segment:
702 example_files.append((f"debian/{first_pkg}.{ppf.stem}.amd64", first_pkg)),
703 if ppf.allow_name_segment:
704 example_files.append(
705 (
706 f"debian/{first_pkg}.my.custom.name.{ppf.stem}.amd64",
707 "my.custom.name",
708 )
709 )
710 fs_root = build_virtual_fs([x for x, _ in example_files])
711 priority = ppf.default_priority if ppf.supports_priority else None
712 rendered_examples = []
713 for example_file, assigned_name in example_files:
714 example_path = fs_root.lookup(example_file)
715 assert example_path is not None and example_path.is_file
716 dest = ppf.compute_dest(
717 assigned_name,
718 owning_package=first_pkg,
719 assigned_priority=priority,
720 path=example_path,
721 )
722 dest_path = "/".join(dest).lstrip(".")
723 rendered_examples.append((example_file, dest_path))
725 fo.print_list_table(["Source file", "Installed As"], rendered_examples)
727 if doc_uris:
728 fo.print()
729 fo.print("Documentation URIs:")
730 for uri in doc_uris:
731 fo.print(f" * {fo.render_url(uri)}")
733 plugin_name = ppf.debputy_plugin_metadata.plugin_name
734 fo.print()
735 fo.print(f"Install Mode: 0{oct(ppf.default_mode)[2:]}")
736 fo.print(f"Provided by plugin: {plugin_name}")
737 if (
738 matched_file
739 and plugin_name != "debputy"
740 and plugin_name not in context.requested_plugins()
741 ):
742 fo.print()
743 _warn(
744 f"The file might *NOT* be used due to missing Build-Depends on debputy-plugin-{plugin_name}"
745 )
748class UnresolvableRuleError(ValueError):
749 pass
752@dataclasses.dataclass
753class PMRRuleLookup:
754 rule_name: str
755 parser: DeclarativeInputParser
756 parser_type_name: str
757 plugin_metadata: DebputyPluginMetadata
758 is_root_rule: bool
759 manifest_attribute_path: str
762def lookup_pmr_rule(
763 feature_set: PluginProvidedFeatureSet,
764 pmr_rule_name: str,
765) -> PMRRuleLookup:
766 req_rule_type = None
767 rule_name = pmr_rule_name
768 if "::" in rule_name and rule_name != "::":
769 req_rule_type, rule_name = rule_name.split("::", 1)
771 matched = []
773 base_type = Iterable[Tuple[Union[str, Type[Any]], DispatchingParserBase[Any]]]
774 parser_generator = feature_set.manifest_parser_generator
775 table_parsers: base_type = parser_generator.dispatchable_table_parsers.items()
776 object_parsers: base_type = parser_generator.dispatchable_object_parsers.items()
778 parsers = chain(
779 table_parsers,
780 object_parsers,
781 )
783 for rule_type, dispatching_parser in parsers:
784 if req_rule_type is not None and req_rule_type not in parser_type_name(
785 rule_type
786 ):
787 continue
788 if dispatching_parser.is_known_keyword(rule_name):
789 matched.append((rule_type, dispatching_parser))
791 if len(matched) != 1 and (matched or rule_name != "::"):
792 if not matched:
793 raise UnresolvableRuleError(
794 f"Could not find any pluggable manifest rule related to {pmr_rule_name}."
795 f" Please use `debputy plugin list pluggable-manifest-rules` to see the list of rules."
796 )
797 match_a = matched[0][0]
798 match_b = matched[1][0]
799 raise UnresolvableRuleError(
800 f"The name {rule_name} was ambiguous and matched multiple rule types. Please use"
801 f" <rule-type>::{rule_name} to clarify which rule to use"
802 f" (such as {parser_type_name(match_a)}::{rule_name} or {parser_type_name(match_b)}::{rule_name})."
803 f" Please use `debputy plugin list pluggable-manifest-rules` to see the list of rules."
804 )
806 if matched:
807 rule_type, matched_dispatching_parser = matched[0]
808 plugin_provided_parser = matched_dispatching_parser.parser_for(rule_name)
809 if isinstance(rule_type, str):
810 manifest_attribute_path = rule_type
811 else:
812 manifest_attribute_path = SUPPORTED_DISPATCHABLE_TABLE_PARSERS[rule_type]
813 full_parser_type_name = parser_type_name(rule_type)
814 parser = plugin_provided_parser.parser
815 plugin_metadata = plugin_provided_parser.plugin_metadata
816 else:
817 rule_name = "::"
818 parser = parser_generator.dispatchable_object_parsers[OPARSER_MANIFEST_ROOT]
819 full_parser_type_name = ""
820 plugin_metadata = plugin_metadata_for_debputys_own_plugin()
821 manifest_attribute_path = ""
823 is_root_rule = rule_name == "::"
824 return PMRRuleLookup(
825 rule_name,
826 parser,
827 full_parser_type_name,
828 plugin_metadata,
829 is_root_rule,
830 manifest_attribute_path,
831 )
834@plugin_show_cmds.register_subcommand(
835 ["pluggable-manifest-rules", "p-m-r", "pmr"],
836 help_description="Pluggable manifest rules (such as install rules)",
837 argparser=add_arg(
838 "pmr_rule_name",
839 metavar="rule-name",
840 help="Name of the rule (such as `install`) to display details about",
841 ),
842)
843def _plugin_cmd_show_manifest_rule(context: CommandContext) -> None:
844 feature_set = context.load_plugins()
845 parsed_args = context.parsed_args
846 fo = _output_styling(parsed_args, sys.stdout)
848 try:
849 pmr_rule = lookup_pmr_rule(feature_set, parsed_args.pmr_rule_name)
850 except UnresolvableRuleError as e:
851 _error(e.args[0])
853 print(
854 render_rule(
855 pmr_rule.rule_name,
856 pmr_rule.parser,
857 pmr_rule.plugin_metadata,
858 fo,
859 is_root_rule=pmr_rule.is_root_rule,
860 )
861 )
863 if not pmr_rule.is_root_rule:
864 manifest_attribute_path = pmr_rule.manifest_attribute_path
865 print(
866 f"Used in: {manifest_attribute_path if manifest_attribute_path != '<ROOT>' else 'The manifest root'}"
867 )
868 print(f"Rule reference: {pmr_rule.parser_type_name}::{pmr_rule.rule_name}")
869 print(f"Plugin: {pmr_rule.plugin_metadata.plugin_name}")
870 else:
871 print(f"Rule reference: {pmr_rule.rule_name}")
873 print()
874 print(
875 "PS: If you want to know more about a non-trivial type of an attribute such as `FileSystemMatchRule`,"
876 )
877 print(
878 "you can use `debputy plugin show type-mappings FileSystemMatchRule` to look it up "
879 )
882def _render_discard_rule_example(
883 fo: IOBasedOutputStyling,
884 discard_rule: PluginProvidedDiscardRule,
885 example: AutomaticDiscardRuleExample,
886) -> None:
887 processed = process_discard_rule_example(discard_rule, example)
889 if processed.inconsistent_paths:
890 plugin_name = discard_rule.plugin_metadata.plugin_name
891 _warn(
892 f"This example is inconsistent with what the code actually does."
893 f" Please consider filing a bug against the plugin {plugin_name}"
894 )
896 doc = example.description
897 if doc:
898 print(doc)
900 print("Consider the following source paths matched by a glob or directory match:")
901 print()
902 if fo.optimize_for_screen_reader:
903 for p, _ in processed.rendered_paths:
904 path_name = p.absolute
905 print(
906 f"The path {path_name} is a {'directory' if p.is_dir else 'file or symlink.'}"
907 )
909 print()
910 if any(v.is_consistent and v.is_discarded for _, v in processed.rendered_paths):
911 print("The following paths will be discarded by this rule:")
912 for p, verdict in processed.rendered_paths:
913 path_name = p.absolute
914 if verdict.is_consistent and verdict.is_discarded:
915 print()
916 if p.is_dir:
917 print(f"{path_name} along with anything beneath it")
918 else:
919 print(path_name)
920 else:
921 print("No paths will be discarded in this example.")
923 print()
924 if any(v.is_consistent and v.is_kept for _, v in processed.rendered_paths):
925 print("The following paths will be not be discarded by this rule:")
926 for p, verdict in processed.rendered_paths:
927 path_name = p.absolute
928 if verdict.is_consistent and verdict.is_kept:
929 print()
930 print(path_name)
932 if any(not v.is_consistent for _, v in processed.rendered_paths):
933 print()
934 print(
935 "The example was inconsistent with the code. These are the paths where the code disagrees with"
936 " the provided example:"
937 )
938 for p, verdict in processed.rendered_paths:
939 path_name = p.absolute
940 if not verdict.is_consistent:
941 print()
942 if verdict == DiscardVerdict.DISCARDED_BY_CODE:
943 print(
944 f"The path {path_name} was discarded by the code, but the example said it should"
945 f" have been installed."
946 )
947 else:
948 print(
949 f"The path {path_name} was not discarded by the code, but the example said it should"
950 f" have been discarded."
951 )
952 return
954 # Add +1 for dirs because we want trailing slashes in the output
955 max_len = max(
956 (len(p.absolute) + (1 if p.is_dir else 0)) for p, _ in processed.rendered_paths
957 )
958 for p, verdict in processed.rendered_paths:
959 path_name = p.absolute
960 if p.is_dir:
961 path_name += "/"
963 if not verdict.is_consistent:
964 print(f" {path_name:<{max_len}} !! {verdict.message}")
965 elif verdict.is_discarded:
966 print(f" {path_name:<{max_len}} << {verdict.message}")
967 else:
968 print(f" {path_name:<{max_len}}")
971def _render_discard_rule(
972 context: CommandContext,
973 discard_rule: PluginProvidedDiscardRule,
974) -> None:
975 fo = _output_styling(context.parsed_args, sys.stdout)
976 print(fo.colored(f"Automatic Discard Rule: {discard_rule.name}", style="bold"))
977 fo.print_visual_formatting(
978 f"========================{'=' * len(discard_rule.name)}"
979 )
980 print()
981 doc = discard_rule.reference_documentation or "No documentation provided"
982 for line in render_multiline_documentation(
983 doc, first_line_prefix="", following_line_prefix=""
984 ):
985 print(line)
987 if len(discard_rule.examples) > 1:
988 print()
989 fo.print_visual_formatting("Examples")
990 fo.print_visual_formatting("--------")
991 print()
992 for no, example in enumerate(discard_rule.examples, start=1):
993 print(
994 fo.colored(
995 f"Example {no} of {len(discard_rule.examples)}", style="bold"
996 )
997 )
998 fo.print_visual_formatting(f"........{'.' * len(str(no))}")
999 _render_discard_rule_example(fo, discard_rule, example)
1000 elif discard_rule.examples:
1001 print()
1002 print(fo.colored("Example", style="bold"))
1003 fo.print_visual_formatting("-------")
1004 print()
1005 _render_discard_rule_example(fo, discard_rule, discard_rule.examples[0])
1008@plugin_show_cmds.register_subcommand(
1009 ["automatic-discard-rules", "a-d-r"],
1010 help_description="Automatic discard rules",
1011 argparser=add_arg(
1012 "discard_rule",
1013 metavar="automatic-discard-rule",
1014 help="Name of the automatic discard rule (such as `backup-files`)",
1015 ),
1016)
1017def _plugin_cmd_show_automatic_discard_rules(context: CommandContext) -> None:
1018 auto_discard_rules = context.load_plugins().auto_discard_rules
1019 name = context.parsed_args.discard_rule
1020 discard_rule = auto_discard_rules.get(name)
1021 if discard_rule is None:
1022 _error(
1023 f'No automatic discard rule with the name "{name}". Please use'
1024 f" `debputy plugin list automatic-discard-rules` to see the list of automatic discard rules"
1025 )
1027 _render_discard_rule(context, discard_rule)
1030@plugin_list_cmds.register_subcommand(
1031 "type-mappings",
1032 help_description="Registered type mappings/descriptions",
1033)
1034def _plugin_cmd_list_type_mappings(context: CommandContext) -> None:
1035 type_mappings = context.load_plugins().mapped_types
1037 with _stream_to_pager(context.parsed_args) as (fd, fo):
1038 fo.print_list_table(
1039 ["Type", "Base Type", "Provided By"],
1040 [
1041 (
1042 target_type.__name__,
1043 render_source_type(type_mapping.mapped_type.source_type),
1044 type_mapping.plugin_metadata.plugin_name,
1045 )
1046 for target_type, type_mapping in type_mappings.items()
1047 ],
1048 )
1051@plugin_show_cmds.register_subcommand(
1052 "type-mappings",
1053 help_description="Registered type mappings/descriptions",
1054 argparser=add_arg(
1055 "type_mapping",
1056 metavar="type-mapping",
1057 help="Name of the type",
1058 ),
1059)
1060def _plugin_cmd_show_type_mappings(context: CommandContext) -> None:
1061 type_mapping_name = context.parsed_args.type_mapping
1062 type_mappings = context.load_plugins().mapped_types
1063 fo = _output_styling(context.parsed_args, sys.stdout)
1065 matches = []
1066 for type_ in type_mappings:
1067 if type_.__name__ == type_mapping_name:
1068 matches.append(type_)
1070 if not matches:
1071 simple_types = set(BASIC_SIMPLE_TYPES.values())
1072 simple_types.update(t.__name__ for t in BASIC_SIMPLE_TYPES)
1074 if type_mapping_name in simple_types:
1075 print(f"The type {type_mapping_name} is a YAML scalar.")
1076 return
1077 if type_mapping_name == "Any":
1078 print(
1079 "The Any type is a placeholder for when no typing information is provided. Often this implies"
1080 " custom parse logic."
1081 )
1082 return
1084 if type_mapping_name in ("List", "list"):
1085 print(
1086 f"The {type_mapping_name} is a YAML Sequence. Please see the YAML documentation for examples."
1087 )
1088 return
1090 if type_mapping_name in ("Mapping", "dict"):
1091 print(
1092 f"The {type_mapping_name} is a YAML mapping. Please see the YAML documentation for examples."
1093 )
1094 return
1096 if "[" in type_mapping_name:
1097 _error(
1098 f"No known matches for {type_mapping_name}. Note: It looks like a composite type. Try searching"
1099 " for its component parts. As an example, replace List[FileSystemMatchRule] with FileSystemMatchRule."
1100 )
1102 _error(f"Sorry, no known matches for {type_mapping_name}")
1104 if len(matches) > 1:
1105 _error(
1106 f"Too many matches for {type_mapping_name}... Sorry, there is no way to avoid this right now :'("
1107 )
1109 match = matches[0]
1110 manifest_parser = context.manifest_parser()
1112 pptm = type_mappings[match]
1113 fo.print(
1114 render_type_mapping(
1115 pptm,
1116 fo,
1117 manifest_parser,
1118 recover_from_broken_examples=context.parsed_args.debug_mode,
1119 )
1120 )
1122 fo.print()
1123 fo.print(f"Provided by plugin: {pptm.plugin_metadata.plugin_name}")
1126def ensure_plugin_commands_are_loaded() -> None:
1127 # Loading the module does the heavy lifting
1128 # However, having this function means that we do not have an "unused" import that some tool
1129 # gets tempted to remove
1130 assert ROOT_COMMAND.has_command("plugin")