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

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) 

18 

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 

67 

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) 

75 

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) 

84 

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) 

92 

93 

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...") 

101 

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 ) 

110 

111 return _configurator 

112 

113 

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) 

120 

121 

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) 

127 

128 

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 ) 

144 

145 

146def _path(path: str) -> str: 

147 if path.startswith("./"): 

148 return path[1:] 

149 return path 

150 

151 

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) 

165 

166 

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)) 

182 

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 ] 

193 

194 if not used_ppfs and not inactive_ppfs: 

195 print("No packager provided files detected; not even a changelog... ?") 

196 return 

197 

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 ) 

220 

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 ) 

244 

245 

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 ) 

275 

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.") 

282 

283 

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 ) 

295 

296 def _sort_key(md: "MetadataOrMaintscriptDetector") -> Any: 

297 return md.plugin_metadata.plugin_name, md.detector_id 

298 

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 ) 

307 

308 

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) 

319 

320 

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) 

328 

329 

330def _render_list_filter(v: Optional[bool]) -> str: 

331 if v is None: 

332 return "N/A" 

333 return "shown" if v else "hidden" 

334 

335 

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 

349 

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 

360 

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 ) 

375 

376 fo.print() 

377 

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 ] 

390 

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 ) 

402 

403 

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) 

430 

431 

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() 

439 

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]]] 

443 

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() 

447 

448 parsers = chain( 

449 table_parsers, 

450 object_parsers, 

451 ) 

452 

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 ) 

466 

467 

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 

475 

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 ) 

487 

488 

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 

494 

495 

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 

513 

514 

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}" 

546 

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 ) 

562 

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() 

572 

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 

580 

581 doc = variable.variable_reference_documentation or "No documentation provided" 

582 _render_multiline_documentation(doc) 

583 

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)}") 

590 

591 if variable.is_for_special_case: 

592 print( 

593 'Special-case: The variable has been marked as a "special-case"-only variable.' 

594 ) 

595 

596 if not variable.is_documentation_placeholder: 

597 print(f"Plugin: {variable.plugin_metadata.plugin_name}") 

598 

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.") 

603 

604 

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 

614 

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:] 

622 

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 ) 

632 

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 

650 

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 ) 

655 

656 

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) 

668 

669 fo = _output_styling(context.parsed_args, sys.stdout) 

670 

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) 

684 

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 ) 

705 

706 fo.print() 

707 fo.print("Examples matches:") 

708 

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)) 

744 

745 fo.print_list_table(["Source file", "Installed As"], rendered_examples) 

746 

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)}") 

752 

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 ) 

766 

767 

768class UnresolvableRuleError(ValueError): 

769 pass 

770 

771 

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 

780 

781 

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) 

790 

791 matched = [] 

792 

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() 

797 

798 parsers = chain( 

799 table_parsers, 

800 object_parsers, 

801 ) 

802 

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)) 

810 

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 ) 

825 

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 = "" 

842 

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 ) 

852 

853 

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 

866 

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]) 

871 

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 ) 

880 

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}") 

890 

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 ) 

898 

899 

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) 

906 

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 ) 

913 

914 doc = example.description 

915 if doc: 

916 print(doc) 

917 

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 ) 

926 

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.") 

940 

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) 

949 

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 

971 

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 += "/" 

980 

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}}") 

987 

988 

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="") 

1001 

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]) 

1021 

1022 

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 ) 

1041 

1042 _render_discard_rule(context, discard_rule) 

1043 

1044 

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) 

1057 

1058 

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 

1065 

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 ) 

1078 

1079 

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 

1092 

1093 matches = [] 

1094 for type_ in type_mappings: 

1095 if type_.__name__ == type_mapping_name: 

1096 matches.append(type_) 

1097 

1098 if not matches: 

1099 simple_types = set(BASIC_SIMPLE_TYPES.values()) 

1100 simple_types.update(t.__name__ for t in BASIC_SIMPLE_TYPES) 

1101 

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 

1111 

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 

1117 

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 

1123 

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 ) 

1129 

1130 _error(f"Sorry, no known matches for {type_mapping_name}") 

1131 

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 ) 

1136 

1137 match = matches[0] 

1138 _render_type(context, type_mappings[match]) 

1139 

1140 

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 

1167 

1168 

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() 

1179 

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.") 

1188 

1189 context.parse_manifest() 

1190 

1191 manifest_parser = context.manifest_parser() 

1192 

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 

1207 

1208 fo.print() 

1209 fo.print(f"Provided by plugin: {pptm.plugin_metadata.plugin_name}") 

1210 

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") 

1220 

1221 

1222def _render_value(v: Any) -> str: 

1223 if isinstance(v, str) and '"' not in v: 

1224 return f'"{v}"' 

1225 return str(v) 

1226 

1227 

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")