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