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