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