Coverage for src/debputy/analysis/debian_dir.py: 27%

308 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +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.packager_provided_files import ( 

30 PackagerProvidedFile, 

31 detect_all_packager_provided_files, 

32) 

33from debputy.packages import BinaryPackage 

34from debputy.plugin.api import ( 

35 VirtualPath, 

36 packager_provided_file_reference_documentation, 

37) 

38from debputy.plugin.api.feature_set import PluginProvidedFeatureSet 

39from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin 

40from debputy.plugin.api.impl_types import ( 

41 PluginProvidedKnownPackagingFile, 

42 DebputyPluginMetadata, 

43 KnownPackagingFileInfo, 

44 InstallPatternDHCompatRule, 

45 PackagerProvidedFileClassSpec, 

46 expand_known_packaging_config_features, 

47) 

48from debputy.util import assume_not_none, escape_shell 

49 

50PackagingFileInfo = TypedDict( 

51 "PackagingFileInfo", 

52 { 

53 "path": str, 

54 "binary-package": NotRequired[str], 

55 "install-path": NotRequired[str], 

56 "install-pattern": NotRequired[str], 

57 "file-categories": NotRequired[List[str]], 

58 "config-features": NotRequired[List[str]], 

59 "pkgfile-is-active-in-build": NotRequired[bool], 

60 "pkgfile-stem": NotRequired[str], 

61 "pkgfile-explicit-package-name": NotRequired[bool], 

62 "pkgfile-name-segment": NotRequired[str], 

63 "pkgfile-architecture-restriction": NotRequired[str], 

64 "likely-typo-of": NotRequired[str], 

65 "likely-generated-from": NotRequired[List[str]], 

66 "related-tools": NotRequired[List[str]], 

67 "documentation-uris": NotRequired[List[str]], 

68 "debputy-cmd-templates": NotRequired[List[List[str]]], 

69 "generates": NotRequired[str], 

70 "generated-from": NotRequired[str], 

71 }, 

72) 

73 

74 

75def scan_debian_dir( 

76 feature_set: PluginProvidedFeatureSet, 

77 binary_packages: Mapping[str, BinaryPackage], 

78 debian_dir: VirtualPath, 

79 *, 

80 uses_dh_sequencer: bool = True, 

81 dh_sequences: Optional[AbstractSet[str]] = None, 

82) -> Tuple[List[PackagingFileInfo], List[str], int, Optional[object]]: 

83 known_packaging_files = feature_set.known_packaging_files 

84 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin() 

85 

86 reference_data_set_names = [ 

87 "config-features", 

88 "file-categories", 

89 ] 

90 for n in reference_data_set_names: 

91 assert n in REFERENCE_DATA_TABLE 

92 

93 annotated: List[PackagingFileInfo] = [] 

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

95 

96 if dh_sequences is None: 

97 r = read_dh_addon_sequences(debian_dir) 

98 if r is not None: 

99 bd_sequences, dr_sequences, uses_dh_sequencer = r 

100 dh_sequences = bd_sequences | dr_sequences 

101 else: 

102 dh_sequences = set() 

103 uses_dh_sequencer = False 

104 is_debputy_package = ( 

105 "debputy" in dh_sequences 

106 or "zz-debputy" in dh_sequences 

107 or "zz_debputy" in dh_sequences 

108 or "zz-debputy-rrr" in dh_sequences 

109 ) 

110 dh_compat_level, dh_assistant_exit_code = extract_dh_compat_level() 

111 dh_issues = [] 

112 

113 static_packaging_files = { 

114 kpf.detection_value: kpf 

115 for kpf in known_packaging_files.values() 

116 if kpf.detection_method == "path" 

117 } 

118 dh_pkgfile_docs = { 

119 kpf.detection_value: kpf 

120 for kpf in known_packaging_files.values() 

121 if kpf.detection_method == "dh.pkgfile" 

122 } 

123 

124 if is_debputy_package: 

125 all_debputy_ppfs = list( 

126 flatten_ppfs( 

127 detect_all_packager_provided_files( 

128 feature_set.packager_provided_files, 

129 debian_dir, 

130 binary_packages, 

131 allow_fuzzy_matches=True, 

132 detect_typos=True, 

133 ignore_paths=static_packaging_files, 

134 ) 

135 ) 

136 ) 

137 else: 

138 all_debputy_ppfs = [] 

139 

140 if dh_compat_level is not None: 

141 ( 

142 all_dh_ppfs, 

143 dh_issues, 

144 dh_assistant_exit_code, 

145 ) = resolve_debhelper_config_files( 

146 debian_dir, 

147 binary_packages, 

148 debputy_plugin_metadata, 

149 dh_pkgfile_docs, 

150 dh_sequences, 

151 dh_compat_level, 

152 uses_dh_sequencer, 

153 ignore_paths=static_packaging_files, 

154 ) 

155 

156 else: 

157 all_dh_ppfs = [] 

158 

159 for ppf in all_debputy_ppfs: 

160 key = ppf.path.path 

161 ref_doc = ppf.definition.reference_documentation 

162 documentation_uris = ( 

163 ref_doc.format_documentation_uris if ref_doc is not None else None 

164 ) 

165 details: PackagingFileInfo = { 

166 "path": key, 

167 "binary-package": ppf.package_name, 

168 "pkgfile-stem": ppf.definition.stem, 

169 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name, 

170 "pkgfile-is-active-in-build": ppf.definition.has_active_command, 

171 "debputy-cmd-templates": [ 

172 ["debputy", "plugin", "show", "p-p-f", ppf.definition.stem] 

173 ], 

174 } 

175 if ppf.fuzzy_match and key.endswith(".in"): 

176 _merge_list(details, "file-categories", ["generic-template"]) 

177 details["generates"] = key[:-3] 

178 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"): 

179 _merge_list(details, "file-categories", ["generated"]) 

180 details["generated-from"] = key + ".in" 

181 name_segment = ppf.name_segment 

182 arch_restriction = ppf.architecture_restriction 

183 if name_segment is not None: 

184 details["pkgfile-name-segment"] = name_segment 

185 if arch_restriction: 

186 details["pkgfile-architecture-restriction"] = arch_restriction 

187 seen_paths[key] = details 

188 annotated.append(details) 

189 static_details = static_packaging_files.get(key) 

190 if static_details is not None: 

191 # debhelper compat rules does not apply to debputy files 

192 _add_known_packaging_data(details, static_details, None) 

193 if documentation_uris: 

194 details["documentation-uris"] = list(documentation_uris) 

195 

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

197 

198 for virtual_path in _scan_debian_dir(debian_dir): 

199 key = virtual_path.path 

200 if key in seen_paths: 

201 continue 

202 if virtual_path.is_symlink: 

203 try: 

204 st = os.stat(virtual_path.fs_path) 

205 except FileNotFoundError: 

206 continue 

207 else: 

208 if not stat.S_ISREG(st.st_mode): 

209 continue 

210 elif not virtual_path.is_file: 

211 continue 

212 

213 static_match = static_packaging_files.get(virtual_path.path) 

214 if static_match is not None: 

215 details: PackagingFileInfo = { 

216 "path": key, 

217 } 

218 annotated.append(details) 

219 if assume_not_none(virtual_path.parent_dir).get(virtual_path.name + ".in"): 

220 details["generated-from"] = key + ".in" 

221 _merge_list(details, "file-categories", ["generated"]) 

222 _add_known_packaging_data(details, static_match, dh_compat_level) 

223 

224 return annotated, reference_data_set_names, dh_assistant_exit_code, dh_issues 

225 

226 

227def _fake_PPFClassSpec( 

228 debputy_plugin_metadata: DebputyPluginMetadata, 

229 stem: str, 

230 doc_uris: Optional[Sequence[str]], 

231 install_pattern: Optional[str], 

232 *, 

233 default_priority: Optional[int] = None, 

234 packageless_is_fallback_for_all_packages: bool = False, 

235 post_formatting_rewrite: Optional[str] = None, 

236 bug_950723: bool = False, 

237 has_active_command: bool = False, 

238) -> PackagerProvidedFileClassSpec: 

239 if install_pattern is None: 239 ↛ 241line 239 didn't jump to line 241 because the condition on line 239 was always true

240 install_pattern = "not-a-real-ppf" 

241 if post_formatting_rewrite is not None: 241 ↛ 242line 241 didn't jump to line 242 because the condition on line 241 was never true

242 formatting_hook = _POST_FORMATTING_REWRITE[post_formatting_rewrite] 

243 else: 

244 formatting_hook = None 

245 return PackagerProvidedFileClassSpec( 

246 debputy_plugin_metadata, 

247 stem, 

248 install_pattern, 

249 allow_architecture_segment=True, 

250 allow_name_segment=True, 

251 default_priority=default_priority, 

252 default_mode=0o644, 

253 post_formatting_rewrite=formatting_hook, 

254 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages, 

255 reservation_only=False, 

256 formatting_callback=None, 

257 bug_950723=bug_950723, 

258 has_active_command=has_active_command, 

259 reference_documentation=packager_provided_file_reference_documentation( 

260 format_documentation_uris=doc_uris, 

261 ), 

262 ) 

263 

264 

265def _relevant_dh_compat_rules( 

266 compat_level: Optional[int], 

267 info: KnownPackagingFileInfo, 

268) -> Iterable[InstallPatternDHCompatRule]: 

269 if compat_level is None: 

270 return 

271 dh_compat_rules = info.get("dh_compat_rules") 

272 if not dh_compat_rules: 

273 return 

274 for dh_compat_rule in dh_compat_rules: 

275 rule_compat_level = dh_compat_rule.get("starting_with_compat_level") 

276 if rule_compat_level is not None and compat_level < rule_compat_level: 

277 continue 

278 yield dh_compat_rule 

279 

280 

281def _kpf_install_pattern( 

282 compat_level: Optional[int], 

283 ppkpf: PluginProvidedKnownPackagingFile, 

284) -> Optional[str]: 

285 for compat_rule in _relevant_dh_compat_rules(compat_level, ppkpf.info): 

286 install_pattern = compat_rule.get("install_pattern") 

287 if install_pattern is not None: 

288 return install_pattern 

289 return ppkpf.info.get("install_pattern") 

290 

291 

292def resolve_debhelper_config_files( 

293 debian_dir: VirtualPath, 

294 binary_packages: Mapping[str, BinaryPackage], 

295 debputy_plugin_metadata: DebputyPluginMetadata, 

296 dh_ppf_docs: Dict[str, PluginProvidedKnownPackagingFile], 

297 dh_rules_addons: AbstractSet[str], 

298 dh_compat_level: int, 

299 saw_dh: bool, 

300 ignore_paths: Container[str] = frozenset(), 

301) -> Tuple[List[PackagerProvidedFile], Optional[object], int]: 

302 dh_ppfs = {} 

303 commands, exit_code = _relevant_dh_commands(dh_rules_addons) 

304 

305 cmd = ["dh_assistant", "list-guessed-dh-config-files"] 

306 if dh_rules_addons: 

307 addons = ",".join(dh_rules_addons) 

308 cmd.append(f"--with={addons}") 

309 try: 

310 output = subprocess.check_output( 

311 cmd, 

312 stderr=subprocess.DEVNULL, 

313 ) 

314 except (subprocess.CalledProcessError, FileNotFoundError) as e: 

315 config_files = [] 

316 issues = None 

317 if isinstance(e, subprocess.CalledProcessError): 

318 exit_code = e.returncode 

319 else: 

320 exit_code = 127 

321 else: 

322 result = json.loads(output) 

323 config_files: List[Union[Mapping[str, Any], object]] = result.get( 

324 "config-files", [] 

325 ) 

326 issues = result.get("issues") 

327 dh_commands = resolve_active_and_inactive_dh_commands(dh_rules_addons) 

328 for config_file in config_files: 

329 if not isinstance(config_file, dict): 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true

330 continue 

331 if config_file.get("file-type") != "pkgfile": 331 ↛ 332line 331 didn't jump to line 332 because the condition on line 331 was never true

332 continue 

333 stem = config_file.get("pkgfile") 

334 if stem is None: 334 ↛ 335line 334 didn't jump to line 335 because the condition on line 334 was never true

335 continue 

336 internal = config_file.get("internal") 

337 if isinstance(internal, dict): 

338 bug_950723 = internal.get("bug#950723", False) is True 

339 else: 

340 bug_950723 = False 

341 commands = config_file.get("commands") 

342 documentation_uris = [] 

343 related_tools = [] 

344 seen_commands = set() 

345 seen_docs = set() 

346 ppkpf = dh_ppf_docs.get(stem) 

347 

348 if ppkpf: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true

349 dh_cmds = ppkpf.info.get("debhelper_commands") 

350 doc_uris = ppkpf.info.get("documentation_uris") 

351 default_priority = ppkpf.info.get("default_priority") 

352 if doc_uris is not None: 

353 seen_docs.update(doc_uris) 

354 documentation_uris.extend(doc_uris) 

355 if dh_cmds is not None: 

356 seen_commands.update(dh_cmds) 

357 related_tools.extend(dh_cmds) 

358 install_pattern = _kpf_install_pattern(dh_compat_level, ppkpf) 

359 post_formatting_rewrite = ppkpf.info.get("post_formatting_rewrite") 

360 packageless_is_fallback_for_all_packages = ppkpf.info.get( 

361 "packageless_is_fallback_for_all_packages", 

362 False, 

363 ) 

364 # If it is a debhelper PPF, then `has_active_command` is false by default. 

365 has_active_command = ppkpf.info.get("has_active_command", False) 

366 else: 

367 install_pattern = None 

368 default_priority = None 

369 post_formatting_rewrite = None 

370 packageless_is_fallback_for_all_packages = False 

371 has_active_command = False 

372 for command in commands: 

373 if isinstance(command, dict): 373 ↛ 372line 373 didn't jump to line 372 because the condition on line 373 was always true

374 command_name = command.get("command") 

375 if isinstance(command_name, str) and command_name: 375 ↛ 384line 375 didn't jump to line 384 because the condition on line 375 was always true

376 if command_name not in seen_commands: 376 ↛ 379line 376 didn't jump to line 379 because the condition on line 376 was always true

377 related_tools.append(command_name) 

378 seen_commands.add(command_name) 

379 manpage = f"man:{command_name}(1)" 

380 if manpage not in seen_docs: 380 ↛ 385line 380 didn't jump to line 385 because the condition on line 380 was always true

381 documentation_uris.append(manpage) 

382 seen_docs.add(manpage) 

383 else: 

384 continue 

385 is_active = command.get("is-active", True) 

386 if is_active is None and command_name in dh_commands.active_commands: 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true

387 is_active = True 

388 if not isinstance(is_active, bool): 388 ↛ 389line 388 didn't jump to line 389 because the condition on line 388 was never true

389 continue 

390 if is_active: 

391 has_active_command = True 

392 dh_ppfs[stem] = _fake_PPFClassSpec( 

393 debputy_plugin_metadata, 

394 stem, 

395 documentation_uris, 

396 install_pattern, 

397 default_priority=default_priority, 

398 post_formatting_rewrite=post_formatting_rewrite, 

399 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages, 

400 bug_950723=bug_950723, 

401 has_active_command=has_active_command if saw_dh else True, 

402 ) 

403 for ppkpf in dh_ppf_docs.values(): 403 ↛ 404line 403 didn't jump to line 404 because the loop on line 403 never started

404 stem = ppkpf.detection_value 

405 if stem in dh_ppfs: 

406 continue 

407 

408 default_priority = ppkpf.info.get("default_priority") 

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 has_active_command = ( 

416 ppkpf.info.get("has_active_command", False) if saw_dh else False 

417 ) 

418 if not has_active_command: 

419 dh_cmds = ppkpf.info.get("debhelper_commands") 

420 if dh_cmds: 

421 has_active_command = any( 

422 c in dh_commands.active_commands for c in dh_cmds 

423 ) 

424 dh_ppfs[stem] = _fake_PPFClassSpec( 

425 debputy_plugin_metadata, 

426 stem, 

427 ppkpf.info.get("documentation_uris"), 

428 install_pattern, 

429 default_priority=default_priority, 

430 post_formatting_rewrite=post_formatting_rewrite, 

431 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages, 

432 has_active_command=has_active_command, 

433 ) 

434 all_dh_ppfs = list( 

435 flatten_ppfs( 

436 detect_all_packager_provided_files( 

437 dh_ppfs, 

438 debian_dir, 

439 binary_packages, 

440 allow_fuzzy_matches=True, 

441 detect_typos=True, 

442 ignore_paths=ignore_paths, 

443 ) 

444 ) 

445 ) 

446 return all_dh_ppfs, issues, exit_code 

447 

448 

449def _merge_list( 

450 existing_table: Dict[str, Any], 

451 key: str, 

452 new_data: Optional[Sequence[str]], 

453) -> None: 

454 if not new_data: 

455 return 

456 existing_values = existing_table.get(key, []) 

457 if isinstance(existing_values, tuple): 

458 existing_values = list(existing_values) 

459 assert isinstance(existing_values, list) 

460 seen = set(existing_values) 

461 existing_values.extend(x for x in new_data if x not in seen) 

462 existing_table[key] = existing_values 

463 

464 

465def _merge_ppfs( 

466 identified: List[PackagingFileInfo], 

467 seen_paths: Dict[str, PackagingFileInfo], 

468 ppfs: List[PackagerProvidedFile], 

469 context: Mapping[str, PluginProvidedKnownPackagingFile], 

470 dh_compat_level: Optional[int], 

471) -> None: 

472 for ppf in ppfs: 

473 key = ppf.path.path 

474 ref_doc = ppf.definition.reference_documentation 

475 documentation_uris = ( 

476 ref_doc.format_documentation_uris if ref_doc is not None else None 

477 ) 

478 if not ppf.definition.installed_as_format.startswith("not-a-real-ppf"): 

479 try: 

480 parts = ppf.compute_dest() 

481 except RuntimeError: 

482 dest = None 

483 else: 

484 dest = "/".join(parts).lstrip(".") 

485 else: 

486 dest = None 

487 orig_details = seen_paths.get(key) 

488 if orig_details is None: 

489 details: PackagingFileInfo = { 

490 "path": key, 

491 "pkgfile-stem": ppf.definition.stem, 

492 "pkgfile-is-active-in-build": ppf.definition.has_active_command, 

493 "pkgfile-explicit-package-name": ppf.uses_explicit_package_name, 

494 "binary-package": ppf.package_name, 

495 } 

496 if ppf.expected_path is not None: 

497 details["likely-typo-of"] = ppf.expected_path 

498 identified.append(details) 

499 else: 

500 details = orig_details 

501 # We do not merge the "is typo" field; if the original 

502 for k, v in [ 

503 ("pkgfile-stem", ppf.definition.stem), 

504 ("pkgfile-explicit-package-name", ppf.definition.has_active_command), 

505 ("binary-package", ppf.package_name), 

506 ]: 

507 if k not in details: 

508 details[k] = v 

509 if ppf.definition.has_active_command and details.get( 

510 "pkgfile-is-active-in-build", False 

511 ): 

512 details["pkgfile-is-active-in-build"] = True 

513 if ppf.expected_path is None and "likely-typo-of" in details: 

514 del details["likely-typo-of"] 

515 

516 name_segment = ppf.name_segment 

517 arch_restriction = ppf.architecture_restriction 

518 if name_segment is not None and "pkgfile-name-segment" not in details: 

519 details["pkgfile-name-segment"] = name_segment 

520 if ( 

521 arch_restriction is not None 

522 and "pkgfile-architecture-restriction" not in details 

523 ): 

524 details["pkgfile-architecture-restriction"] = arch_restriction 

525 if ppf.fuzzy_match and key.endswith(".in"): 

526 _merge_list(details, "file-categories", ["generic-template"]) 

527 details["generates"] = key[:-3] 

528 elif assume_not_none(ppf.path.parent_dir).get(ppf.path.name + ".in"): 

529 _merge_list(details, "file-categories", ["generated"]) 

530 details["generated-from"] = key + ".in" 

531 if dest is not None and "install-path" not in details: 

532 details["install-path"] = dest 

533 

534 extra_details = context.get(ppf.definition.stem) 

535 if extra_details is not None: 

536 _add_known_packaging_data(details, extra_details, dh_compat_level) 

537 

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

539 

540 

541def _relevant_dh_commands(dh_rules_addons: Iterable[str]) -> Tuple[List[str], int]: 

542 cmd = ["dh_assistant", "list-commands", "--output-format=json"] 

543 if dh_rules_addons: 

544 addons = ",".join(dh_rules_addons) 

545 cmd.append(f"--with={addons}") 

546 try: 

547 output = subprocess.check_output( 

548 cmd, 

549 stderr=subprocess.DEVNULL, 

550 ) 

551 except (FileNotFoundError, subprocess.CalledProcessError) as e: 

552 exit_code = 127 

553 if isinstance(e, subprocess.CalledProcessError): 

554 exit_code = e.returncode 

555 return [], exit_code 

556 else: 

557 data = json.loads(output) 

558 commands_json = data.get("commands") 

559 commands = [] 

560 for command in commands_json: 

561 if isinstance(command, dict): 561 ↛ 560line 561 didn't jump to line 560 because the condition on line 561 was always true

562 command_name = command.get("command") 

563 if isinstance(command_name, str) and command_name: 563 ↛ 560line 563 didn't jump to line 560 because the condition on line 563 was always true

564 commands.append(command_name) 

565 return commands, 0 

566 

567 

568def _add_known_packaging_data( 

569 details: PackagingFileInfo, 

570 plugin_data: PluginProvidedKnownPackagingFile, 

571 dh_compat_level: Optional[int], 

572): 

573 install_pattern = _kpf_install_pattern( 

574 dh_compat_level, 

575 plugin_data, 

576 ) 

577 config_features = plugin_data.info.get("config_features") 

578 if config_features: 

579 config_features = expand_known_packaging_config_features( 

580 dh_compat_level or 0, 

581 config_features, 

582 ) 

583 _merge_list(details, "config-features", config_features) 

584 

585 if dh_compat_level is not None: 

586 extra_config_features = [] 

587 for dh_compat_rule in _relevant_dh_compat_rules( 

588 dh_compat_level, plugin_data.info 

589 ): 

590 cf = dh_compat_rule.get("add_config_features") 

591 if cf: 

592 extra_config_features.extend(cf) 

593 if extra_config_features: 

594 extra_config_features = expand_known_packaging_config_features( 

595 dh_compat_level, 

596 extra_config_features, 

597 ) 

598 _merge_list(details, "config-features", extra_config_features) 

599 if "install-pattern" not in details and install_pattern is not None: 

600 details["install-pattern"] = install_pattern 

601 for mk, ok in [ 

602 ("file_categories", "file-categories"), 

603 ("documentation_uris", "documentation-uris"), 

604 ("debputy_cmd_templates", "debputy-cmd-templates"), 

605 ]: 

606 value = plugin_data.info.get(mk) 

607 if value and ok == "debputy-cmd-templates": 

608 value = [escape_shell(*c) for c in value] 

609 _merge_list(details, ok, value) 

610 

611 

612def _scan_debian_dir(debian_dir: VirtualPath) -> Iterator[VirtualPath]: 

613 for p in debian_dir.iterdir: 

614 yield p 

615 if p.is_dir and p.path in ("debian/source", "debian/tests"): 

616 yield from p.iterdir 

617 

618 

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

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

621}