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

583 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-04 10:15 +0000

1import dataclasses 

2import os.path 

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

4from importlib.resources.abc import Traversable 

5from pathlib import Path 

6from typing import ( 

7 Optional, 

8 Generic, 

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 _normalize_path, package_cross_check_precheck 

62 

63if TYPE_CHECKING: 

64 from debputy.lsp.diagnostics import LintSeverity 

65 from debputy.plugin.api.spec import ( 

66 ServiceDetector, 

67 ServiceIntegrator, 

68 ) 

69 from debputy.manifest_parser.parser_data import ParserContextData 

70 from debputy.highlevel_manifest import ( 

71 HighLevelManifest, 

72 PackageTransformationDefinition, 

73 BinaryPackageData, 

74 ) 

75 from debputy.plugins.debputy.to_be_api_types import ( 

76 BuildRuleParsedFormat, 

77 ) 

78 

79 

80TD = TypeVar("TD", bound="Union[DebputyParsedContent, List[DebputyParsedContent]]") 

81PF = TypeVar("PF") 

82SF = TypeVar("SF") 

83TP = TypeVar("TP") 

84TTP = type[TP] 

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

86 

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

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

89 

90 

91@dataclasses.dataclass(slots=True) 

92class DebputyPluginMetadata: 

93 plugin_name: str 

94 api_compat_version: int 

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

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

97 plugin_path: str 

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

99 is_from_python_path: bool = False 

100 _is_initialized: bool = False 

101 _is_doc_path_resolved: bool = False 

102 _plugin_doc_path: Traversable | Path | None = None 

103 

104 @property 

105 def is_bundled(self) -> bool: 

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

107 

108 @property 

109 def is_loaded(self) -> bool: 

110 return self.plugin_initializer is not None 

111 

112 @property 

113 def is_initialized(self) -> bool: 

114 return self._is_initialized 

115 

116 @property 

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

118 if not self._is_doc_path_resolved: 

119 self._plugin_doc_path = self.plugin_doc_path_resolver() 

120 self._is_doc_path_resolved = True 

121 return self._plugin_doc_path 

122 

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

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

125 raise RuntimeError("Cannot load plugins twice") 

126 if not self.is_loaded: 

127 self.load_plugin() 

128 plugin_initializer = self.plugin_initializer 

129 assert plugin_initializer is not None 

130 plugin_initializer(api) 

131 self._is_initialized = True 

132 

133 def load_plugin(self) -> None: 

134 plugin_loader = self.plugin_loader 

135 assert plugin_loader is not None 

136 try: 

137 self.plugin_initializer = run_in_context_of_plugin( 

138 self.plugin_name, 

139 plugin_loader, 

140 ) 

141 except PluginBaseError: 

142 raise 

143 except Exception as e: 

144 raise PluginInitializationError( 

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

146 ) from e 

147 assert self.plugin_initializer is not None 

148 

149 

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

151class PluginProvidedParser(Generic[PF, TP]): 

152 parser: "DeclarativeInputParser[PF]" 

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

154 plugin_metadata: DebputyPluginMetadata 

155 

156 def parse( 

157 self, 

158 name: str, 

159 value: object, 

160 attribute_path: "AttributePath", 

161 *, 

162 parser_context: "ParserContextData", 

163 ) -> TP: 

164 parsed_value = self.parser.parse_input( 

165 value, 

166 attribute_path, 

167 parser_context=parser_context, 

168 ) 

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

170 

171 

172class PPFFormatParam(TypedDict): 

173 priority: int | None 

174 name: str 

175 owning_package: str 

176 

177 

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

179class PackagerProvidedFileClassSpec: 

180 debputy_plugin_metadata: DebputyPluginMetadata 

181 stem: str 

182 installed_as_format: str 

183 default_mode: int 

184 default_priority: int | None 

185 allow_name_segment: bool 

186 allow_architecture_segment: bool 

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

188 packageless_is_fallback_for_all_packages: bool 

189 reservation_only: bool 

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

191 reference_documentation: PackagerProvidedFileReferenceDocumentation | None = None 

192 bug_950723: bool = False 

193 has_active_command: bool = True 

194 

195 @property 

196 def supports_priority(self) -> bool: 

197 return self.default_priority is not None 

198 

199 def compute_dest( 

200 self, 

201 assigned_name: str, 

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

203 *, 

204 owning_package: str | None = None, 

205 assigned_priority: int | None = None, 

206 path: VirtualPath | None = None, 

207 ) -> tuple[str, str]: 

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

209 raise ValueError( 

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

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

212 " do not use priority at all." 

213 ) 

214 

215 path_format = self.installed_as_format 

216 if self.supports_priority and assigned_priority is None: 

217 assigned_priority = self.default_priority 

218 

219 if owning_package is None: 

220 owning_package = assigned_name 

221 

222 params: PPFFormatParam = { 

223 "priority": assigned_priority, 

224 "name": assigned_name, 

225 "owning_package": owning_package, 

226 } 

227 

228 if self.formatting_callback is not None: 

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

230 raise ValueError( 

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

232 ) 

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

234 else: 

235 dest_path = path_format.format(**params) 

236 

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

238 dirname = _normalize_path(dirname) 

239 

240 if self.post_formatting_rewrite: 

241 basename = self.post_formatting_rewrite(basename) 

242 return dirname, basename 

243 

244 

245@dataclasses.dataclass(slots=True) 

246class MetadataOrMaintscriptDetector: 

247 plugin_metadata: DebputyPluginMetadata 

248 detector_id: str 

249 detector: MetadataAutoDetector 

250 applies_to_package_types: frozenset[str] 

251 enabled: bool = True 

252 

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

254 return binary_package.package_type in self.applies_to_package_types 

255 

256 def run_detector( 

257 self, 

258 fs_root: "VirtualPath", 

259 ctrl: "BinaryCtrlAccessor", 

260 context: "PackageProcessingContext", 

261 ) -> None: 

262 try: 

263 self.detector(fs_root, ctrl, context) 

264 except DebputyFSIsROError as e: 

265 nv = self.plugin_metadata.plugin_name 

266 raise PluginAPIViolationError( 

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

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

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

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

271 " would be lost)." 

272 ) from e 

273 except UnhandledOrUnexpectedErrorFromPluginError as e: 

274 e.add_note( 

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

276 ) 

277 

278 

279class DeclarativeInputParser(Generic[TD]): 

280 @property 

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

282 return None 

283 

284 @property 

285 def expected_debputy_integration_mode( 

286 self, 

287 ) -> Container[DebputyIntegrationMode] | None: 

288 return None 

289 

290 @property 

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

292 doc = self.inline_reference_documentation 

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

294 

295 def parse_input( 

296 self, 

297 value: object, 

298 path: "AttributePath", 

299 *, 

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

301 ) -> TD: 

302 raise NotImplementedError 

303 

304 

305class DelegatingDeclarativeInputParser(DeclarativeInputParser[TD]): 

306 __slots__ = ( 

307 "delegate", 

308 "_reference_documentation", 

309 "_expected_debputy_integration_mode", 

310 ) 

311 

312 def __init__( 

313 self, 

314 delegate: DeclarativeInputParser[TD], 

315 *, 

316 inline_reference_documentation: ParserDocumentation | None = None, 

317 expected_debputy_integration_mode: None | ( 

318 Container[DebputyIntegrationMode] 

319 ) = None, 

320 ) -> None: 

321 self.delegate = delegate 

322 self._reference_documentation = inline_reference_documentation 

323 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

324 

325 @property 

326 def expected_debputy_integration_mode( 

327 self, 

328 ) -> Container[DebputyIntegrationMode] | None: 

329 return self._expected_debputy_integration_mode 

330 

331 @property 

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

333 doc = self._reference_documentation 

334 if doc is None: 

335 return self.delegate.inline_reference_documentation 

336 return doc 

337 

338 

339class ListWrappedDeclarativeInputParser(DelegatingDeclarativeInputParser[TD]): 

340 __slots__ = () 

341 

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

343 doc_url = self.reference_documentation_url 

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

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

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

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

348 return "" 

349 

350 def parse_input( 

351 self, 

352 value: object, 

353 path: "AttributePath", 

354 *, 

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

356 ) -> TD: 

357 check_integration_mode( 

358 path, parser_context, self._expected_debputy_integration_mode 

359 ) 

360 if not isinstance(value, list): 

361 doc_ref = self._doc_url_error_suffix(see_url_version=True) 

362 raise ManifestParseException( 

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

364 ) 

365 result = [] 

366 delegate = self.delegate 

367 for idx, element in enumerate(value): 

368 element_path = path[idx] 

369 result.append( 

370 delegate.parse_input( 

371 element, 

372 element_path, 

373 parser_context=parser_context, 

374 ) 

375 ) 

376 return result 

377 

378 

379class DispatchingParserBase(Generic[TP]): 

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

381 self.manifest_attribute_path_template = manifest_attribute_path_template 

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

383 

384 @property 

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

386 return "error" 

387 

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

389 return keyword in self._parsers 

390 

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

392 yield from self._parsers 

393 

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

395 return self._parsers[keyword] 

396 

397 def register_keyword( 

398 self, 

399 keyword: str | Sequence[str], 

400 handler: DIPKWHandler, 

401 plugin_metadata: DebputyPluginMetadata, 

402 *, 

403 inline_reference_documentation: ParserDocumentation | None = None, 

404 ) -> None: 

405 reference_documentation_url = None 

406 if inline_reference_documentation: 

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

408 raise ValueError( 

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

410 ) 

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

412 raise ValueError( 

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

414 ) 

415 reference_documentation_url = ( 

416 inline_reference_documentation.documentation_reference_url 

417 ) 

418 parser = DeclarativeValuelessKeywordInputParser( 

419 inline_reference_documentation, 

420 documentation_reference=reference_documentation_url, 

421 ) 

422 

423 def _combined_handler( 

424 name: str, 

425 _ignored: Any, 

426 attr_path: AttributePath, 

427 context: "ParserContextData", 

428 ) -> TP: 

429 return handler(name, attr_path, context) 

430 

431 p = PluginProvidedParser( 

432 parser, 

433 _combined_handler, 

434 plugin_metadata, 

435 ) 

436 

437 self._add_parser(keyword, p) 

438 

439 def register_parser( 

440 self, 

441 keyword: str | list[str], 

442 parser: "DeclarativeInputParser[PF]", 

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

444 plugin_metadata: DebputyPluginMetadata, 

445 ) -> None: 

446 p = PluginProvidedParser( 

447 parser, 

448 handler, 

449 plugin_metadata, 

450 ) 

451 self._add_parser(keyword, p) 

452 

453 def _add_parser( 

454 self, 

455 keyword: str | Iterable[str], 

456 ppp: "PluginProvidedParser[PF, TP]", 

457 ) -> None: 

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

459 for k in ks: 

460 existing_parser = self._parsers.get(k) 

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

462 message = ( 

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

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

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

466 ) 

467 raise PluginConflictError( 

468 message, 

469 existing_parser.plugin_metadata, 

470 ppp.plugin_metadata, 

471 ) 

472 self._new_parser(k, ppp) 

473 

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

475 self._parsers[keyword] = ppp 

476 

477 def parse_input( 

478 self, 

479 orig_value: object, 

480 attribute_path: "AttributePath", 

481 *, 

482 parser_context: "ParserContextData", 

483 ) -> TP: 

484 raise NotImplementedError 

485 

486 

487class DispatchingObjectParser( 

488 DispatchingParserBase[Mapping[str, Any]], 

489 DeclarativeInputParser[Mapping[str, Any]], 

490): 

491 def __init__( 

492 self, 

493 manifest_attribute_path_template: str, 

494 *, 

495 parser_documentation: ParserDocumentation | None = None, 

496 expected_debputy_integration_mode: None | ( 

497 Container[DebputyIntegrationMode] 

498 ) = None, 

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

500 allow_unknown_keys: bool = False, 

501 ) -> None: 

502 super().__init__(manifest_attribute_path_template) 

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

504 if parser_documentation is None: 

505 parser_documentation = reference_documentation() 

506 self._parser_documentation = parser_documentation 

507 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

508 self._unknown_keys_diagnostic_severity = unknown_keys_diagnostic_severity 

509 self._allow_unknown_keys = allow_unknown_keys 

510 

511 @property 

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

513 return self._unknown_keys_diagnostic_severity 

514 

515 @property 

516 def expected_debputy_integration_mode( 

517 self, 

518 ) -> Container[DebputyIntegrationMode] | None: 

519 return self._expected_debputy_integration_mode 

520 

521 @property 

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

523 return self._parser_documentation.documentation_reference_url 

524 

525 @property 

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

527 ref_doc = self._parser_documentation 

528 return reference_documentation( 

529 title=ref_doc.title, 

530 description=ref_doc.description, 

531 attributes=self._attribute_documentation, 

532 reference_documentation_url=self.reference_documentation_url, 

533 ) 

534 

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

536 super()._new_parser(keyword, ppp) 

537 doc = ppp.parser.inline_reference_documentation 

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

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

540 else: 

541 self._attribute_documentation.append( 

542 documented_attr(keyword, doc.description) 

543 ) 

544 

545 def register_child_parser( 

546 self, 

547 keyword: str, 

548 parser: "DispatchingObjectParser", 

549 plugin_metadata: DebputyPluginMetadata, 

550 *, 

551 on_end_parse_step: None | ( 

552 Callable[ 

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

554 None, 

555 ] 

556 ) = None, 

557 nested_in_package_context: bool = False, 

558 ) -> None: 

559 def _handler( 

560 name: str, 

561 value: Mapping[str, Any], 

562 path: AttributePath, 

563 parser_context: "ParserContextData", 

564 ) -> Mapping[str, Any]: 

565 on_end_parse_step(name, value, path, parser_context) 

566 return value 

567 

568 if nested_in_package_context: 

569 parser = InPackageContextParser( 

570 keyword, 

571 parser, 

572 ) 

573 

574 p = PluginProvidedParser( 

575 parser, 

576 _handler, 

577 plugin_metadata, 

578 ) 

579 self._add_parser(keyword, p) 

580 

581 def parse_input( 

582 self, 

583 orig_value: object, 

584 attribute_path: "AttributePath", 

585 *, 

586 parser_context: "ParserContextData", 

587 ) -> TP: 

588 check_integration_mode( 

589 attribute_path, 

590 parser_context, 

591 self._expected_debputy_integration_mode, 

592 ) 

593 doc_ref = "" 

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

595 doc_ref = ( 

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

597 ) 

598 if not isinstance(orig_value, dict): 

599 raise ManifestParseException( 

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

601 ) 

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

603 raise ManifestParseException( 

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

605 ) 

606 result = {} 

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

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

609 first_key = next(iter(unknown_keys)) 

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

611 if not remaining_valid_attributes: 

612 raise ManifestParseException( 

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

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

615 ) 

616 remaining_valid_attribute_names = ", ".join(remaining_valid_attributes) 

617 raise ManifestParseException( 

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

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

620 f" {remaining_valid_attribute_names}.{doc_ref}" 

621 ) 

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

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

624 value = orig_value.get(key) 

625 if value is None: 

626 if isinstance(provided_parser.parser, DispatchingObjectParser): 

627 provided_parser.handler( 

628 key, 

629 {}, 

630 attribute_path[key], 

631 parser_context, 

632 ) 

633 continue 

634 value_path = attribute_path[key] 

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

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

637 raise ManifestParseException( 

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

639 " Valid options at this location are:" 

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

641 ) 

642 parsed_value = provided_parser.parse( 

643 key, value, value_path, parser_context=parser_context 

644 ) 

645 result[key] = parsed_value 

646 return result 

647 

648 

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

650class PackageContextData(Generic[TP]): 

651 resolved_package_name: str 

652 value: TP 

653 

654 

655class InPackageContextParser( 

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

657): 

658 __slots__ = () 

659 

660 def __init__( 

661 self, 

662 manifest_attribute_path_template: str, 

663 delegate: DeclarativeInputParser[TP], 

664 *, 

665 parser_documentation: ParserDocumentation | None = None, 

666 ) -> None: 

667 self.manifest_attribute_path_template = manifest_attribute_path_template 

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

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

670 

671 def parse_input( 

672 self, 

673 orig_value: object, 

674 attribute_path: "AttributePath", 

675 *, 

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

677 ) -> TP: 

678 assert parser_context is not None 

679 check_integration_mode( 

680 attribute_path, 

681 parser_context, 

682 self._expected_debputy_integration_mode, 

683 ) 

684 doc_ref = "" 

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

686 doc_ref = ( 

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

688 ) 

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

690 raise ManifestParseException( 

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

692 ) 

693 delegate = self.delegate 

694 result = {} 

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

696 

697 definition_source = attribute_path[package_name_raw] 

698 package_name = package_name_raw 

699 if "{{" in package_name: 

700 package_name = parser_context.substitution.substitute( 

701 package_name_raw, 

702 definition_source.path, 

703 ) 

704 package_state: PackageTransformationDefinition 

705 with parser_context.binary_package_context(package_name) as package_state: 

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

707 # Maybe lift (part) of this restriction. 

708 raise ManifestParseException( 

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

710 " auto-generated package." 

711 ) 

712 parsed_value = delegate.parse_input( 

713 value, definition_source, parser_context=parser_context 

714 ) 

715 result[package_name_raw] = PackageContextData( 

716 package_name, parsed_value 

717 ) 

718 return result 

719 

720 

721class DispatchingTableParser( 

722 DispatchingParserBase[TP], 

723 DeclarativeInputParser[TP], 

724): 

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

726 super().__init__(manifest_attribute_path_template) 

727 self.base_type = base_type 

728 

729 def parse_input( 

730 self, 

731 orig_value: object, 

732 attribute_path: "AttributePath", 

733 *, 

734 parser_context: "ParserContextData", 

735 ) -> TP: 

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

737 key = orig_value 

738 value = None 

739 value_path = attribute_path 

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

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

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

743 raise ManifestParseException( 

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

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

746 f" possible keys are: {valid_keys}" 

747 ) 

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

749 value_path = attribute_path[key] 

750 else: 

751 raise ManifestParseException( 

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

753 ) 

754 provided_parser = self._parsers.get(key) 

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

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

757 raise ManifestParseException( 

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

759 " Valid actions at this location are:" 

760 f" {valid_keys}" 

761 ) 

762 return provided_parser.parse( 

763 key, value, value_path, parser_context=parser_context 

764 ) 

765 

766 

767@dataclasses.dataclass(slots=True) 

768class DeclarativeValuelessKeywordInputParser(DeclarativeInputParser[None]): 

769 inline_reference_documentation: ParserDocumentation | None = None 

770 documentation_reference: str | None = None 

771 

772 def parse_input( 

773 self, 

774 value: object, 

775 path: "AttributePath", 

776 *, 

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

778 ) -> TD: 

779 if value is None: 

780 return cast("TD", value) 

781 if self.documentation_reference is not None: 

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

783 else: 

784 doc_ref = "" 

785 raise ManifestParseException( 

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

787 ) 

788 

789 

790@dataclasses.dataclass(slots=True) 

791class PluginProvidedManifestVariable: 

792 plugin_metadata: DebputyPluginMetadata 

793 variable_name: str 

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

795 is_context_specific_variable: bool 

796 variable_reference_documentation: str | None = None 

797 is_documentation_placeholder: bool = False 

798 is_for_special_case: bool = False 

799 

800 @property 

801 def is_internal(self) -> bool: 

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

803 

804 @property 

805 def is_token(self) -> bool: 

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

807 

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

809 value_resolver = self.variable_value 

810 if isinstance(value_resolver, str): 

811 res = value_resolver 

812 else: 

813 res = value_resolver(variable_context) 

814 return res 

815 

816 

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

818class AutomaticDiscardRuleExample: 

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

820 description: str | None = None 

821 

822 

823def automatic_discard_rule_example( 

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

825 example_description: str | None = None, 

826) -> AutomaticDiscardRuleExample: 

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

828 

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

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

831 part of a sequence of examples. 

832 

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

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

835 >>> # will be kept. 

836 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

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

839 ... ) 

840 AutomaticDiscardRuleExample(...) 

841 

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

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

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

845 

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

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

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

849 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

851 ... ".../__pycache__/", 

852 ... ".../__pycache__/...", 

853 ... ".../foo.pyc", 

854 ... ".../foo.pyo", 

855 ... ) 

856 AutomaticDiscardRuleExample(...) 

857 

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

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

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

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

862 

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

864 

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

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

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

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

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

870 

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

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

873 verdict is assumed to be discarded (True). 

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

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

876 """ 

877 example = [] 

878 for d in content: 

879 if not isinstance(d, tuple): 

880 pd = d 

881 verdict = True 

882 else: 

883 pd, verdict = d 

884 

885 path_def = as_path_def(pd) 

886 example.append((path_def, verdict)) 

887 

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

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

890 

891 return AutomaticDiscardRuleExample( 

892 tuple(example), 

893 description=example_description, 

894 ) 

895 

896 

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

898class PluginProvidedPackageProcessor: 

899 processor_id: str 

900 applies_to_package_types: frozenset[str] 

901 package_processor: PackageProcessor 

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

903 plugin_metadata: DebputyPluginMetadata 

904 

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

906 return binary_package.package_type in self.applies_to_package_types 

907 

908 @property 

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

910 return self.plugin_metadata.plugin_name, self.processor_id 

911 

912 def run_package_processor( 

913 self, 

914 fs_root: "VirtualPath", 

915 unused: None, 

916 context: "PackageProcessingContext", 

917 ) -> None: 

918 self.package_processor(fs_root, unused, context) 

919 

920 

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

922class PluginProvidedDiscardRule: 

923 name: str 

924 plugin_metadata: DebputyPluginMetadata 

925 discard_check: Callable[[VirtualPath], bool] 

926 reference_documentation: str | None 

927 examples: Sequence[AutomaticDiscardRuleExample] = tuple() 

928 

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

930 return self.discard_check(path) 

931 

932 

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

934class ServiceManagerDetails: 

935 service_manager: str 

936 service_detector: "ServiceDetector" 

937 service_integrator: "ServiceIntegrator" 

938 plugin_metadata: DebputyPluginMetadata 

939 

940 

941class ReferenceValue(TypedDict): 

942 description: str 

943 

944 

945def _reference_data_value( 

946 *, 

947 description: str, 

948) -> ReferenceValue: 

949 return { 

950 "description": description, 

951 } 

952 

953 

954KnownPackagingFileCategories = Literal[ 

955 "generated", 

956 "generic-template", 

957 "ppf-file", 

958 "ppf-control-file", 

959 "maint-config", 

960 "pkg-metadata", 

961 "pkg-helper-config", 

962 "testing", 

963 "lint-config", 

964] 

965KNOWN_PACKAGING_FILE_CATEGORY_DESCRIPTIONS: Mapping[ 

966 KnownPackagingFileCategories, ReferenceValue 

967] = { 

968 "generated": _reference_data_value( 

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

970 ), 

971 "generic-template": _reference_data_value( 

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

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

974 " language inside it." 

975 ), 

976 "ppf-file": _reference_data_value( 

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

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

979 ), 

980 "ppf-control-file": _reference_data_value( 

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

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

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

984 ), 

985 "maint-config": _reference_data_value( 

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

987 ), 

988 "pkg-metadata": _reference_data_value( 

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

990 ), 

991 "pkg-helper-config": _reference_data_value( 

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

993 ), 

994 "testing": _reference_data_value( 

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

996 ), 

997 "lint-config": _reference_data_value( 

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

999 ), 

1000} 

1001 

1002KnownPackagingConfigFeature = Literal[ 

1003 "dh-filearray", 

1004 "dh-filedoublearray", 

1005 "dh-hash-subst", 

1006 "dh-dollar-subst", 

1007 "dh-glob", 

1008 "dh-partial-glob", 

1009 "dh-late-glob", 

1010 "dh-glob-after-execute", 

1011 "dh-executable-config", 

1012 "dh-custom-format", 

1013 "dh-file-list", 

1014 "dh-install-list", 

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

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

1017 "dh-fixed-dest-dir", 

1018 "dh-exec-rename", 

1019 "dh-docs-only", 

1020] 

1021 

1022KNOWN_PACKAGING_FILE_CONFIG_FEATURE_DESCRIPTION: Mapping[ 

1023 KnownPackagingConfigFeature, ReferenceValue 

1024] = { 

1025 "dh-filearray": _reference_data_value( 

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

1027 ), 

1028 "dh-filedoublearray": _reference_data_value( 

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

1030 ), 

1031 "dh-hash-subst": _reference_data_value( 

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

1033 ), 

1034 "dh-dollar-subst": _reference_data_value( 

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

1036 ), 

1037 "dh-glob": _reference_data_value( 

1038 description="Supports standard debhelper globing", 

1039 ), 

1040 "dh-partial-glob": _reference_data_value( 

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

1042 ), 

1043 "dh-late-glob": _reference_data_value( 

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

1045 ), 

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

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

1048 ), 

1049 "dh-executable-config": _reference_data_value( 

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

1051 ), 

1052 "dh-custom-format": _reference_data_value( 

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

1054 ), 

1055 "dh-file-list": _reference_data_value( 

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

1057 ), 

1058 "dh-install-list": _reference_data_value( 

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

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

1061 ), 

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

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

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

1065 ), 

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

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

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

1069 ), 

1070 "dh-exec-rename": _reference_data_value( 

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

1072 " requested/used", 

1073 ), 

1074 "dh-docs-only": _reference_data_value( 

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

1076 ), 

1077} 

1078 

1079CONFIG_FEATURE_ALIASES: dict[ 

1080 KnownPackagingConfigFeature, list[tuple[KnownPackagingConfigFeature, int]] 

1081] = { 

1082 "dh-filearray": [ 

1083 ("dh-filearray", 0), 

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

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

1086 ], 

1087 "dh-filedoublearray": [ 

1088 ("dh-filedoublearray", 0), 

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

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

1091 ], 

1092} 

1093 

1094 

1095def _implies( 

1096 features: list[KnownPackagingConfigFeature], 

1097 seen: set[KnownPackagingConfigFeature], 

1098 implying: Sequence[KnownPackagingConfigFeature], 

1099 implied: KnownPackagingConfigFeature, 

1100) -> None: 

1101 if implied in seen: 

1102 return 

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

1104 seen.add(implied) 

1105 features.append(implied) 

1106 

1107 

1108def expand_known_packaging_config_features( 

1109 compat_level: int, 

1110 features: list[KnownPackagingConfigFeature], 

1111) -> list[KnownPackagingConfigFeature]: 

1112 final_features: list[KnownPackagingConfigFeature] = [] 

1113 seen = set() 

1114 for feature in features: 

1115 expanded = CONFIG_FEATURE_ALIASES.get(feature) 

1116 if not expanded: 

1117 expanded = [(feature, 0)] 

1118 for v, c in expanded: 

1119 if compat_level < c or v in seen: 

1120 continue 

1121 seen.add(v) 

1122 final_features.append(v) 

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

1124 final_features.remove("dh-glob") 

1125 

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

1127 _implies( 

1128 final_features, 

1129 seen, 

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

1131 "dh-glob-after-execute", 

1132 ) 

1133 return sorted(final_features) 

1134 

1135 

1136class DHCompatibilityBasedRule(DebputyParsedContent): 

1137 install_pattern: NotRequired[str] 

1138 add_config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1139 starting_with_compat_level: NotRequired[int] 

1140 

1141 

1142class KnownPackagingFileInfo(DebputyParsedContent): 

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

1144 path: NotRequired[str] 

1145 pkgfile: NotRequired[str] 

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

1147 file_categories: NotRequired[list[KnownPackagingFileCategories]] 

1148 documentation_uris: NotRequired[list[str]] 

1149 debputy_cmd_templates: NotRequired[list[list[str]]] 

1150 debhelper_commands: NotRequired[list[str]] 

1151 config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1152 install_pattern: NotRequired[str] 

1153 dh_compat_rules: NotRequired[list[DHCompatibilityBasedRule]] 

1154 default_priority: NotRequired[int] 

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

1156 packageless_is_fallback_for_all_packages: NotRequired[bool] 

1157 has_active_command: NotRequired[bool] 

1158 

1159 

1160@dataclasses.dataclass(slots=True) 

1161class PluginProvidedKnownPackagingFile: 

1162 info: KnownPackagingFileInfo 

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

1164 detection_value: str 

1165 plugin_metadata: DebputyPluginMetadata 

1166 

1167 

1168class BuildSystemAutoDetector(Protocol): 

1169 

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

1171 

1172 

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

1174class PluginProvidedTypeMapping: 

1175 mapped_type: TypeMapping[Any, Any] 

1176 reference_documentation: TypeMappingDocumentation | None 

1177 plugin_metadata: DebputyPluginMetadata 

1178 

1179 

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

1181class PluginProvidedBuildSystemAutoDetection(Generic[BSR]): 

1182 manifest_keyword: str 

1183 build_system_rule_type: type[BSR] 

1184 detector: BuildSystemAutoDetector 

1185 constructor: Callable[ 

1186 ["BuildRuleParsedFormat", AttributePath, "HighLevelManifest"], 

1187 BSR, 

1188 ] 

1189 auto_detection_shadow_build_systems: frozenset[str] 

1190 plugin_metadata: DebputyPluginMetadata 

1191 

1192 

1193class PackageDataTable: 

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

1195 self._package_data_table = package_data_table 

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

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

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

1199 self.enable_cross_package_checks = False 

1200 

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

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

1203 

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

1205 return self._package_data_table[item] 

1206 

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

1208 return item in self._package_data_table 

1209 

1210 

1211class PackageProcessingContextProvider(PackageProcessingContext): 

1212 __slots__ = ( 

1213 "_manifest", 

1214 "_binary_package", 

1215 "_related_udeb_package", 

1216 "_package_data_table", 

1217 "_cross_check_cache", 

1218 ) 

1219 

1220 def __init__( 

1221 self, 

1222 manifest: "HighLevelManifest", 

1223 binary_package: BinaryPackage, 

1224 related_udeb_package: BinaryPackage | None, 

1225 package_data_table: PackageDataTable, 

1226 ) -> None: 

1227 self._manifest = manifest 

1228 self._binary_package = binary_package 

1229 self._related_udeb_package = related_udeb_package 

1230 self._package_data_table = ref(package_data_table) 

1231 self._cross_check_cache: None | ( 

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

1233 ) = None 

1234 

1235 def _package_state_for( 

1236 self, 

1237 package: BinaryPackage, 

1238 ) -> "PackageTransformationDefinition": 

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

1240 

1241 def _package_version_for( 

1242 self, 

1243 package: BinaryPackage, 

1244 ) -> str: 

1245 package_state = self._package_state_for(package) 

1246 version = package_state.binary_version 

1247 if version is not None: 

1248 return version 

1249 return self._manifest.source_version( 

1250 include_binnmu_version=not package.is_arch_all 

1251 ) 

1252 

1253 @property 

1254 def source_package(self) -> SourcePackage: 

1255 return self._manifest.source_package 

1256 

1257 @property 

1258 def binary_package(self) -> BinaryPackage: 

1259 return self._binary_package 

1260 

1261 @property 

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

1263 return self._related_udeb_package 

1264 

1265 @property 

1266 def binary_package_version(self) -> str: 

1267 return self._package_version_for(self._binary_package) 

1268 

1269 @property 

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

1271 udeb = self._related_udeb_package 

1272 if udeb is None: 

1273 return None 

1274 return self._package_version_for(udeb) 

1275 

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

1277 package_table = self._package_data_table() 

1278 if package_table is None: 

1279 raise ReferenceError( 

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

1281 ) 

1282 if not package_table.enable_cross_package_checks: 

1283 raise PluginAPIViolationError( 

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

1285 ) 

1286 cache = self._cross_check_cache 

1287 if cache is None: 

1288 matches = [] 

1289 pkg = self.binary_package 

1290 for pkg_data in package_table: 

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

1292 continue 

1293 res = package_cross_check_precheck(pkg, pkg_data.binary_package) 

1294 if not res[0]: 

1295 continue 

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

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

1298 self._cross_check_cache = cache 

1299 return cache 

1300 

1301 def manifest_configuration[T]( 

1302 self, 

1303 context_package: SourcePackage | BinaryPackage, 

1304 value_type: type[T], 

1305 ) -> T | None: 

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

1307 

1308 @property 

1309 def dpkg_arch_query_table(self) -> DpkgArchTable: 

1310 return self._manifest.dpkg_arch_query_table 

1311 

1312 @property 

1313 def deb_options_and_profiles(self) -> DebBuildOptionsAndProfiles: 

1314 return self._manifest.deb_options_and_profiles 

1315 

1316 @property 

1317 def source_condition_context(self) -> ConditionContext: 

1318 return self._manifest.source_condition_context 

1319 

1320 def condition_context( 

1321 self, binary_package: BinaryPackage | None 

1322 ) -> ConditionContext: 

1323 return self._manifest.condition_context(binary_package) 

1324 

1325 

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

1327class PluginProvidedTrigger: 

1328 dpkg_trigger_type: DpkgTriggerType 

1329 dpkg_trigger_target: str 

1330 provider: DebputyPluginMetadata 

1331 provider_source_id: str 

1332 

1333 def serialized_format(self) -> str: 

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