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

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) 

21 

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) 

57 

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) 

81 

82 

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 

100 

101 annotated: List[PackagingFileInfo] = [] 

102 seen_paths: Dict[str, PackagingFileInfo] = {} 

103 

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

119 

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 } 

130 

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

146 

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 ) 

163 

164 else: 

165 all_dh_ppfs = [] 

166 

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) 

203 

204 _merge_ppfs(annotated, seen_paths, all_dh_ppfs, dh_pkgfile_docs, dh_compat_level) 

205 

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 

220 

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) 

231 

232 return annotated, reference_data_set_names, dh_assistant_exit_code, dh_issues 

233 

234 

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 ) 

271 

272 

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 

287 

288 

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

298 

299 

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) 

315 

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) 

367 

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 

412 

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 

435 

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 

475 

476 

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 

491 

492 

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

543 

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 

561 

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) 

565 

566 _merge_list(details, "documentation-uris", documentation_uris) 

567 

568 

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 

606 

607 

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) 

624 

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) 

650 

651 

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 

657 

658 

659_POST_FORMATTING_REWRITE = { 659 ↛ exitline 659 didn't jump to the function exit

660 "period-to-underscore": lambda n: n.replace(".", "_"), 

661}