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