Coverage for src/debputy/analysis/debian_dir.py: 28%
323 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-24 16:38 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-24 16:38 +0000
1import json
2import os
3import stat
4import subprocess
5from typing import (
6 AbstractSet,
7 List,
8 Mapping,
9 Iterable,
10 Tuple,
11 Optional,
12 Sequence,
13 Dict,
14 Any,
15 Union,
16 Iterator,
17 TypedDict,
18 NotRequired,
19 Container,
20)
22from debputy.analysis import REFERENCE_DATA_TABLE
23from debputy.analysis.analysis_util import flatten_ppfs
24from debputy.dh.dh_assistant import (
25 resolve_active_and_inactive_dh_commands,
26 read_dh_addon_sequences,
27 extract_dh_compat_level,
28)
29from debputy.integration_detection import determine_debputy_integration_mode
30from debputy.packager_provided_files import (
31 PackagerProvidedFile,
32 detect_all_packager_provided_files,
33)
34from debputy.packages import BinaryPackage, SourcePackage
35from debputy.plugin.api import (
36 VirtualPath,
37 packager_provided_file_reference_documentation,
38)
39from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
40from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin
41from debputy.plugin.api.impl_types import (
42 PluginProvidedKnownPackagingFile,
43 DebputyPluginMetadata,
44 KnownPackagingFileInfo,
45 InstallPatternDHCompatRule,
46 PackagerProvidedFileClassSpec,
47 expand_known_packaging_config_features,
48)
49from debputy.plugin.api.spec import DebputyIntegrationMode
50from debputy.util import (
51 assume_not_none,
52 escape_shell,
53 _trace_log,
54 _is_trace_log_enabled,
55 render_command,
56)
58PackagingFileInfo = TypedDict(
59 "PackagingFileInfo",
60 {
61 "path": str,
62 "binary-package": NotRequired[str],
63 "install-path": NotRequired[str],
64 "install-pattern": NotRequired[str],
65 "file-categories": NotRequired[List[str]],
66 "config-features": NotRequired[List[str]],
67 "pkgfile-is-active-in-build": NotRequired[bool],
68 "pkgfile-stem": NotRequired[str],
69 "pkgfile-explicit-package-name": NotRequired[bool],
70 "pkgfile-name-segment": NotRequired[str],
71 "pkgfile-architecture-restriction": NotRequired[str],
72 "likely-typo-of": NotRequired[str],
73 "likely-generated-from": NotRequired[List[str]],
74 "related-tools": NotRequired[List[str]],
75 "documentation-uris": NotRequired[List[str]],
76 "debputy-cmd-templates": NotRequired[List[List[str]]],
77 "generates": NotRequired[str],
78 "generated-from": NotRequired[str],
79 },
80)
83def scan_debian_dir(
84 feature_set: PluginProvidedFeatureSet,
85 source_package: SourcePackage,
86 binary_packages: Mapping[str, BinaryPackage],
87 debian_dir: VirtualPath,
88 *,
89 uses_dh_sequencer: bool = True,
90 dh_sequences: Optional[AbstractSet[str]] = None,
91) -> Tuple[List[PackagingFileInfo], List[str], int, Optional[object]]:
92 known_packaging_files = feature_set.known_packaging_files
93 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin()
94 reference_data_set_names = [
95 "config-features",
96 "file-categories",
97 ]
98 for n in reference_data_set_names:
99 assert n in REFERENCE_DATA_TABLE
101 annotated: List[PackagingFileInfo] = []
102 seen_paths: Dict[str, PackagingFileInfo] = {}
104 if dh_sequences is None:
105 r = read_dh_addon_sequences(debian_dir)
106 if r is not None:
107 bd_sequences, dr_sequences, uses_dh_sequencer = r
108 dh_sequences = bd_sequences | dr_sequences
109 else:
110 dh_sequences = set()
111 uses_dh_sequencer = False
112 debputy_integration_mode = determine_debputy_integration_mode(
113 source_package.fields,
114 dh_sequences,
115 )
116 is_debputy_package = debputy_integration_mode is not None
117 dh_compat_level, dh_assistant_exit_code = extract_dh_compat_level()
118 dh_issues = []
120 static_packaging_files = {
121 kpf.detection_value: kpf
122 for kpf in known_packaging_files.values()
123 if kpf.detection_method == "path"
124 }
125 dh_pkgfile_docs = {
126 kpf.detection_value: kpf
127 for kpf in known_packaging_files.values()
128 if kpf.detection_method == "dh.pkgfile"
129 }
131 if is_debputy_package:
132 all_debputy_ppfs = list(
133 flatten_ppfs(
134 detect_all_packager_provided_files(
135 feature_set.packager_provided_files,
136 debian_dir,
137 binary_packages,
138 allow_fuzzy_matches=True,
139 detect_typos=True,
140 ignore_paths=static_packaging_files,
141 )
142 )
143 )
144 else:
145 all_debputy_ppfs = []
147 if dh_compat_level is not None:
148 (
149 all_dh_ppfs,
150 dh_issues,
151 dh_assistant_exit_code,
152 ) = resolve_debhelper_config_files(
153 debian_dir,
154 binary_packages,
155 debputy_plugin_metadata,
156 dh_pkgfile_docs,
157 dh_sequences,
158 dh_compat_level,
159 uses_dh_sequencer,
160 debputy_integration_mode=debputy_integration_mode,
161 ignore_paths=static_packaging_files,
162 )
164 else:
165 all_dh_ppfs = []
167 for ppf in all_debputy_ppfs:
168 key = ppf.path.path
169 ref_doc = ppf.definition.reference_documentation
170 documentation_uris = (
171 ref_doc.format_documentation_uris if ref_doc is not None else None
172 )
173 details: PackagingFileInfo = {
174 "path": key,
175 "binary-package": ppf.package_name,
176 "pkgfile-stem": ppf.definition.stem,
177 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name,
178 "pkgfile-is-active-in-build": ppf.definition.has_active_command,
179 "debputy-cmd-templates": [
180 ["debputy", "plugin", "show", "p-p-f", ppf.definition.stem]
181 ],
182 }
183 if ppf.fuzzy_match and key.endswith(".in"):
184 _merge_list(details, "file-categories", ["generic-template"])
185 details["generates"] = key[:-3]
186 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"):
187 _merge_list(details, "file-categories", ["generated"])
188 details["generated-from"] = key + ".in"
189 name_segment = ppf.name_segment
190 arch_restriction = ppf.architecture_restriction
191 if name_segment is not None:
192 details["pkgfile-name-segment"] = name_segment
193 if arch_restriction:
194 details["pkgfile-architecture-restriction"] = arch_restriction
195 seen_paths[key] = details
196 annotated.append(details)
197 static_details = static_packaging_files.get(key)
198 if static_details is not None:
199 # debhelper compat rules does not apply to debputy files
200 _add_known_packaging_data(details, static_details, None)
201 if documentation_uris:
202 details["documentation-uris"] = list(documentation_uris)
204 _merge_ppfs(annotated, seen_paths, all_dh_ppfs, dh_pkgfile_docs, dh_compat_level)
206 for virtual_path in _scan_debian_dir(debian_dir):
207 key = virtual_path.path
208 if key in seen_paths:
209 continue
210 if virtual_path.is_symlink:
211 try:
212 st = os.stat(virtual_path.fs_path)
213 except FileNotFoundError:
214 continue
215 else:
216 if not stat.S_ISREG(st.st_mode):
217 continue
218 elif not virtual_path.is_file:
219 continue
221 static_match = static_packaging_files.get(virtual_path.path)
222 if static_match is not None:
223 details: PackagingFileInfo = {
224 "path": key,
225 }
226 annotated.append(details)
227 if assume_not_none(virtual_path.parent_dir).get(virtual_path.name + ".in"):
228 details["generated-from"] = key + ".in"
229 _merge_list(details, "file-categories", ["generated"])
230 _add_known_packaging_data(details, static_match, dh_compat_level)
232 return annotated, reference_data_set_names, dh_assistant_exit_code, dh_issues
235def _fake_PPFClassSpec(
236 debputy_plugin_metadata: DebputyPluginMetadata,
237 stem: str,
238 doc_uris: Optional[Sequence[str]],
239 install_pattern: Optional[str],
240 *,
241 default_priority: Optional[int] = None,
242 packageless_is_fallback_for_all_packages: bool = False,
243 post_formatting_rewrite: Optional[str] = None,
244 bug_950723: bool = False,
245 has_active_command: bool = False,
246) -> PackagerProvidedFileClassSpec:
247 if install_pattern is None: 247 ↛ 249line 247 didn't jump to line 249 because the condition on line 247 was always true
248 install_pattern = "not-a-real-ppf"
249 if post_formatting_rewrite is not None: 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true
250 formatting_hook = _POST_FORMATTING_REWRITE[post_formatting_rewrite]
251 else:
252 formatting_hook = None
253 return PackagerProvidedFileClassSpec(
254 debputy_plugin_metadata,
255 stem,
256 install_pattern,
257 allow_architecture_segment=True,
258 allow_name_segment=True,
259 default_priority=default_priority,
260 default_mode=0o644,
261 post_formatting_rewrite=formatting_hook,
262 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
263 reservation_only=False,
264 formatting_callback=None,
265 bug_950723=bug_950723,
266 has_active_command=has_active_command,
267 reference_documentation=packager_provided_file_reference_documentation(
268 format_documentation_uris=doc_uris,
269 ),
270 )
273def _relevant_dh_compat_rules(
274 compat_level: Optional[int],
275 info: KnownPackagingFileInfo,
276) -> Iterable[InstallPatternDHCompatRule]:
277 if compat_level is None:
278 return
279 dh_compat_rules = info.get("dh_compat_rules")
280 if not dh_compat_rules:
281 return
282 for dh_compat_rule in dh_compat_rules:
283 rule_compat_level = dh_compat_rule.get("starting_with_compat_level")
284 if rule_compat_level is not None and compat_level < rule_compat_level:
285 continue
286 yield dh_compat_rule
289def _kpf_install_pattern(
290 compat_level: Optional[int],
291 ppkpf: PluginProvidedKnownPackagingFile,
292) -> Optional[str]:
293 for compat_rule in _relevant_dh_compat_rules(compat_level, ppkpf.info):
294 install_pattern = compat_rule.get("install_pattern")
295 if install_pattern is not None:
296 return install_pattern
297 return ppkpf.info.get("install_pattern")
300def resolve_debhelper_config_files(
301 debian_dir: VirtualPath,
302 binary_packages: Mapping[str, BinaryPackage],
303 debputy_plugin_metadata: DebputyPluginMetadata,
304 dh_ppf_docs: Dict[str, PluginProvidedKnownPackagingFile],
305 dh_rules_addons: AbstractSet[str],
306 dh_compat_level: int,
307 saw_dh: bool,
308 ignore_paths: Container[str] = frozenset(),
309 *,
310 debputy_integration_mode: Optional[DebputyIntegrationMode] = None,
311 cwd: Optional[str] = None,
312) -> Tuple[List[PackagerProvidedFile], Optional[object], int]:
313 dh_ppfs = {}
314 commands, exit_code = _relevant_dh_commands(dh_rules_addons, cwd=cwd)
316 cmd = ["dh_assistant", "list-guessed-dh-config-files"]
317 if dh_rules_addons:
318 addons = ",".join(dh_rules_addons)
319 cmd.append(f"--with={addons}")
320 try:
321 output = subprocess.check_output(
322 cmd,
323 stderr=subprocess.DEVNULL,
324 cwd=cwd,
325 )
326 except (subprocess.CalledProcessError, FileNotFoundError) as e:
327 config_files = []
328 issues = None
329 if isinstance(e, subprocess.CalledProcessError):
330 exit_code = e.returncode
331 else:
332 exit_code = 127
333 if _is_trace_log_enabled():
334 _trace_log(
335 f"Command {render_command(*cmd, cwd=cwd)} failed with {exit_code} ({cwd=})"
336 )
337 else:
338 result = json.loads(output)
339 config_files: List[Union[Mapping[str, Any], object]] = result.get(
340 "config-files", []
341 )
342 issues = result.get("issues")
343 if _is_trace_log_enabled(): 343 ↛ 344line 343 didn't jump to line 344 because the condition on line 343 was never true
344 _trace_log(
345 f"Command {render_command(*cmd, cwd=cwd)} returned successfully: {output}"
346 )
347 dh_commands = resolve_active_and_inactive_dh_commands(dh_rules_addons)
348 for config_file in config_files:
349 if not isinstance(config_file, dict): 349 ↛ 350line 349 didn't jump to line 350 because the condition on line 349 was never true
350 continue
351 if config_file.get("file-type") != "pkgfile": 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true
352 continue
353 stem = config_file.get("pkgfile")
354 if stem is None: 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true
355 continue
356 internal = config_file.get("internal")
357 if isinstance(internal, dict):
358 bug_950723 = internal.get("bug#950723", False) is True
359 else:
360 bug_950723 = False
361 commands = config_file.get("commands")
362 documentation_uris = []
363 related_tools = []
364 seen_commands = set()
365 seen_docs = set()
366 ppkpf = dh_ppf_docs.get(stem)
368 if ppkpf: 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true
369 dh_cmds = ppkpf.info.get("debhelper_commands")
370 doc_uris = ppkpf.info.get("documentation_uris")
371 default_priority = ppkpf.info.get("default_priority")
372 if doc_uris is not None:
373 seen_docs.update(doc_uris)
374 documentation_uris.extend(doc_uris)
375 if dh_cmds is not None:
376 seen_commands.update(dh_cmds)
377 related_tools.extend(dh_cmds)
378 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf)
379 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite")
380 packageless_is_fallback_for_all_packages = ppkpf.info.get(
381 "packageless_is_fallback_for_all_packages",
382 False,
383 )
384 # If it is a debhelper PPF, then `has_active_command` is false by default.
385 has_active_command = ppkpf.info.get("has_active_command", False)
386 else:
387 install_pattern = None
388 default_priority = None
389 post_formatting_rewrite = None
390 packageless_is_fallback_for_all_packages = False
391 has_active_command = False
392 for command in commands:
393 if isinstance(command, dict): 393 ↛ 392line 393 didn't jump to line 392 because the condition on line 393 was always true
394 command_name = command.get("command")
395 if isinstance(command_name, str) and command_name: 395 ↛ 404line 395 didn't jump to line 404 because the condition on line 395 was always true
396 if command_name not in seen_commands: 396 ↛ 399line 396 didn't jump to line 399 because the condition on line 396 was always true
397 related_tools.append(command_name)
398 seen_commands.add(command_name)
399 manpage = f"man:{command_name}(1)"
400 if manpage not in seen_docs: 400 ↛ 405line 400 didn't jump to line 405 because the condition on line 400 was always true
401 documentation_uris.append(manpage)
402 seen_docs.add(manpage)
403 else:
404 continue
405 is_active = command.get("is-active", True)
406 if is_active is None and command_name in dh_commands.active_commands: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true
407 is_active = True
408 if not isinstance(is_active, bool): 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 continue
410 if is_active:
411 has_active_command = True
413 if debputy_integration_mode == "full": 413 ↛ 415line 413 didn't jump to line 415 because the condition on line 413 was never true
414 # dh commands are never active in full integration mode.
415 has_active_command = False
416 elif not saw_dh: 416 ↛ 420line 416 didn't jump to line 420 because the condition on line 416 was always true
417 # If we did not see `dh`, we assume classic `debhelper` where we have no way of knowing
418 # which commands are active.
419 has_active_command = True
420 dh_ppfs[stem] = _fake_PPFClassSpec(
421 debputy_plugin_metadata,
422 stem,
423 documentation_uris,
424 install_pattern,
425 default_priority=default_priority,
426 post_formatting_rewrite=post_formatting_rewrite,
427 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
428 bug_950723=bug_950723,
429 has_active_command=has_active_command,
430 )
431 for ppkpf in dh_ppf_docs.values(): 431 ↛ 432line 431 didn't jump to line 432 because the loop on line 431 never started
432 stem = ppkpf.detection_value
433 if stem in dh_ppfs:
434 continue
436 default_priority = ppkpf.info.get("default_priority")
437 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf)
438 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite")
439 packageless_is_fallback_for_all_packages = ppkpf.info.get(
440 "packageless_is_fallback_for_all_packages",
441 False,
442 )
443 has_active_command = (
444 ppkpf.info.get("has_active_command", False) if saw_dh else False
445 )
446 if not has_active_command:
447 dh_cmds = ppkpf.info.get("debhelper_commands")
448 if dh_cmds:
449 has_active_command = any(
450 c in dh_commands.active_commands for c in dh_cmds
451 )
452 dh_ppfs[stem] = _fake_PPFClassSpec(
453 debputy_plugin_metadata,
454 stem,
455 ppkpf.info.get("documentation_uris"),
456 install_pattern,
457 default_priority=default_priority,
458 post_formatting_rewrite=post_formatting_rewrite,
459 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
460 has_active_command=has_active_command,
461 )
462 all_dh_ppfs = list(
463 flatten_ppfs(
464 detect_all_packager_provided_files(
465 dh_ppfs,
466 debian_dir,
467 binary_packages,
468 allow_fuzzy_matches=True,
469 detect_typos=True,
470 ignore_paths=ignore_paths,
471 )
472 )
473 )
474 return all_dh_ppfs, issues, exit_code
477def _merge_list(
478 existing_table: Dict[str, Any],
479 key: str,
480 new_data: Optional[Sequence[str]],
481) -> None:
482 if not new_data:
483 return
484 existing_values = existing_table.get(key, [])
485 if isinstance(existing_values, tuple):
486 existing_values = list(existing_values)
487 assert isinstance(existing_values, list)
488 seen = set(existing_values)
489 existing_values.extend(x for x in new_data if x not in seen)
490 existing_table[key] = existing_values
493def _merge_ppfs(
494 identified: List[PackagingFileInfo],
495 seen_paths: Dict[str, PackagingFileInfo],
496 ppfs: List[PackagerProvidedFile],
497 context: Mapping[str, PluginProvidedKnownPackagingFile],
498 dh_compat_level: Optional[int],
499) -> None:
500 for ppf in ppfs:
501 key = ppf.path.path
502 ref_doc = ppf.definition.reference_documentation
503 documentation_uris = (
504 ref_doc.format_documentation_uris if ref_doc is not None else None
505 )
506 if not ppf.definition.installed_as_format.startswith("not-a-real-ppf"):
507 try:
508 parts = ppf.compute_dest()
509 except RuntimeError:
510 dest = None
511 else:
512 dest = "/".join(parts).lstrip(".")
513 else:
514 dest = None
515 orig_details = seen_paths.get(key)
516 if orig_details is None:
517 details: PackagingFileInfo = {
518 "path": key,
519 "pkgfile-stem": ppf.definition.stem,
520 "pkgfile-is-active-in-build": ppf.definition.has_active_command,
521 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name,
522 "binary-package": ppf.package_name,
523 }
524 if ppf.expected_path is not None:
525 details["likely-typo-of"] = ppf.expected_path
526 identified.append(details)
527 else:
528 details = orig_details
529 # We do not merge the "is typo" field; if the original
530 for k, v in [
531 ("pkgfile-stem", ppf.definition.stem),
532 ("pkgfile-explicit-package-name", ppf.definition.has_active_command),
533 ("binary-package", ppf.package_name),
534 ]:
535 if k not in details:
536 details[k] = v
537 if ppf.definition.has_active_command and details.get(
538 "pkgfile-is-active-in-build", False
539 ):
540 details["pkgfile-is-active-in-build"] = True
541 if ppf.expected_path is None and "likely-typo-of" in details:
542 del details["likely-typo-of"]
544 name_segment = ppf.name_segment
545 arch_restriction = ppf.architecture_restriction
546 if name_segment is not None and "pkgfile-name-segment" not in details:
547 details["pkgfile-name-segment"] = name_segment
548 if (
549 arch_restriction is not None
550 and "pkgfile-architecture-restriction" not in details
551 ):
552 details["pkgfile-architecture-restriction"] = arch_restriction
553 if ppf.fuzzy_match and key.endswith(".in"):
554 _merge_list(details, "file-categories", ["generic-template"])
555 details["generates"] = key[:-3]
556 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"):
557 _merge_list(details, "file-categories", ["generated"])
558 details["generated-from"] = key + ".in"
559 if dest is not None and "install-path" not in details:
560 details["install-path"] = dest
562 extra_details = context.get(ppf.definition.stem)
563 if extra_details is not None:
564 _add_known_packaging_data(details, extra_details, dh_compat_level)
566 _merge_list(details, "documentation-uris", documentation_uris)
569def _relevant_dh_commands(
570 dh_rules_addons: Iterable[str],
571 cwd: Optional[str] = None,
572) -> Tuple[List[str], int]:
573 cmd = ["dh_assistant", "list-commands", "--output-format=json"]
574 if dh_rules_addons:
575 addons = ",".join(dh_rules_addons)
576 cmd.append(f"--with={addons}")
577 try:
578 output = subprocess.check_output(
579 cmd,
580 stderr=subprocess.DEVNULL,
581 cwd=cwd,
582 )
583 except (FileNotFoundError, subprocess.CalledProcessError) as e:
584 exit_code = 127
585 if isinstance(e, subprocess.CalledProcessError):
586 exit_code = e.returncode
587 if _is_trace_log_enabled():
588 _trace_log(
589 f"Command {render_command(*cmd, cwd=cwd)} failed with {exit_code} ({cwd=})"
590 )
591 return [], exit_code
592 else:
593 data = json.loads(output)
594 commands_json = data.get("commands")
595 commands = []
596 if _is_trace_log_enabled(): 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true
597 _trace_log(
598 f"Command {render_command(*cmd, cwd=cwd)} returned successfully: {output}"
599 )
600 for command in commands_json:
601 if isinstance(command, dict): 601 ↛ 600line 601 didn't jump to line 600 because the condition on line 601 was always true
602 command_name = command.get("command")
603 if isinstance(command_name, str) and command_name: 603 ↛ 600line 603 didn't jump to line 600 because the condition on line 603 was always true
604 commands.append(command_name)
605 return commands, 0
608def _add_known_packaging_data(
609 details: PackagingFileInfo,
610 plugin_data: PluginProvidedKnownPackagingFile,
611 dh_compat_level: Optional[int],
612):
613 install_pattern = _kpf_install_pattern(
614 dh_compat_level,
615 plugin_data,
616 )
617 config_features = plugin_data.info.get("config_features")
618 if config_features:
619 config_features = expand_known_packaging_config_features(
620 dh_compat_level or 0,
621 config_features,
622 )
623 _merge_list(details, "config-features", config_features)
625 if dh_compat_level is not None:
626 extra_config_features = []
627 for dh_compat_rule in _relevant_dh_compat_rules(
628 dh_compat_level, plugin_data.info
629 ):
630 cf = dh_compat_rule.get("add_config_features")
631 if cf:
632 extra_config_features.extend(cf)
633 if extra_config_features:
634 extra_config_features = expand_known_packaging_config_features(
635 dh_compat_level,
636 extra_config_features,
637 )
638 _merge_list(details, "config-features", extra_config_features)
639 if "install-pattern" not in details and install_pattern is not None:
640 details["install-pattern"] = install_pattern
641 for mk, ok in [
642 ("file_categories", "file-categories"),
643 ("documentation_uris", "documentation-uris"),
644 ("debputy_cmd_templates", "debputy-cmd-templates"),
645 ]:
646 value = plugin_data.info.get(mk)
647 if value and ok == "debputy-cmd-templates":
648 value = [escape_shell(*c) for c in value]
649 _merge_list(details, ok, value)
652def _scan_debian_dir(debian_dir: VirtualPath) -> Iterator[VirtualPath]:
653 for p in debian_dir.iterdir:
654 yield p
655 if p.is_dir and p.path in ("debian/source", "debian/tests"):
656 yield from p.iterdir
659_POST_FORMATTING_REWRITE = { 659 ↛ exitline 659 didn't jump to the function exit
660 "period-to-underscore": lambda n: n.replace(".", "_"),
661}