Coverage for src/debputy/plugin/api/impl_types.py: 78%

588 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-04-06 19:25 +0000

1import dataclasses 

2import os.path 

3import typing 

4from collections.abc import Callable, Sequence, Iterable, Mapping, Iterator, Container 

5from importlib.resources.abc import Traversable 

6from pathlib import Path 

7from typing import ( 

8 Optional, 

9 TYPE_CHECKING, 

10 TypeVar, 

11 cast, 

12 Any, 

13 TypedDict, 

14 NotRequired, 

15 Literal, 

16 Protocol, 

17) 

18from weakref import ref 

19 

20from debian.debian_support import DpkgArchTable 

21 

22from debputy._deb_options_profiles import DebBuildOptionsAndProfiles 

23from debputy.exceptions import ( 

24 DebputyFSIsROError, 

25 PluginAPIViolationError, 

26 PluginConflictError, 

27 UnhandledOrUnexpectedErrorFromPluginError, 

28 PluginBaseError, 

29 PluginInitializationError, 

30) 

31from debputy.filesystem_scan import as_path_def 

32from debputy.manifest_conditions import ConditionContext 

33from debputy.manifest_parser.exceptions import ManifestParseException 

34from debputy.manifest_parser.tagging_types import DebputyParsedContent, TypeMapping 

35from debputy.manifest_parser.util import AttributePath, check_integration_mode 

36from debputy.packages import BinaryPackage, SourcePackage 

37from debputy.plugin.api import ( 

38 VirtualPath, 

39 BinaryCtrlAccessor, 

40 PackageProcessingContext, 

41) 

42from debputy.plugin.api.spec import ( 

43 DebputyPluginInitializer, 

44 MetadataAutoDetector, 

45 DpkgTriggerType, 

46 ParserDocumentation, 

47 PackageProcessor, 

48 PathDef, 

49 ParserAttributeDocumentation, 

50 undocumented_attr, 

51 documented_attr, 

52 reference_documentation, 

53 PackagerProvidedFileReferenceDocumentation, 

54 TypeMappingDocumentation, 

55 DebputyIntegrationMode, 

56) 

57from debputy.plugin.plugin_state import ( 

58 run_in_context_of_plugin, 

59) 

60from debputy.substitution import VariableContext 

61from debputy.util import ( 

62 _error, 

63 _normalize_path, 

64 package_cross_check_precheck, 

65 PackageTypeSelector, 

66) 

67 

68if TYPE_CHECKING: 

69 from debputy.lsp.diagnostics import LintSeverity 

70 from debputy.plugin.api.spec import ( 

71 ServiceDetector, 

72 ServiceIntegrator, 

73 ) 

74 from debputy.manifest_parser.parser_data import ParserContextData 

75 from debputy.highlevel_manifest import ( 

76 HighLevelManifest, 

77 PackageTransformationDefinition, 

78 BinaryPackageData, 

79 ) 

80 from debputy.plugins.debputy.to_be_api_types import ( 

81 BuildRuleParsedFormat, 

82 BuildSystemRule, 

83 ) 

84 

85 

86TD = TypeVar("TD", bound=DebputyParsedContent | list[DebputyParsedContent]) 

87PF = TypeVar("PF") 

88SF = TypeVar("SF") 

89TP = TypeVar("TP") 

90TTP = type[TP] 

91BSR = TypeVar("BSR", bound="BuildSystemRule") 

92 

93DIPKWHandler = Callable[[str, AttributePath, "ParserContextData"], TP] 

94DIPHandler = Callable[[str, PF, AttributePath, "ParserContextData"], TP] 

95 

96 

97@dataclasses.dataclass(slots=True) 

98class DebputyPluginMetadata: 

99 plugin_name: str 

100 api_compat_version: int 

101 plugin_loader: Callable[[], Callable[["DebputyPluginInitializer"], None]] | None 

102 plugin_initializer: Callable[["DebputyPluginInitializer"], None] | None 

103 plugin_path: str 

104 plugin_doc_path_resolver: Callable[[], Traversable | Path | None] = lambda: None 

105 is_from_python_path: bool = False 

106 _is_initialized: bool = False 

107 _is_doc_path_resolved: bool = False 

108 _plugin_doc_path: Traversable | Path | None = None 

109 

110 @property 

111 def is_bundled(self) -> bool: 

112 return self.plugin_path == "<bundled>" 

113 

114 @property 

115 def is_loaded(self) -> bool: 

116 return self.plugin_initializer is not None 

117 

118 @property 

119 def is_initialized(self) -> bool: 

120 return self._is_initialized 

121 

122 @property 

123 def plugin_doc_path(self) -> Traversable | Path | None: 

124 if not self._is_doc_path_resolved: 

125 self._plugin_doc_path = self.plugin_doc_path_resolver() 

126 self._is_doc_path_resolved = True 

127 return self._plugin_doc_path 

128 

129 def initialize_plugin(self, api: "DebputyPluginInitializer") -> None: 

130 if self.is_initialized: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 raise RuntimeError("Cannot load plugins twice") 

132 if not self.is_loaded: 

133 self.load_plugin() 

134 plugin_initializer = self.plugin_initializer 

135 assert plugin_initializer is not None 

136 plugin_initializer(api) 

137 self._is_initialized = True 

138 

139 def load_plugin(self) -> None: 

140 plugin_loader = self.plugin_loader 

141 assert plugin_loader is not None 

142 try: 

143 self.plugin_initializer = run_in_context_of_plugin( 

144 self.plugin_name, 

145 plugin_loader, 

146 ) 

147 except PluginBaseError: 

148 raise 

149 except Exception as e: 

150 raise PluginInitializationError( 

151 f"Initialization of {self.plugin_name} failed due to its initializer raising an exception" 

152 ) from e 

153 assert self.plugin_initializer is not None 

154 

155 

156@dataclasses.dataclass(slots=True, frozen=True) 

157class PluginProvidedParser[PF, TP]: 

158 parser: "DeclarativeInputParser[PF]" 

159 handler: Callable[[str, PF, AttributePath, "ParserContextData"], TP] 

160 plugin_metadata: DebputyPluginMetadata 

161 

162 def parse( 

163 self, 

164 name: str, 

165 value: object, 

166 attribute_path: AttributePath, 

167 *, 

168 parser_context: "ParserContextData", 

169 ) -> TP: 

170 parsed_value = self.parser.parse_input( 

171 value, 

172 attribute_path, 

173 parser_context=parser_context, 

174 ) 

175 return self.handler(name, parsed_value, attribute_path, parser_context) 

176 

177 

178class PPFFormatParam(TypedDict): 

179 priority: int | None 

180 name: str 

181 owning_package: str 

182 

183 

184@dataclasses.dataclass(slots=True, frozen=True) 

185class PackagerProvidedFileClassSpec: 

186 debputy_plugin_metadata: DebputyPluginMetadata 

187 stem: str 

188 installed_as_format: str 

189 default_mode: int 

190 default_priority: int | None 

191 allow_name_segment: bool 

192 allow_architecture_segment: bool 

193 post_formatting_rewrite: Callable[[str], str] | None 

194 packageless_is_fallback_for_all_packages: bool 

195 package_types: PackageTypeSelector 

196 reservation_only: bool 

197 formatting_callback: Callable[[str, PPFFormatParam, VirtualPath], str] | None = None 

198 reference_documentation: PackagerProvidedFileReferenceDocumentation | None = None 

199 bug_950723: bool = False 

200 has_active_command: bool = True 

201 

202 @property 

203 def supports_priority(self) -> bool: 

204 return self.default_priority is not None 

205 

206 def compute_dest( 

207 self, 

208 assigned_name: str, 

209 # Note this method is currently used 1:1 inside plugin tests. 

210 *, 

211 owning_package: str | None = None, 

212 assigned_priority: int | None = None, 

213 path: VirtualPath | None = None, 

214 ) -> tuple[str, str]: 

215 if assigned_priority is not None and not self.supports_priority: 215 ↛ 216line 215 didn't jump to line 216 because the condition on line 215 was never true

216 raise ValueError( 

217 f"Cannot assign priority to packager provided files with stem" 

218 f' "{self.stem}" (e.g., "debian/foo.{self.stem}"). They' 

219 " do not use priority at all." 

220 ) 

221 

222 path_format = self.installed_as_format 

223 if self.supports_priority and assigned_priority is None: 

224 assigned_priority = self.default_priority 

225 

226 if owning_package is None: 

227 owning_package = assigned_name 

228 

229 params: PPFFormatParam = { 

230 "priority": assigned_priority, 

231 "name": assigned_name, 

232 "owning_package": owning_package, 

233 } 

234 

235 if self.formatting_callback is not None: 

236 if path is None: 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 raise ValueError( 

238 "The path parameter is required for PPFs with formatting_callback" 

239 ) 

240 dest_path = self.formatting_callback(path_format, params, path) 

241 else: 

242 dest_path = path_format.format(**params) 

243 

244 dirname, basename = os.path.split(dest_path) 

245 dirname = _normalize_path(dirname) 

246 

247 if self.post_formatting_rewrite: 

248 basename = self.post_formatting_rewrite(basename) 

249 return dirname, basename 

250 

251 

252@dataclasses.dataclass(slots=True) 

253class MetadataOrMaintscriptDetector: 

254 plugin_metadata: DebputyPluginMetadata 

255 detector_id: str 

256 detector: MetadataAutoDetector 

257 applies_to_package_types: PackageTypeSelector 

258 enabled: bool = True 

259 

260 def applies_to(self, binary_package: BinaryPackage) -> bool: 

261 return binary_package.package_type in self.applies_to_package_types 

262 

263 def run_detector( 

264 self, 

265 fs_root: "VirtualPath", 

266 ctrl: "BinaryCtrlAccessor", 

267 context: "PackageProcessingContext", 

268 ) -> None: 

269 try: 

270 self.detector(fs_root, ctrl, context) 

271 except DebputyFSIsROError as e: 

272 nv = self.plugin_metadata.plugin_name 

273 raise PluginAPIViolationError( 

274 f'The plugin {nv} violated the API contract for "metadata detectors"' 

275 " by attempting to mutate the provided file system in its metadata detector" 

276 f" with id {self.detector_id}. File system mutation is *not* supported at" 

277 " this stage (file system layout is committed and the attempted changes" 

278 " would be lost)." 

279 ) from e 

280 except UnhandledOrUnexpectedErrorFromPluginError as e: 

281 e.add_note( 

282 f"The exception was raised by the detector with the ID: {self.detector_id}" 

283 ) 

284 

285 

286class DeclarativeInputParser[TD]: 

287 @property 

288 def inline_reference_documentation(self) -> ParserDocumentation | None: 

289 return None 

290 

291 @property 

292 def expected_debputy_integration_mode( 

293 self, 

294 ) -> Container[DebputyIntegrationMode] | None: 

295 return None 

296 

297 @property 

298 def reference_documentation_url(self) -> str | None: 

299 doc = self.inline_reference_documentation 

300 return doc.documentation_reference_url if doc is not None else None 

301 

302 def parse_input( 

303 self, 

304 value: object, 

305 path: AttributePath, 

306 *, 

307 parser_context: Optional["ParserContextData"] = None, 

308 ) -> TD: 

309 raise NotImplementedError 

310 

311 

312class DelegatingDeclarativeInputParser(DeclarativeInputParser[TD]): 

313 __slots__ = ( 

314 "delegate", 

315 "_reference_documentation", 

316 "_expected_debputy_integration_mode", 

317 ) 

318 

319 def __init__( 

320 self, 

321 delegate: DeclarativeInputParser[TD], 

322 *, 

323 inline_reference_documentation: ParserDocumentation | None = None, 

324 expected_debputy_integration_mode: None | ( 

325 Container[DebputyIntegrationMode] 

326 ) = None, 

327 ) -> None: 

328 self.delegate = delegate 

329 self._reference_documentation = inline_reference_documentation 

330 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

331 

332 @property 

333 def expected_debputy_integration_mode( 

334 self, 

335 ) -> Container[DebputyIntegrationMode] | None: 

336 return self._expected_debputy_integration_mode 

337 

338 @property 

339 def inline_reference_documentation(self) -> ParserDocumentation | None: 

340 doc = self._reference_documentation 

341 if doc is None: 

342 return self.delegate.inline_reference_documentation 

343 return doc 

344 

345 

346class ListWrappedDeclarativeInputParser(DelegatingDeclarativeInputParser[TD]): 

347 __slots__ = () 

348 

349 def _doc_url_error_suffix(self, *, see_url_version: bool = False) -> str: 

350 doc_url = self.reference_documentation_url 

351 if doc_url is not None: 351 ↛ 355line 351 didn't jump to line 355 because the condition on line 351 was always true

352 if see_url_version: 352 ↛ 354line 352 didn't jump to line 354 because the condition on line 352 was always true

353 return f" Please see {doc_url} for the documentation." 

354 return f" (Documentation: {doc_url})" 

355 return "" 

356 

357 def parse_input( 

358 self, 

359 value: object, 

360 path: AttributePath, 

361 *, 

362 parser_context: Optional["ParserContextData"] = None, 

363 ) -> TD: 

364 check_integration_mode( 

365 path, parser_context, self._expected_debputy_integration_mode 

366 ) 

367 if not isinstance(value, list): 

368 doc_ref = self._doc_url_error_suffix(see_url_version=True) 

369 raise ManifestParseException( 

370 f"The attribute {path.path} must be a list.{doc_ref}" 

371 ) 

372 result = [] 

373 delegate = self.delegate 

374 for idx, element in enumerate(value): 

375 element_path = path[idx] 

376 result.append( 

377 delegate.parse_input( 

378 element, 

379 element_path, 

380 parser_context=parser_context, 

381 ) 

382 ) 

383 return result 

384 

385 

386class DispatchingParserBase[TP]: 

387 def __init__(self, manifest_attribute_path_template: str) -> None: 

388 self.manifest_attribute_path_template = manifest_attribute_path_template 

389 self._parsers: dict[str, PluginProvidedParser[Any, TP]] = {} 

390 

391 @property 

392 def unknown_keys_diagnostic_severity(self) -> Optional["LintSeverity"]: 

393 return "error" 

394 

395 def is_known_keyword(self, keyword: str) -> bool: 

396 return keyword in self._parsers 

397 

398 def registered_keywords(self) -> Iterable[str]: 

399 yield from self._parsers 

400 

401 def parser_for(self, keyword: str) -> PluginProvidedParser[Any, TP]: 

402 return self._parsers[keyword] 

403 

404 def register_keyword( 

405 self, 

406 keyword: str | Sequence[str], 

407 handler: DIPKWHandler, 

408 plugin_metadata: DebputyPluginMetadata, 

409 *, 

410 inline_reference_documentation: ParserDocumentation | None = None, 

411 ) -> None: 

412 reference_documentation_url = None 

413 if inline_reference_documentation: 

414 if inline_reference_documentation.attribute_doc: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true

415 raise ValueError( 

416 "Cannot provide per-attribute documentation for a value-less keyword!" 

417 ) 

418 if inline_reference_documentation.alt_parser_description: 418 ↛ 419line 418 didn't jump to line 419 because the condition on line 418 was never true

419 raise ValueError( 

420 "Cannot provide non-mapping-format documentation for a value-less keyword!" 

421 ) 

422 reference_documentation_url = ( 

423 inline_reference_documentation.documentation_reference_url 

424 ) 

425 parser = DeclarativeValuelessKeywordInputParser( 

426 inline_reference_documentation, 

427 documentation_reference=reference_documentation_url, 

428 ) 

429 

430 def _combined_handler( 

431 name: str, 

432 _ignored: Any, 

433 attr_path: AttributePath, 

434 context: "ParserContextData", 

435 ) -> TP: 

436 return handler(name, attr_path, context) 

437 

438 p = PluginProvidedParser( 

439 parser, 

440 _combined_handler, 

441 plugin_metadata, 

442 ) 

443 

444 self._add_parser(keyword, p) 

445 

446 def register_parser( 

447 self, 

448 keyword: str | list[str], 

449 parser: "DeclarativeInputParser[PF]", 

450 handler: Callable[[str, PF, AttributePath, "ParserContextData"], TP], 

451 plugin_metadata: DebputyPluginMetadata, 

452 ) -> None: 

453 p = PluginProvidedParser( 

454 parser, 

455 handler, 

456 plugin_metadata, 

457 ) 

458 self._add_parser(keyword, p) 

459 

460 def _add_parser( 

461 self, 

462 keyword: str | Iterable[str], 

463 ppp: PluginProvidedParser[PF, TP], 

464 ) -> None: 

465 ks = [keyword] if isinstance(keyword, str) else keyword 

466 for k in ks: 

467 existing_parser = self._parsers.get(k) 

468 if existing_parser is not None: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true

469 message = ( 

470 f'The rule name "{k}" is already taken by the plugin' 

471 f" {existing_parser.plugin_metadata.plugin_name}. This conflict was triggered" 

472 f" when plugin {ppp.plugin_metadata.plugin_name} attempted to register its parser." 

473 ) 

474 raise PluginConflictError( 

475 message, 

476 existing_parser.plugin_metadata, 

477 ppp.plugin_metadata, 

478 ) 

479 self._new_parser(k, ppp) 

480 

481 def _new_parser(self, keyword: str, ppp: PluginProvidedParser[PF, TP]) -> None: 

482 self._parsers[keyword] = ppp 

483 

484 def parse_input( 

485 self, 

486 orig_value: object, 

487 attribute_path: AttributePath, 

488 *, 

489 parser_context: "ParserContextData", 

490 ) -> TP: 

491 raise NotImplementedError 

492 

493 

494class DispatchingObjectParser( 

495 DispatchingParserBase[Mapping[str, Any]], 

496 DeclarativeInputParser[Mapping[str, Any]], 

497): 

498 def __init__( 

499 self, 

500 manifest_attribute_path_template: str, 

501 *, 

502 parser_documentation: ParserDocumentation | None = None, 

503 expected_debputy_integration_mode: None | ( 

504 Container[DebputyIntegrationMode] 

505 ) = None, 

506 unknown_keys_diagnostic_severity: Optional["LintSeverity"] = "error", 

507 allow_unknown_keys: bool = False, 

508 ) -> None: 

509 super().__init__(manifest_attribute_path_template) 

510 self._attribute_documentation: list[ParserAttributeDocumentation] = [] 

511 if parser_documentation is None: 

512 parser_documentation = reference_documentation() 

513 self._parser_documentation = parser_documentation 

514 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

515 self._unknown_keys_diagnostic_severity = unknown_keys_diagnostic_severity 

516 self._allow_unknown_keys = allow_unknown_keys 

517 

518 @property 

519 def unknown_keys_diagnostic_severity(self) -> Optional["LintSeverity"]: 

520 return self._unknown_keys_diagnostic_severity 

521 

522 @property 

523 def expected_debputy_integration_mode( 

524 self, 

525 ) -> Container[DebputyIntegrationMode] | None: 

526 return self._expected_debputy_integration_mode 

527 

528 @property 

529 def reference_documentation_url(self) -> str | None: 

530 return self._parser_documentation.documentation_reference_url 

531 

532 @property 

533 def inline_reference_documentation(self) -> ParserDocumentation | None: 

534 ref_doc = self._parser_documentation 

535 return reference_documentation( 

536 title=ref_doc.title, 

537 description=ref_doc.description, 

538 attributes=self._attribute_documentation, 

539 reference_documentation_url=self.reference_documentation_url, 

540 ) 

541 

542 def _new_parser(self, keyword: str, ppp: PluginProvidedParser[PF, TP]) -> None: 

543 super()._new_parser(keyword, ppp) 

544 doc = ppp.parser.inline_reference_documentation 

545 if doc is None or doc.description is None: 

546 self._attribute_documentation.append(undocumented_attr(keyword)) 

547 else: 

548 self._attribute_documentation.append( 

549 documented_attr(keyword, doc.description) 

550 ) 

551 

552 def register_child_parser( 

553 self, 

554 keyword: str, 

555 parser: "DispatchingObjectParser", 

556 plugin_metadata: DebputyPluginMetadata, 

557 *, 

558 on_end_parse_step: None | ( 

559 Callable[ 

560 [str, Mapping[str, Any] | None, AttributePath, "ParserContextData"], 

561 None, 

562 ] 

563 ) = None, 

564 nested_in_package_context: bool = False, 

565 ) -> None: 

566 def _handler( 

567 name: str, 

568 value: Mapping[str, Any], 

569 path: AttributePath, 

570 parser_context: "ParserContextData", 

571 ) -> Mapping[str, Any]: 

572 if on_end_parse_step is not None: 572 ↛ 574line 572 didn't jump to line 574 because the condition on line 572 was always true

573 on_end_parse_step(name, value, path, parser_context) 

574 return value 

575 

576 if nested_in_package_context: 

577 parser = InPackageContextParser( 

578 keyword, 

579 parser, 

580 ) 

581 

582 p = PluginProvidedParser( 

583 parser, 

584 _handler, 

585 plugin_metadata, 

586 ) 

587 self._add_parser(keyword, p) 

588 

589 def parse_input( 

590 self, 

591 orig_value: object, 

592 attribute_path: AttributePath, 

593 *, 

594 parser_context: "ParserContextData", 

595 ) -> TP: 

596 check_integration_mode( 

597 attribute_path, 

598 parser_context, 

599 self._expected_debputy_integration_mode, 

600 ) 

601 doc_ref = "" 

602 if self.reference_documentation_url is not None: 602 ↛ 606line 602 didn't jump to line 606 because the condition on line 602 was always true

603 doc_ref = ( 

604 f" Please see {self.reference_documentation_url} for the documentation." 

605 ) 

606 if not isinstance(orig_value, dict): 

607 raise ManifestParseException( 

608 f"The attribute {attribute_path.path_container_lc} must be a non-empty mapping.{doc_ref}" 

609 ) 

610 if not orig_value: 610 ↛ 611line 610 didn't jump to line 611 because the condition on line 610 was never true

611 raise ManifestParseException( 

612 f"The attribute {attribute_path.path_container_lc} must be a non-empty mapping.{doc_ref}" 

613 ) 

614 result = {} 

615 unknown_keys = orig_value.keys() - self._parsers.keys() 

616 if unknown_keys and not self._allow_unknown_keys: 616 ↛ 617line 616 didn't jump to line 617 because the condition on line 616 was never true

617 first_key = next(iter(unknown_keys)) 

618 remaining_valid_attributes = self._parsers.keys() - orig_value.keys() 

619 if not remaining_valid_attributes: 

620 raise ManifestParseException( 

621 f'The attribute "{first_key}" is not applicable at {attribute_path.path} (with the' 

622 f" current set of plugins).{doc_ref}" 

623 ) 

624 remaining_valid_attribute_names = ", ".join(remaining_valid_attributes) 

625 raise ManifestParseException( 

626 f'The attribute "{first_key}" is not applicable at {attribute_path.path} (with the current set' 

627 " of plugins). Possible attributes available (and not already used) are:" 

628 f" {remaining_valid_attribute_names}.{doc_ref}" 

629 ) 

630 # Parse order is important for the root level (currently we use rule registration order) 

631 for key, provided_parser in self._parsers.items(): 

632 value = orig_value.get(key) 

633 if value is None: 

634 if isinstance(provided_parser.parser, DispatchingObjectParser): 

635 provided_parser.handler( 

636 key, 

637 {}, 

638 attribute_path[key], 

639 parser_context, 

640 ) 

641 continue 

642 value_path = attribute_path[key] 

643 if provided_parser is None: 643 ↛ 644line 643 didn't jump to line 644 because the condition on line 643 was never true

644 valid_keys = ", ".join(sorted(self._parsers.keys())) 

645 raise ManifestParseException( 

646 f'Unknown or unsupported option "{key}" at {value_path.path}.' 

647 " Valid options at this location are:" 

648 f" {valid_keys}\n{doc_ref}" 

649 ) 

650 parsed_value = provided_parser.parse( 

651 key, value, value_path, parser_context=parser_context 

652 ) 

653 result[key] = parsed_value 

654 return result 

655 

656 

657@dataclasses.dataclass(slots=True, frozen=True) 

658class PackageContextData[TP]: 

659 resolved_package_name: str 

660 value: TP 

661 

662 

663class InPackageContextParser( 

664 DelegatingDeclarativeInputParser[Mapping[str, PackageContextData[TP]]] 

665): 

666 __slots__ = () 

667 

668 def __init__( 

669 self, 

670 manifest_attribute_path_template: str, 

671 delegate: DeclarativeInputParser[TP], 

672 *, 

673 parser_documentation: ParserDocumentation | None = None, 

674 ) -> None: 

675 self.manifest_attribute_path_template = manifest_attribute_path_template 

676 self._attribute_documentation: list[ParserAttributeDocumentation] = [] 

677 super().__init__(delegate, inline_reference_documentation=parser_documentation) 

678 

679 def parse_input( 

680 self, 

681 orig_value: object, 

682 attribute_path: AttributePath, 

683 *, 

684 parser_context: Optional["ParserContextData"] = None, 

685 ) -> TP: 

686 assert parser_context is not None 

687 check_integration_mode( 

688 attribute_path, 

689 parser_context, 

690 self._expected_debputy_integration_mode, 

691 ) 

692 doc_ref = "" 

693 if self.reference_documentation_url is not None: 693 ↛ 697line 693 didn't jump to line 697 because the condition on line 693 was always true

694 doc_ref = ( 

695 f" Please see {self.reference_documentation_url} for the documentation." 

696 ) 

697 if not isinstance(orig_value, dict) or not orig_value: 697 ↛ 698line 697 didn't jump to line 698 because the condition on line 697 was never true

698 raise ManifestParseException( 

699 f"The attribute {attribute_path.path_container_lc} must be a non-empty mapping.{doc_ref}" 

700 ) 

701 delegate = self.delegate 

702 result = {} 

703 for package_name_raw, value in orig_value.items(): 

704 

705 definition_source = attribute_path[package_name_raw] 

706 package_name = package_name_raw 

707 if "{{" in package_name: 

708 package_name = parser_context.substitution.substitute( 

709 package_name_raw, 

710 definition_source.path, 

711 ) 

712 package_state: PackageTransformationDefinition 

713 with parser_context.binary_package_context(package_name) as package_state: 

714 if package_state.is_auto_generated_package: 714 ↛ 716line 714 didn't jump to line 716 because the condition on line 714 was never true

715 # Maybe lift (part) of this restriction. 

716 raise ManifestParseException( 

717 f'Cannot define rules for package "{package_name}" (at {definition_source.path}). It is an' 

718 " auto-generated package." 

719 ) 

720 parsed_value = delegate.parse_input( 

721 value, definition_source, parser_context=parser_context 

722 ) 

723 result[package_name_raw] = PackageContextData( 

724 package_name, parsed_value 

725 ) 

726 return result 

727 

728 

729class DispatchingTableParser( 

730 DispatchingParserBase[TP], 

731 DeclarativeInputParser[TP], 

732): 

733 def __init__(self, base_type: TTP, manifest_attribute_path_template: str) -> None: 

734 super().__init__(manifest_attribute_path_template) 

735 self.base_type = base_type 

736 

737 def parse_input( 

738 self, 

739 orig_value: object, 

740 attribute_path: AttributePath, 

741 *, 

742 parser_context: "ParserContextData", 

743 ) -> TP: 

744 if isinstance(orig_value, str): 744 ↛ 745line 744 didn't jump to line 745 because the condition on line 744 was never true

745 key = orig_value 

746 value = None 

747 value_path = attribute_path 

748 elif isinstance(orig_value, dict): 748 ↛ 759line 748 didn't jump to line 759 because the condition on line 748 was always true

749 if len(orig_value) != 1: 749 ↛ 750line 749 didn't jump to line 750 because the condition on line 749 was never true

750 valid_keys = ", ".join(sorted(self._parsers.keys())) 

751 raise ManifestParseException( 

752 f'The mapping "{attribute_path.path}" had two keys, but it should only have one top level key.' 

753 " Maybe you are missing a list marker behind the second key or some indentation. The" 

754 f" possible keys are: {valid_keys}" 

755 ) 

756 key, value = next(iter(orig_value.items())) 

757 value_path = attribute_path[key] 

758 else: 

759 raise ManifestParseException( 

760 f"The attribute {attribute_path.path} must be a string or a mapping." 

761 ) 

762 provided_parser = self._parsers.get(key) 

763 if provided_parser is None: 763 ↛ 764line 763 didn't jump to line 764 because the condition on line 763 was never true

764 valid_keys = ", ".join(sorted(self._parsers.keys())) 

765 raise ManifestParseException( 

766 f'Unknown or unsupported action "{key}" at {value_path.path}.' 

767 " Valid actions at this location are:" 

768 f" {valid_keys}" 

769 ) 

770 return provided_parser.parse( 

771 key, value, value_path, parser_context=parser_context 

772 ) 

773 

774 

775@dataclasses.dataclass(slots=True) 

776class DeclarativeValuelessKeywordInputParser(DeclarativeInputParser[None]): 

777 inline_reference_documentation: ParserDocumentation | None = None 

778 documentation_reference: str | None = None 

779 

780 def parse_input( 

781 self, 

782 value: object, 

783 path: AttributePath, 

784 *, 

785 parser_context: Optional["ParserContextData"] = None, 

786 ) -> TD: 

787 if value is None: 

788 return cast("TD", value) 

789 if self.documentation_reference is not None: 

790 doc_ref = f" (Documentation: {self.documentation_reference})" 

791 else: 

792 doc_ref = "" 

793 raise ManifestParseException( 

794 f"Expected attribute {path.path} to be a string.{doc_ref}" 

795 ) 

796 

797 

798@dataclasses.dataclass(slots=True) 

799class PluginProvidedManifestVariable: 

800 plugin_metadata: DebputyPluginMetadata 

801 variable_name: str 

802 variable_value: str | Callable[[VariableContext], str] | None 

803 is_context_specific_variable: bool 

804 variable_reference_documentation: str | None = None 

805 is_documentation_placeholder: bool = False 

806 is_for_special_case: bool = False 

807 

808 @property 

809 def is_internal(self) -> bool: 

810 return self.variable_name.startswith("_") or ":_" in self.variable_name 

811 

812 @property 

813 def is_token(self) -> bool: 

814 return self.variable_name.startswith("token:") 

815 

816 def resolve(self, variable_context: VariableContext) -> str: 

817 value_resolver = self.variable_value 

818 if isinstance(value_resolver, str): 

819 res = value_resolver 

820 elif value_resolver is None: 820 ↛ 821line 820 didn't jump to line 821 because the condition on line 820 was never true

821 _error(f"variable {self.variable_name} is not set") 

822 else: 

823 res = value_resolver(variable_context) 

824 return res 

825 

826 

827@dataclasses.dataclass(slots=True, frozen=True) 

828class AutomaticDiscardRuleExample: 

829 content: Sequence[tuple[PathDef, bool]] 

830 description: str | None = None 

831 

832 

833def automatic_discard_rule_example( 

834 *content: str | PathDef | tuple[str | PathDef, bool], 

835 example_description: str | None = None, 

836) -> AutomaticDiscardRuleExample: 

837 """Provide an example for an automatic discard rule 

838 

839 The return value of this method should be passed to the `examples` parameter of 

840 `automatic_discard_rule` method - either directly for a single example or as a 

841 part of a sequence of examples. 

842 

843 >>> # Possible example for an exclude rule for ".la" files 

844 >>> # Example shows two files; The ".la" file that will be removed and another file that 

845 >>> # will be kept. 

846 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

847 ... "usr/lib/libfoo.la", 

848 ... ("usr/lib/libfoo.so.1.0.0", False), 

849 ... ) 

850 AutomaticDiscardRuleExample(...) 

851 

852 Keep in mind that you have to explicitly include directories that are relevant for the test 

853 if you want them shown. Also, if a directory is excluded, all path beneath it will be 

854 automatically excluded in the example as well. Your example data must account for that. 

855 

856 >>> # Possible example for python cache file discard rule 

857 >>> # In this example, we explicitly list the __pycache__ directory itself because we 

858 >>> # want it shown in the output (otherwise, we could have omitted it) 

859 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

860 ... (".../foo.py", False), 

861 ... ".../__pycache__/", 

862 ... ".../__pycache__/...", 

863 ... ".../foo.pyc", 

864 ... ".../foo.pyo", 

865 ... ) 

866 AutomaticDiscardRuleExample(...) 

867 

868 Note: Even if `__pycache__` had been implicit, the result would have been the same. However, 

869 the rendered example would not have shown the directory on its own. The use of `...` as 

870 path names is useful for denoting "anywhere" or "anything". Though, there is nothing "magic" 

871 about this name - it happens to be allowed as a path name (unlike `.` or `..`). 

872 

873 These examples can be seen via `debputy plugin show automatic-discard-rules <name-here>`. 

874 

875 :param content: The content of the example. Each element can be either a path definition or 

876 a tuple of a path definition followed by a verdict (boolean). Each provided path definition 

877 describes the paths to be presented in the example. Implicit paths such as parent 

878 directories will be created but not shown in the example. Therefore, if a directory is 

879 relevant to the example, be sure to explicitly list it. 

880 

881 The verdict associated with a path determines whether the path should be discarded (when 

882 True) or kept (when False). When a path is not explicitly associated with a verdict, the 

883 verdict is assumed to be discarded (True). 

884 :param example_description: An optional description displayed together with the example. 

885 :return: An opaque data structure containing the example. 

886 """ 

887 example = [] 

888 for d in content: 

889 if not isinstance(d, tuple): 

890 pd = d 

891 verdict = True 

892 else: 

893 pd, verdict = d 

894 

895 path_def = as_path_def(pd) 

896 example.append((path_def, verdict)) 

897 

898 if not example: 898 ↛ 899line 898 didn't jump to line 899 because the condition on line 898 was never true

899 raise ValueError("At least one path must be given for an example") 

900 

901 return AutomaticDiscardRuleExample( 

902 tuple(example), 

903 description=example_description, 

904 ) 

905 

906 

907@dataclasses.dataclass(slots=True, frozen=True) 

908class PluginProvidedPackageProcessor: 

909 processor_id: str 

910 applies_to_package_types: PackageTypeSelector 

911 package_processor: PackageProcessor 

912 dependencies: frozenset[tuple[str, str]] 

913 plugin_metadata: DebputyPluginMetadata 

914 

915 def applies_to(self, binary_package: BinaryPackage) -> bool: 

916 return binary_package.package_type in self.applies_to_package_types 

917 

918 @property 

919 def dependency_id(self) -> tuple[str, str]: 

920 return self.plugin_metadata.plugin_name, self.processor_id 

921 

922 def run_package_processor( 

923 self, 

924 fs_root: "VirtualPath", 

925 unused: None, 

926 context: "PackageProcessingContext", 

927 ) -> None: 

928 self.package_processor(fs_root, unused, context) 

929 

930 

931@dataclasses.dataclass(slots=True, frozen=True) 

932class PluginProvidedDiscardRule: 

933 name: str 

934 plugin_metadata: DebputyPluginMetadata 

935 discard_check: Callable[[VirtualPath], bool] 

936 reference_documentation: str | None 

937 examples: Sequence[AutomaticDiscardRuleExample] = tuple() 

938 

939 def should_discard(self, path: VirtualPath) -> bool: 

940 return self.discard_check(path) 

941 

942 

943@dataclasses.dataclass(slots=True, frozen=True) 

944class ServiceManagerDetails: 

945 service_manager: str 

946 service_detector: "ServiceDetector" 

947 service_integrator: "ServiceIntegrator" 

948 plugin_metadata: DebputyPluginMetadata 

949 

950 

951class ReferenceValue(TypedDict): 

952 description: str 

953 

954 

955def _reference_data_value( 

956 *, 

957 description: str, 

958) -> ReferenceValue: 

959 return { 

960 "description": description, 

961 } 

962 

963 

964KnownPackagingFileCategories = Literal[ 

965 "generated", 

966 "generic-template", 

967 "ppf-file", 

968 "ppf-control-file", 

969 "maint-config", 

970 "pkg-metadata", 

971 "pkg-helper-config", 

972 "testing", 

973 "lint-config", 

974] 

975KNOWN_PACKAGING_FILE_CATEGORY_DESCRIPTIONS: Mapping[ 

976 KnownPackagingFileCategories, ReferenceValue 

977] = { 

978 "generated": _reference_data_value( 

979 description="The file is (likely) generated from another file" 

980 ), 

981 "generic-template": _reference_data_value( 

982 description="The file is (likely) a generic template that generates a known packaging file. While the" 

983 " file is annotated as if it was the target file, the file might uses a custom template" 

984 " language inside it." 

985 ), 

986 "ppf-file": _reference_data_value( 

987 description="Packager provided file to be installed on the file system - usually as-is." 

988 " When `install-pattern` or `install-path` are provided, this is where the file is installed." 

989 ), 

990 "ppf-control-file": _reference_data_value( 

991 description="Packager provided file that becomes a control file - possible after processing. " 

992 " If `install-pattern` or `install-path` are provided, they denote where the is placed" 

993 " (generally, this will be of the form `DEBIAN/<name>`)" 

994 ), 

995 "maint-config": _reference_data_value( 

996 description="Maintenance configuration for a specific tool that the maintainer uses (tool / style preferences)" 

997 ), 

998 "pkg-metadata": _reference_data_value( 

999 description="The file is related to standard package metadata (usually documented in Debian Policy)" 

1000 ), 

1001 "pkg-helper-config": _reference_data_value( 

1002 description="The file is packaging helper configuration or instruction file" 

1003 ), 

1004 "testing": _reference_data_value( 

1005 description="The file is related to automated testing (autopkgtests, salsa/gitlab CI)." 

1006 ), 

1007 "lint-config": _reference_data_value( 

1008 description="The file is related to a linter (such as overrides for false-positives or style preferences)" 

1009 ), 

1010} 

1011 

1012KnownPackagingConfigFeature = Literal[ 

1013 "dh-filearray", 

1014 "dh-filedoublearray", 

1015 "dh-hash-subst", 

1016 "dh-dollar-subst", 

1017 "dh-glob", 

1018 "dh-partial-glob", 

1019 "dh-late-glob", 

1020 "dh-glob-after-execute", 

1021 "dh-executable-config", 

1022 "dh-custom-format", 

1023 "dh-file-list", 

1024 "dh-install-list", 

1025 "dh-install-list-dest-dir-like-dh_install", 

1026 "dh-install-list-fixed-dest-dir", 

1027 "dh-fixed-dest-dir", 

1028 "dh-exec-rename", 

1029 "dh-docs-only", 

1030] 

1031 

1032KNOWN_PACKAGING_FILE_CONFIG_FEATURE_DESCRIPTION: Mapping[ 

1033 KnownPackagingConfigFeature, ReferenceValue 

1034] = { 

1035 "dh-filearray": _reference_data_value( 

1036 description="The file will be read as a list of space/newline separated tokens", 

1037 ), 

1038 "dh-filedoublearray": _reference_data_value( 

1039 description="Each line in the file will be read as a list of space-separated tokens", 

1040 ), 

1041 "dh-hash-subst": _reference_data_value( 

1042 description="Supports debhelper #PACKAGE# style substitutions (udebs often excluded)", 

1043 ), 

1044 "dh-dollar-subst": _reference_data_value( 

1045 description="Supports debhelper ${PACKAGE} style substitutions (usually requires compat 13+)", 

1046 ), 

1047 "dh-glob": _reference_data_value( 

1048 description="Supports standard debhelper globing", 

1049 ), 

1050 "dh-partial-glob": _reference_data_value( 

1051 description="Supports standard debhelper globing but only to a subset of the values (implies dh-late-glob)", 

1052 ), 

1053 "dh-late-glob": _reference_data_value( 

1054 description="Globbing is done separately instead of using the built-in function", 

1055 ), 

1056 "dh-glob-after-execute": _reference_data_value( 

1057 description="When the dh config file is executable, the generated output will be subject to globbing", 

1058 ), 

1059 "dh-executable-config": _reference_data_value( 

1060 description="If marked executable, debhelper will execute the file and read its output", 

1061 ), 

1062 "dh-custom-format": _reference_data_value( 

1063 description="The dh tool will or may have a custom parser for this file", 

1064 ), 

1065 "dh-file-list": _reference_data_value( 

1066 description="The dh file contains a list of paths to be processed", 

1067 ), 

1068 "dh-install-list": _reference_data_value( 

1069 description="The dh file contains a list of paths/globs to be installed but the tool specific knowledge" 

1070 " required to understand the file cannot be conveyed via this interface.", 

1071 ), 

1072 "dh-install-list-dest-dir-like-dh_install": _reference_data_value( 

1073 description="The dh file is processed similar to dh_install (notably dest-dir handling derived" 

1074 " from the path or the last token on the line)", 

1075 ), 

1076 "dh-install-list-fixed-dest-dir": _reference_data_value( 

1077 description="The dh file is an install list and the dest-dir is always the same for all patterns" 

1078 " (when `install-pattern` or `install-path` are provided, they identify the directory - not the file location)", 

1079 ), 

1080 "dh-exec-rename": _reference_data_value( 

1081 description="When `dh-exec` is the interpreter of this dh config file, its renaming (=>) feature can be" 

1082 " requested/used", 

1083 ), 

1084 "dh-docs-only": _reference_data_value( 

1085 description="The dh config file is used for documentation only. Implicit <!nodocs> Build-Profiles support", 

1086 ), 

1087} 

1088 

1089CONFIG_FEATURE_ALIASES: dict[ 

1090 KnownPackagingConfigFeature, list[tuple[KnownPackagingConfigFeature, int]] 

1091] = { 

1092 "dh-filearray": [ 

1093 ("dh-filearray", 0), 

1094 ("dh-executable-config", 9), 

1095 ("dh-dollar-subst", 13), 

1096 ], 

1097 "dh-filedoublearray": [ 

1098 ("dh-filedoublearray", 0), 

1099 ("dh-executable-config", 9), 

1100 ("dh-dollar-subst", 13), 

1101 ], 

1102} 

1103 

1104 

1105def _implies( 

1106 features: list[KnownPackagingConfigFeature], 

1107 seen: set[KnownPackagingConfigFeature], 

1108 implying: Sequence[KnownPackagingConfigFeature], 

1109 implied: KnownPackagingConfigFeature, 

1110) -> None: 

1111 if implied in seen: 

1112 return 

1113 if all(f in seen for f in implying): 

1114 seen.add(implied) 

1115 features.append(implied) 

1116 

1117 

1118def expand_known_packaging_config_features( 

1119 compat_level: int, 

1120 features: list[KnownPackagingConfigFeature], 

1121) -> list[KnownPackagingConfigFeature]: 

1122 final_features: list[KnownPackagingConfigFeature] = [] 

1123 seen = set() 

1124 for feature in features: 

1125 expanded = CONFIG_FEATURE_ALIASES.get(feature) 

1126 if not expanded: 

1127 expanded = [(feature, 0)] 

1128 for v, c in expanded: 

1129 if compat_level < c or v in seen: 

1130 continue 

1131 seen.add(v) 

1132 final_features.append(v) 

1133 if "dh-glob" in seen and "dh-late-glob" in seen: 

1134 final_features.remove("dh-glob") 

1135 

1136 _implies(final_features, seen, ["dh-partial-glob"], "dh-late-glob") 

1137 _implies( 

1138 final_features, 

1139 seen, 

1140 ["dh-late-glob", "dh-executable-config"], 

1141 "dh-glob-after-execute", 

1142 ) 

1143 return sorted(final_features) 

1144 

1145 

1146class DHCompatibilityBasedRule(DebputyParsedContent): 

1147 install_pattern: NotRequired[str] 

1148 add_config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1149 starting_with_compat_level: NotRequired[int] 

1150 

1151 

1152class KnownPackagingFileInfo(DebputyParsedContent): 

1153 # Exposed directly in the JSON plugin parsing; be careful with changes 

1154 path: NotRequired[str] 

1155 pkgfile: NotRequired[str] 

1156 detection_method: NotRequired[Literal["path", "dh.pkgfile"]] 

1157 file_categories: NotRequired[list[KnownPackagingFileCategories]] 

1158 documentation_uris: NotRequired[list[str]] 

1159 debputy_cmd_templates: NotRequired[list[list[str]]] 

1160 debhelper_commands: NotRequired[list[str]] 

1161 config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1162 install_pattern: NotRequired[str] 

1163 dh_compat_rules: NotRequired[list[DHCompatibilityBasedRule]] 

1164 default_priority: NotRequired[int] 

1165 post_formatting_rewrite: NotRequired[Literal["period-to-underscore"]] 

1166 packageless_is_fallback_for_all_packages: NotRequired[bool] 

1167 has_active_command: NotRequired[bool] 

1168 

1169 

1170@dataclasses.dataclass(slots=True) 

1171class PluginProvidedKnownPackagingFile: 

1172 info: KnownPackagingFileInfo 

1173 detection_method: Literal["path", "dh.pkgfile"] 

1174 detection_value: str 

1175 plugin_metadata: DebputyPluginMetadata 

1176 

1177 

1178class BuildSystemAutoDetector(Protocol): 

1179 

1180 def __call__(self, source_root: VirtualPath, *args: Any, **kwargs: Any) -> bool: ... 1180 ↛ exitline 1180 didn't return from function '__call__' because

1181 

1182 

1183@dataclasses.dataclass(slots=True, frozen=True) 

1184class PluginProvidedTypeMapping: 

1185 mapped_type: TypeMapping[Any, Any] 

1186 reference_documentation: TypeMappingDocumentation | None 

1187 plugin_metadata: DebputyPluginMetadata 

1188 

1189 

1190@dataclasses.dataclass(slots=True, frozen=True) 

1191class PluginProvidedBuildSystemAutoDetection[BSR]: 

1192 manifest_keyword: str 

1193 build_system_rule_type: type[BSR] 

1194 detector: BuildSystemAutoDetector 

1195 constructor: Callable[ 

1196 ["BuildRuleParsedFormat", AttributePath, "HighLevelManifest"], 

1197 BSR, 

1198 ] 

1199 auto_detection_shadow_build_systems: frozenset[str] 

1200 plugin_metadata: DebputyPluginMetadata 

1201 

1202 

1203class PackageDataTable: 

1204 def __init__(self, package_data_table: Mapping[str, "BinaryPackageData"]) -> None: 

1205 self._package_data_table = package_data_table 

1206 # This is enabled for metadata-detectors. But it is deliberate not enabled for package processors, 

1207 # because it is not clear how it should interact with dependencies. For metadata-detectors, things 

1208 # read-only and there are no dependencies, so we cannot "get them wrong". 

1209 self.enable_cross_package_checks = False 

1210 

1211 def __iter__(self) -> Iterator["BinaryPackageData"]: 

1212 return iter(self._package_data_table.values()) 

1213 

1214 def __getitem__(self, item: str) -> "BinaryPackageData": 

1215 return self._package_data_table[item] 

1216 

1217 def __contains__(self, item: str) -> bool: 

1218 return item in self._package_data_table 

1219 

1220 

1221class PackageProcessingContextProvider(PackageProcessingContext): 

1222 __slots__ = ( 

1223 "_manifest", 

1224 "_binary_package", 

1225 "_related_udeb_package", 

1226 "_package_data_table", 

1227 "_cross_check_cache", 

1228 ) 

1229 

1230 def __init__( 

1231 self, 

1232 manifest: "HighLevelManifest", 

1233 binary_package: BinaryPackage, 

1234 related_udeb_package: BinaryPackage | None, 

1235 package_data_table: PackageDataTable, 

1236 ) -> None: 

1237 self._manifest = manifest 

1238 self._binary_package = binary_package 

1239 self._related_udeb_package = related_udeb_package 

1240 self._package_data_table = ref(package_data_table) 

1241 self._cross_check_cache: None | ( 

1242 Sequence[tuple[BinaryPackage, "VirtualPath"]] 

1243 ) = None 

1244 

1245 def _package_state_for( 

1246 self, 

1247 package: BinaryPackage, 

1248 ) -> "PackageTransformationDefinition": 

1249 return self._manifest.package_state_for(package.name) 

1250 

1251 def _package_version_for( 

1252 self, 

1253 package: BinaryPackage, 

1254 ) -> str: 

1255 package_state = self._package_state_for(package) 

1256 version = package_state.binary_version 

1257 if version is not None: 

1258 return version 

1259 return self._manifest.source_version( 

1260 include_binnmu_version=not package.is_arch_all 

1261 ) 

1262 

1263 @property 

1264 def source_package(self) -> SourcePackage: 

1265 return self._manifest.source_package 

1266 

1267 @property 

1268 def binary_package(self) -> BinaryPackage: 

1269 return self._binary_package 

1270 

1271 @property 

1272 def related_udeb_package(self) -> BinaryPackage | None: 

1273 return self._related_udeb_package 

1274 

1275 @property 

1276 def binary_package_version(self) -> str: 

1277 return self._package_version_for(self._binary_package) 

1278 

1279 @property 

1280 def related_udeb_package_version(self) -> str | None: 

1281 udeb = self._related_udeb_package 

1282 if udeb is None: 

1283 return None 

1284 return self._package_version_for(udeb) 

1285 

1286 def accessible_package_roots(self) -> Iterable[tuple[BinaryPackage, "VirtualPath"]]: 

1287 package_table = self._package_data_table() 

1288 if package_table is None: 

1289 raise ReferenceError( 

1290 "Internal error: package_table was garbage collected too early" 

1291 ) 

1292 if not package_table.enable_cross_package_checks: 

1293 raise PluginAPIViolationError( 

1294 "Cross package content checks are not available at this time." 

1295 ) 

1296 cache = self._cross_check_cache 

1297 if cache is None: 

1298 matches = [] 

1299 pkg = self.binary_package 

1300 for pkg_data in package_table: 

1301 if pkg_data.binary_package.name == pkg.name: 

1302 continue 

1303 res = package_cross_check_precheck(pkg, pkg_data.binary_package) 

1304 if not res[0]: 

1305 continue 

1306 matches.append((pkg_data.binary_package, pkg_data.fs_root)) 

1307 cache = tuple(matches) if matches else tuple() 

1308 self._cross_check_cache = cache 

1309 return cache 

1310 

1311 def manifest_configuration[T]( 

1312 self, 

1313 context_package: SourcePackage | BinaryPackage, 

1314 value_type: type[T], 

1315 ) -> T | None: 

1316 return self._manifest.manifest_configuration(context_package, value_type) 

1317 

1318 @property 

1319 def dpkg_arch_query_table(self) -> DpkgArchTable: 

1320 return self._manifest.dpkg_arch_query_table 

1321 

1322 @property 

1323 def deb_options_and_profiles(self) -> DebBuildOptionsAndProfiles: 

1324 return self._manifest.deb_options_and_profiles 

1325 

1326 @property 

1327 def source_condition_context(self) -> ConditionContext: 

1328 return self._manifest.source_condition_context 

1329 

1330 def condition_context( 

1331 self, binary_package: BinaryPackage | None 

1332 ) -> ConditionContext: 

1333 return self._manifest.condition_context(binary_package) 

1334 

1335 

1336@dataclasses.dataclass(slots=True, frozen=True) 

1337class PluginProvidedTrigger: 

1338 dpkg_trigger_type: DpkgTriggerType 

1339 dpkg_trigger_target: str 

1340 provider: DebputyPluginMetadata 

1341 provider_source_id: str 

1342 

1343 def serialized_format(self) -> str: 

1344 return f"{self.dpkg_trigger_type} {self.dpkg_trigger_target}"