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

526 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import dataclasses 

2import os.path 

3import textwrap 

4from typing import ( 

5 Optional, 

6 Callable, 

7 FrozenSet, 

8 Dict, 

9 List, 

10 Tuple, 

11 Generic, 

12 TYPE_CHECKING, 

13 TypeVar, 

14 cast, 

15 Any, 

16 Sequence, 

17 Union, 

18 Type, 

19 TypedDict, 

20 Iterable, 

21 Mapping, 

22 NotRequired, 

23 Literal, 

24 Set, 

25 Iterator, 

26) 

27from weakref import ref 

28 

29from debputy import DEBPUTY_DOC_ROOT_DIR 

30from debputy.exceptions import ( 

31 DebputyFSIsROError, 

32 PluginAPIViolationError, 

33 PluginConflictError, 

34 UnhandledOrUnexpectedErrorFromPluginError, 

35) 

36from debputy.filesystem_scan import as_path_def 

37from debputy.installations import InstallRule 

38from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand 

39from debputy.manifest_conditions import ManifestCondition 

40from debputy.manifest_parser.base_types import DebputyParsedContent, TypeMapping 

41from debputy.manifest_parser.exceptions import ManifestParseException 

42from debputy.manifest_parser.util import AttributePath 

43from debputy.packages import BinaryPackage 

44from debputy.plugin.api import ( 

45 VirtualPath, 

46 BinaryCtrlAccessor, 

47 PackageProcessingContext, 

48) 

49from debputy.plugin.api.spec import ( 

50 DebputyPluginInitializer, 

51 MetadataAutoDetector, 

52 DpkgTriggerType, 

53 ParserDocumentation, 

54 PackageProcessor, 

55 PathDef, 

56 ParserAttributeDocumentation, 

57 undocumented_attr, 

58 documented_attr, 

59 reference_documentation, 

60 PackagerProvidedFileReferenceDocumentation, 

61 TypeMappingDocumentation, 

62) 

63from debputy.substitution import VariableContext 

64from debputy.transformation_rules import TransformationRule 

65from debputy.util import _normalize_path, package_cross_check_precheck 

66 

67if TYPE_CHECKING: 

68 from debputy.plugin.api.spec import ( 

69 ServiceDetector, 

70 ServiceIntegrator, 

71 PackageTypeSelector, 

72 ) 

73 from debputy.manifest_parser.parser_data import ParserContextData 

74 from debputy.highlevel_manifest import ( 

75 HighLevelManifest, 

76 PackageTransformationDefinition, 

77 BinaryPackageData, 

78 ) 

79 

80 

81_PACKAGE_TYPE_DEB_ONLY = frozenset(["deb"]) 

82_ALL_PACKAGE_TYPES = frozenset(["deb", "udeb"]) 

83 

84 

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

86PF = TypeVar("PF") 

87SF = TypeVar("SF") 

88TP = TypeVar("TP") 

89TTP = Type[TP] 

90 

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

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

93 

94 

95def resolve_package_type_selectors( 

96 package_type: "PackageTypeSelector", 

97) -> FrozenSet[str]: 

98 if package_type is _ALL_PACKAGE_TYPES or package_type is _PACKAGE_TYPE_DEB_ONLY: 

99 return cast("FrozenSet[str]", package_type) 

100 if isinstance(package_type, str): 

101 return ( 

102 _PACKAGE_TYPE_DEB_ONLY 

103 if package_type == "deb" 

104 else frozenset([package_type]) 

105 ) 

106 else: 

107 return frozenset(package_type) 

108 

109 

110@dataclasses.dataclass(slots=True) 

111class DebputyPluginMetadata: 

112 plugin_name: str 

113 api_compat_version: int 

114 plugin_loader: Optional[Callable[[], Callable[["DebputyPluginInitializer"], None]]] 

115 plugin_initializer: Optional[Callable[["DebputyPluginInitializer"], None]] 

116 plugin_path: str 

117 _is_initialized: bool = False 

118 

119 @property 

120 def is_loaded(self) -> bool: 

121 return self.plugin_initializer is not None 

122 

123 @property 

124 def is_initialized(self) -> bool: 

125 return self._is_initialized 

126 

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

128 if self.is_initialized: 128 ↛ 129line 128 didn't jump to line 129, because the condition on line 128 was never true

129 raise RuntimeError("Cannot load plugins twice") 

130 if not self.is_loaded: 

131 self.load_plugin() 

132 plugin_initializer = self.plugin_initializer 

133 assert plugin_initializer is not None 

134 plugin_initializer(api) 

135 self._is_initialized = True 

136 

137 def load_plugin(self) -> None: 

138 plugin_loader = self.plugin_loader 

139 assert plugin_loader is not None 

140 self.plugin_initializer = plugin_loader() 

141 assert self.plugin_initializer is not None 

142 

143 

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

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

146 parser: "DeclarativeInputParser[PF]" 

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

148 plugin_metadata: DebputyPluginMetadata 

149 

150 def parse( 

151 self, 

152 name: str, 

153 value: object, 

154 attribute_path: "AttributePath", 

155 *, 

156 parser_context: "ParserContextData", 

157 ) -> TP: 

158 parsed_value = self.parser.parse_input( 

159 value, attribute_path, parser_context=parser_context 

160 ) 

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

162 

163 

164class PPFFormatParam(TypedDict): 

165 priority: Optional[int] 

166 name: str 

167 owning_package: str 

168 

169 

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

171class PackagerProvidedFileClassSpec: 

172 debputy_plugin_metadata: DebputyPluginMetadata 

173 stem: str 

174 installed_as_format: str 

175 default_mode: int 

176 default_priority: Optional[int] 

177 allow_name_segment: bool 

178 allow_architecture_segment: bool 

179 post_formatting_rewrite: Optional[Callable[[str], str]] 

180 packageless_is_fallback_for_all_packages: bool 

181 reservation_only: bool 

182 formatting_callback: Optional[Callable[[str, PPFFormatParam, VirtualPath], str]] = ( 

183 None 

184 ) 

185 reference_documentation: Optional[PackagerProvidedFileReferenceDocumentation] = None 

186 bug_950723: bool = False 

187 

188 @property 

189 def supports_priority(self) -> bool: 

190 return self.default_priority is not None 

191 

192 def compute_dest( 

193 self, 

194 assigned_name: str, 

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

196 *, 

197 owning_package: Optional[str] = None, 

198 assigned_priority: Optional[int] = None, 

199 path: Optional[VirtualPath] = None, 

200 ) -> Tuple[str, str]: 

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

202 raise ValueError( 

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

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

205 " do not use priority at all." 

206 ) 

207 

208 path_format = self.installed_as_format 

209 if self.supports_priority and assigned_priority is None: 

210 assigned_priority = self.default_priority 

211 

212 if owning_package is None: 

213 owning_package = assigned_name 

214 

215 params: PPFFormatParam = { 

216 "priority": assigned_priority, 

217 "name": assigned_name, 

218 "owning_package": owning_package, 

219 } 

220 

221 if self.formatting_callback is not None: 

222 if path is None: 222 ↛ 223line 222 didn't jump to line 223, because the condition on line 222 was never true

223 raise ValueError( 

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

225 ) 

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

227 else: 

228 dest_path = path_format.format(**params) 

229 

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

231 dirname = _normalize_path(dirname) 

232 

233 if self.post_formatting_rewrite: 

234 basename = self.post_formatting_rewrite(basename) 

235 return dirname, basename 

236 

237 

238@dataclasses.dataclass(slots=True) 

239class MetadataOrMaintscriptDetector: 

240 plugin_metadata: DebputyPluginMetadata 

241 detector_id: str 

242 detector: MetadataAutoDetector 

243 applies_to_package_types: FrozenSet[str] 

244 enabled: bool = True 

245 

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

247 return binary_package.package_type in self.applies_to_package_types 

248 

249 def run_detector( 

250 self, 

251 fs_root: "VirtualPath", 

252 ctrl: "BinaryCtrlAccessor", 

253 context: "PackageProcessingContext", 

254 ) -> None: 

255 try: 

256 self.detector(fs_root, ctrl, context) 

257 except DebputyFSIsROError as e: 257 ↛ 266line 257 didn't jump to line 266

258 nv = self.plugin_metadata.plugin_name 

259 raise PluginAPIViolationError( 

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

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

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

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

264 " would be lost)." 

265 ) from e 

266 except (ChildProcessError, RuntimeError, AttributeError) as e: 

267 nv = f"{self.plugin_metadata.plugin_name}" 

268 raise UnhandledOrUnexpectedErrorFromPluginError( 

269 f"The plugin {nv} threw an unhandled or unexpected exception from its metadata" 

270 f" detector with id {self.detector_id}." 

271 ) from e 

272 

273 

274class DeclarativeInputParser(Generic[TD]): 

275 @property 

276 def inline_reference_documentation(self) -> Optional[ParserDocumentation]: 

277 return None 

278 

279 @property 

280 def reference_documentation_url(self) -> Optional[str]: 

281 doc = self.inline_reference_documentation 

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

283 

284 def parse_input( 

285 self, 

286 value: object, 

287 path: "AttributePath", 

288 *, 

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

290 ) -> TD: 

291 raise NotImplementedError 

292 

293 

294class DelegatingDeclarativeInputParser(DeclarativeInputParser[TD]): 

295 __slots__ = ("delegate", "_reference_documentation") 

296 

297 def __init__( 

298 self, 

299 delegate: DeclarativeInputParser[TD], 

300 *, 

301 inline_reference_documentation: Optional[ParserDocumentation] = None, 

302 ) -> None: 

303 self.delegate = delegate 

304 self._reference_documentation = inline_reference_documentation 

305 

306 @property 

307 def inline_reference_documentation(self) -> Optional[ParserDocumentation]: 

308 doc = self._reference_documentation 

309 if doc is None: 

310 return self.delegate.inline_reference_documentation 

311 return doc 

312 

313 

314class ListWrappedDeclarativeInputParser(DelegatingDeclarativeInputParser[TD]): 

315 __slots__ = () 

316 

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

318 doc_url = self.reference_documentation_url 

319 if doc_url is not None: 319 ↛ 323line 319 didn't jump to line 323, because the condition on line 319 was never false

320 if see_url_version: 320 ↛ 322line 320 didn't jump to line 322, because the condition on line 320 was never false

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

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

323 return "" 

324 

325 def parse_input( 

326 self, 

327 value: object, 

328 path: "AttributePath", 

329 *, 

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

331 ) -> TD: 

332 if not isinstance(value, list): 

333 doc_ref = self._doc_url_error_suffix(see_url_version=True) 

334 raise ManifestParseException( 

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

336 ) 

337 result = [] 

338 delegate = self.delegate 

339 for idx, element in enumerate(value): 

340 element_path = path[idx] 

341 result.append( 

342 delegate.parse_input( 

343 element, 

344 element_path, 

345 parser_context=parser_context, 

346 ) 

347 ) 

348 return result 

349 

350 

351class DispatchingParserBase(Generic[TP]): 

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

353 self.manifest_attribute_path_template = manifest_attribute_path_template 

354 self._parsers: Dict[str, PluginProvidedParser[Any, TP]] = {} 

355 

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

357 return keyword in self._parsers 

358 

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

360 yield from self._parsers 

361 

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

363 return self._parsers[keyword] 

364 

365 def register_keyword( 

366 self, 

367 keyword: Union[str, Sequence[str]], 

368 handler: DIPKWHandler, 

369 plugin_metadata: DebputyPluginMetadata, 

370 *, 

371 inline_reference_documentation: Optional[ParserDocumentation] = None, 

372 ) -> None: 

373 reference_documentation_url = None 

374 if inline_reference_documentation: 374 ↛ 386line 374 didn't jump to line 386, because the condition on line 374 was never false

375 if inline_reference_documentation.attribute_doc: 375 ↛ 376line 375 didn't jump to line 376, because the condition on line 375 was never true

376 raise ValueError( 

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

378 ) 

379 if inline_reference_documentation.alt_parser_description: 379 ↛ 380line 379 didn't jump to line 380, because the condition on line 379 was never true

380 raise ValueError( 

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

382 ) 

383 reference_documentation_url = ( 

384 inline_reference_documentation.documentation_reference_url 

385 ) 

386 parser = DeclarativeValuelessKeywordInputParser( 

387 inline_reference_documentation, 

388 documentation_reference=reference_documentation_url, 

389 ) 

390 

391 def _combined_handler( 

392 name: str, 

393 _ignored: Any, 

394 attr_path: AttributePath, 

395 context: "ParserContextData", 

396 ) -> TP: 

397 return handler(name, attr_path, context) 

398 

399 p = PluginProvidedParser( 

400 parser, 

401 _combined_handler, 

402 plugin_metadata, 

403 ) 

404 

405 self._add_parser(keyword, p) 

406 

407 def register_parser( 

408 self, 

409 keyword: Union[str, List[str]], 

410 parser: "DeclarativeInputParser[PF]", 

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

412 plugin_metadata: DebputyPluginMetadata, 

413 ) -> None: 

414 p = PluginProvidedParser( 

415 parser, 

416 handler, 

417 plugin_metadata, 

418 ) 

419 self._add_parser(keyword, p) 

420 

421 def _add_parser( 

422 self, 

423 keyword: Union[str, List[str]], 

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

425 ) -> None: 

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

427 for k in ks: 

428 existing_parser = self._parsers.get(k) 

429 if existing_parser is not None: 429 ↛ 430line 429 didn't jump to line 430

430 message = ( 

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

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

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

434 ) 

435 raise PluginConflictError( 

436 message, 

437 existing_parser.plugin_metadata, 

438 ppp.plugin_metadata, 

439 ) 

440 self._new_parser(k, ppp) 

441 

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

443 self._parsers[keyword] = ppp 

444 

445 def parse_input( 

446 self, 

447 orig_value: object, 

448 attribute_path: "AttributePath", 

449 *, 

450 parser_context: "ParserContextData", 

451 ) -> TP: 

452 raise NotImplementedError 

453 

454 

455class DispatchingObjectParser( 

456 DispatchingParserBase[Mapping[str, Any]], 

457 DeclarativeInputParser[Mapping[str, Any]], 

458): 

459 def __init__( 

460 self, 

461 manifest_attribute_path_template: str, 

462 *, 

463 parser_documentation: Optional[ParserDocumentation] = None, 

464 ) -> None: 

465 super().__init__(manifest_attribute_path_template) 

466 self._attribute_documentation: List[ParserAttributeDocumentation] = [] 

467 if parser_documentation is None: 467 ↛ 468line 467 didn't jump to line 468, because the condition on line 467 was never true

468 parser_documentation = reference_documentation() 

469 self._parser_documentation = parser_documentation 

470 

471 @property 

472 def reference_documentation_url(self) -> Optional[str]: 

473 return self._parser_documentation.documentation_reference_url 

474 

475 @property 

476 def inline_reference_documentation(self) -> Optional[ParserDocumentation]: 

477 ref_doc = self._parser_documentation 

478 return reference_documentation( 

479 title=ref_doc.title, 

480 description=ref_doc.description, 

481 attributes=self._attribute_documentation, 

482 reference_documentation_url=self.reference_documentation_url, 

483 ) 

484 

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

486 super()._new_parser(keyword, ppp) 

487 doc = ppp.parser.inline_reference_documentation 

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

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

490 else: 

491 self._attribute_documentation.append( 

492 documented_attr(keyword, doc.description) 

493 ) 

494 

495 def register_child_parser( 

496 self, 

497 keyword: str, 

498 parser: "DispatchingObjectParser", 

499 plugin_metadata: DebputyPluginMetadata, 

500 *, 

501 on_end_parse_step: Optional[ 

502 Callable[ 

503 [str, Optional[Mapping[str, Any]], AttributePath, "ParserContextData"], 

504 None, 

505 ] 

506 ] = None, 

507 nested_in_package_context: bool = False, 

508 ) -> None: 

509 def _handler( 

510 name: str, 

511 value: Mapping[str, Any], 

512 path: AttributePath, 

513 parser_context: "ParserContextData", 

514 ) -> Mapping[str, Any]: 

515 on_end_parse_step(name, value, path, parser_context) 

516 return value 

517 

518 if nested_in_package_context: 

519 parser = InPackageContextParser( 

520 keyword, 

521 parser, 

522 ) 

523 

524 p = PluginProvidedParser( 

525 parser, 

526 _handler, 

527 plugin_metadata, 

528 ) 

529 self._add_parser(keyword, p) 

530 

531 def parse_input( 

532 self, 

533 orig_value: object, 

534 attribute_path: "AttributePath", 

535 *, 

536 parser_context: "ParserContextData", 

537 ) -> TP: 

538 doc_ref = "" 

539 if self.reference_documentation_url is not None: 539 ↛ 543line 539 didn't jump to line 543, because the condition on line 539 was never false

540 doc_ref = ( 

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

542 ) 

543 if not isinstance(orig_value, dict): 

544 raise ManifestParseException( 

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

546 ) 

547 if not orig_value: 547 ↛ 548line 547 didn't jump to line 548, because the condition on line 547 was never true

548 raise ManifestParseException( 

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

550 ) 

551 result = {} 

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

553 if unknown_keys: 553 ↛ 554line 553 didn't jump to line 554, because the condition on line 553 was never true

554 first_key = next(iter(unknown_keys)) 

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

556 if not remaining_valid_attributes: 

557 raise ManifestParseException( 

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

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

560 ) 

561 remaining_valid_attribute_names = ", ".join(remaining_valid_attributes) 

562 raise ManifestParseException( 

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

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

565 f" {remaining_valid_attribute_names}.{doc_ref}" 

566 ) 

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

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

569 value = orig_value.get(key) 

570 if value is None: 

571 if isinstance(provided_parser.parser, DispatchingObjectParser): 

572 provided_parser.handler( 

573 key, {}, attribute_path[key], parser_context 

574 ) 

575 continue 

576 value_path = attribute_path[key] 

577 if provided_parser is None: 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true

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

579 raise ManifestParseException( 

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

581 " Valid options at this location are:" 

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

583 ) 

584 parsed_value = provided_parser.parse( 

585 key, value, value_path, parser_context=parser_context 

586 ) 

587 result[key] = parsed_value 

588 return result 

589 

590 

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

592class PackageContextData(Generic[TP]): 

593 resolved_package_name: str 

594 value: TP 

595 

596 

597class InPackageContextParser( 

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

599): 

600 def __init__( 

601 self, 

602 manifest_attribute_path_template: str, 

603 delegate: DeclarativeInputParser[TP], 

604 *, 

605 parser_documentation: Optional[ParserDocumentation] = None, 

606 ) -> None: 

607 self.manifest_attribute_path_template = manifest_attribute_path_template 

608 self._attribute_documentation: List[ParserAttributeDocumentation] = [] 

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

610 

611 def parse_input( 

612 self, 

613 orig_value: object, 

614 attribute_path: "AttributePath", 

615 *, 

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

617 ) -> TP: 

618 assert parser_context is not None 

619 doc_ref = "" 

620 if self.reference_documentation_url is not None: 620 ↛ 624line 620 didn't jump to line 624, because the condition on line 620 was never false

621 doc_ref = ( 

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

623 ) 

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

625 raise ManifestParseException( 

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

627 ) 

628 delegate = self.delegate 

629 result = {} 

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

631 

632 definition_source = attribute_path[package_name_raw] 

633 package_name = package_name_raw 

634 if "{{" in package_name: 

635 package_name = parser_context.substitution.substitute( 

636 package_name_raw, 

637 definition_source.path, 

638 ) 

639 package_state: PackageTransformationDefinition 

640 with parser_context.binary_package_context(package_name) as package_state: 

641 if package_state.is_auto_generated_package: 641 ↛ 643line 641 didn't jump to line 643, because the condition on line 641 was never true

642 # Maybe lift (part) of this restriction. 

643 raise ManifestParseException( 

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

645 " auto-generated package." 

646 ) 

647 parsed_value = delegate.parse_input( 

648 value, definition_source, parser_context=parser_context 

649 ) 

650 result[package_name_raw] = PackageContextData( 

651 package_name, parsed_value 

652 ) 

653 return result 

654 

655 

656class DispatchingTableParser( 

657 DispatchingParserBase[TP], 

658 DeclarativeInputParser[TP], 

659): 

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

661 super().__init__(manifest_attribute_path_template) 

662 self.base_type = base_type 

663 

664 def parse_input( 

665 self, 

666 orig_value: object, 

667 attribute_path: "AttributePath", 

668 *, 

669 parser_context: "ParserContextData", 

670 ) -> TP: 

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

672 key = orig_value 

673 value = None 

674 value_path = attribute_path 

675 elif isinstance(orig_value, dict): 675 ↛ 686line 675 didn't jump to line 686, because the condition on line 675 was never false

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

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

678 raise ManifestParseException( 

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

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

681 f" possible keys are: {valid_keys}" 

682 ) 

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

684 value_path = attribute_path[key] 

685 else: 

686 raise ManifestParseException( 

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

688 ) 

689 provided_parser = self._parsers.get(key) 

690 if provided_parser is None: 690 ↛ 691line 690 didn't jump to line 691, because the condition on line 690 was never true

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

692 raise ManifestParseException( 

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

694 " Valid actions at this location are:" 

695 f" {valid_keys}" 

696 ) 

697 return provided_parser.parse( 

698 key, value, value_path, parser_context=parser_context 

699 ) 

700 

701 

702@dataclasses.dataclass(slots=True) 

703class DeclarativeValuelessKeywordInputParser(DeclarativeInputParser[None]): 

704 inline_reference_documentation: Optional[ParserDocumentation] = None 

705 documentation_reference: Optional[str] = None 

706 

707 def parse_input( 

708 self, 

709 value: object, 

710 path: "AttributePath", 

711 *, 

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

713 ) -> TD: 

714 if value is None: 

715 return cast("TD", value) 

716 if self.documentation_reference is not None: 

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

718 else: 

719 doc_ref = "" 

720 raise ManifestParseException( 

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

722 ) 

723 

724 

725SUPPORTED_DISPATCHABLE_TABLE_PARSERS = { 

726 InstallRule: "installations", 

727 TransformationRule: "packages.{{PACKAGE}}.transformations", 

728 DpkgMaintscriptHelperCommand: "packages.{{PACKAGE}}.conffile-management", 

729 ManifestCondition: "*.when", 

730} 

731 

732OPARSER_MANIFEST_ROOT = "<ROOT>" 

733OPARSER_PACKAGES_ROOT = "packages" 

734OPARSER_PACKAGES = "packages.{{PACKAGE}}" 

735OPARSER_MANIFEST_DEFINITIONS = "definitions" 

736 

737SUPPORTED_DISPATCHABLE_OBJECT_PARSERS = { 

738 OPARSER_MANIFEST_ROOT: reference_documentation( 

739 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md", 

740 ), 

741 OPARSER_MANIFEST_DEFINITIONS: reference_documentation( 

742 title="Packager provided definitions", 

743 description="Reusable packager provided definitions such as manifest variables.", 

744 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#packager-provided-definitions", 

745 ), 

746 OPARSER_PACKAGES: reference_documentation( 

747 title="Binary package rules", 

748 description=textwrap.dedent( 

749 """\ 

750 Inside the manifest, the `packages` mapping can be used to define requests for the binary packages 

751 you want `debputy` to produce. Each key inside `packages` must be the name of a binary package 

752 defined in `debian/control`. The value is a dictionary defining which features that `debputy` 

753 should apply to that binary package. An example could be: 

754 

755 packages: 

756 foo: 

757 transformations: 

758 - create-symlink: 

759 path: usr/share/foo/my-first-symlink 

760 target: /usr/share/bar/symlink-target 

761 - create-symlink: 

762 path: usr/lib/{{DEB_HOST_MULTIARCH}}/my-second-symlink 

763 target: /usr/lib/{{DEB_HOST_MULTIARCH}}/baz/symlink-target 

764 bar: 

765 transformations: 

766 - create-directories: 

767 - some/empty/directory.d 

768 - another/empty/integration-point.d 

769 - create-directories: 

770 path: a/third-empty/directory.d 

771 owner: www-data 

772 group: www-data 

773 

774 In this case, `debputy` will create some symlinks inside the `foo` package and some directories for 

775 the `bar` package. The following subsections define the keys you can use under each binary package. 

776 """ 

777 ), 

778 reference_documentation_url=f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#binary-package-rules", 

779 ), 

780} 

781 

782 

783@dataclasses.dataclass(slots=True) 

784class PluginProvidedManifestVariable: 

785 plugin_metadata: DebputyPluginMetadata 

786 variable_name: str 

787 variable_value: Optional[Union[str, Callable[[VariableContext], str]]] 

788 is_context_specific_variable: bool 

789 variable_reference_documentation: Optional[str] = None 

790 is_documentation_placeholder: bool = False 

791 is_for_special_case: bool = False 

792 

793 @property 

794 def is_internal(self) -> bool: 

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

796 

797 @property 

798 def is_token(self) -> bool: 

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

800 

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

802 value_resolver = self.variable_value 

803 if isinstance(value_resolver, str): 

804 res = value_resolver 

805 else: 

806 res = value_resolver(variable_context) 

807 return res 

808 

809 

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

811class AutomaticDiscardRuleExample: 

812 content: Sequence[Tuple[PathDef, bool]] 

813 description: Optional[str] = None 

814 

815 

816def automatic_discard_rule_example( 

817 *content: Union[str, PathDef, Tuple[Union[str, PathDef], bool]], 

818 example_description: Optional[str] = None, 

819) -> AutomaticDiscardRuleExample: 

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

821 

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

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

824 part of a sequence of examples. 

825 

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

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

828 >>> # will be kept. 

829 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

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

832 ... ) 

833 AutomaticDiscardRuleExample(...) 

834 

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

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

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

838 

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

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

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

842 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

844 ... ".../__pycache__/", 

845 ... ".../__pycache__/...", 

846 ... ".../foo.pyc", 

847 ... ".../foo.pyo", 

848 ... ) 

849 AutomaticDiscardRuleExample(...) 

850 

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

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

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

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

855 

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

857 

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

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

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

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

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

863 

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

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

866 verdict is assumed to be discarded (True). 

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

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

869 """ 

870 example = [] 

871 for d in content: 

872 if not isinstance(d, tuple): 

873 pd = d 

874 verdict = True 

875 else: 

876 pd, verdict = d 

877 

878 path_def = as_path_def(pd) 

879 example.append((path_def, verdict)) 

880 

881 if not example: 881 ↛ 882line 881 didn't jump to line 882, because the condition on line 881 was never true

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

883 

884 return AutomaticDiscardRuleExample( 

885 tuple(example), 

886 description=example_description, 

887 ) 

888 

889 

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

891class PluginProvidedPackageProcessor: 

892 processor_id: str 

893 applies_to_package_types: FrozenSet[str] 

894 package_processor: PackageProcessor 

895 dependencies: FrozenSet[Tuple[str, str]] 

896 plugin_metadata: DebputyPluginMetadata 

897 

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

899 return binary_package.package_type in self.applies_to_package_types 

900 

901 @property 

902 def dependency_id(self) -> Tuple[str, str]: 

903 return self.plugin_metadata.plugin_name, self.processor_id 

904 

905 def run_package_processor( 

906 self, 

907 fs_root: "VirtualPath", 

908 unused: None, 

909 context: "PackageProcessingContext", 

910 ) -> None: 

911 self.package_processor(fs_root, unused, context) 

912 

913 

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

915class PluginProvidedDiscardRule: 

916 name: str 

917 plugin_metadata: DebputyPluginMetadata 

918 discard_check: Callable[[VirtualPath], bool] 

919 reference_documentation: Optional[str] 

920 examples: Sequence[AutomaticDiscardRuleExample] = tuple() 

921 

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

923 return self.discard_check(path) 

924 

925 

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

927class ServiceManagerDetails: 

928 service_manager: str 

929 service_detector: "ServiceDetector" 

930 service_integrator: "ServiceIntegrator" 

931 plugin_metadata: DebputyPluginMetadata 

932 

933 

934ReferenceValue = TypedDict( 

935 "ReferenceValue", 

936 { 

937 "description": str, 

938 }, 

939) 

940 

941 

942def _reference_data_value( 

943 *, 

944 description: str, 

945) -> ReferenceValue: 

946 return { 

947 "description": description, 

948 } 

949 

950 

951KnownPackagingFileCategories = Literal[ 

952 "generated", 

953 "generic-template", 

954 "ppf-file", 

955 "ppf-control-file", 

956 "maint-config", 

957 "pkg-metadata", 

958 "pkg-helper-config", 

959 "testing", 

960 "lint-config", 

961] 

962KNOWN_PACKAGING_FILE_CATEGORY_DESCRIPTIONS: Mapping[ 

963 KnownPackagingFileCategories, ReferenceValue 

964] = { 

965 "generated": _reference_data_value( 

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

967 ), 

968 "generic-template": _reference_data_value( 

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

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

971 " language inside it." 

972 ), 

973 "ppf-file": _reference_data_value( 

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

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

976 ), 

977 "ppf-control-file": _reference_data_value( 

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

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

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

981 ), 

982 "maint-config": _reference_data_value( 

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

984 ), 

985 "pkg-metadata": _reference_data_value( 

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

987 ), 

988 "pkg-helper-config": _reference_data_value( 

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

990 ), 

991 "testing": _reference_data_value( 

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

993 ), 

994 "lint-config": _reference_data_value( 

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

996 ), 

997} 

998 

999KnownPackagingConfigFeature = Literal[ 

1000 "dh-filearray", 

1001 "dh-filedoublearray", 

1002 "dh-hash-subst", 

1003 "dh-dollar-subst", 

1004 "dh-glob", 

1005 "dh-partial-glob", 

1006 "dh-late-glob", 

1007 "dh-glob-after-execute", 

1008 "dh-executable-config", 

1009 "dh-custom-format", 

1010 "dh-file-list", 

1011 "dh-install-list", 

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

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

1014 "dh-fixed-dest-dir", 

1015 "dh-exec-rename", 

1016 "dh-docs-only", 

1017] 

1018 

1019KNOWN_PACKAGING_FILE_CONFIG_FEATURE_DESCRIPTION: Mapping[ 

1020 KnownPackagingConfigFeature, ReferenceValue 

1021] = { 

1022 "dh-filearray": _reference_data_value( 

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

1024 ), 

1025 "dh-filedoublearray": _reference_data_value( 

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

1027 ), 

1028 "dh-hash-subst": _reference_data_value( 

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

1030 ), 

1031 "dh-dollar-subst": _reference_data_value( 

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

1033 ), 

1034 "dh-glob": _reference_data_value( 

1035 description="Supports standard debhelper globing", 

1036 ), 

1037 "dh-partial-glob": _reference_data_value( 

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

1039 ), 

1040 "dh-late-glob": _reference_data_value( 

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

1042 ), 

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

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

1045 ), 

1046 "dh-executable-config": _reference_data_value( 

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

1048 ), 

1049 "dh-custom-format": _reference_data_value( 

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

1051 ), 

1052 "dh-file-list": _reference_data_value( 

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

1054 ), 

1055 "dh-install-list": _reference_data_value( 

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

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

1058 ), 

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

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

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

1062 ), 

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

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

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

1066 ), 

1067 "dh-exec-rename": _reference_data_value( 

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

1069 " requested/used", 

1070 ), 

1071 "dh-docs-only": _reference_data_value( 

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

1073 ), 

1074} 

1075 

1076CONFIG_FEATURE_ALIASES: Dict[ 

1077 KnownPackagingConfigFeature, List[Tuple[KnownPackagingConfigFeature, int]] 

1078] = { 

1079 "dh-filearray": [ 

1080 ("dh-filearray", 0), 

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

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

1083 ], 

1084 "dh-filedoublearray": [ 

1085 ("dh-filedoublearray", 0), 

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

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

1088 ], 

1089} 

1090 

1091 

1092def _implies( 

1093 features: List[KnownPackagingConfigFeature], 

1094 seen: Set[KnownPackagingConfigFeature], 

1095 implying: Sequence[KnownPackagingConfigFeature], 

1096 implied: KnownPackagingConfigFeature, 

1097) -> None: 

1098 if implied in seen: 

1099 return 

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

1101 seen.add(implied) 

1102 features.append(implied) 

1103 

1104 

1105def expand_known_packaging_config_features( 

1106 compat_level: int, 

1107 features: List[KnownPackagingConfigFeature], 

1108) -> List[KnownPackagingConfigFeature]: 

1109 final_features: List[KnownPackagingConfigFeature] = [] 

1110 seen = set() 

1111 for feature in features: 

1112 expanded = CONFIG_FEATURE_ALIASES.get(feature) 

1113 if not expanded: 

1114 expanded = [(feature, 0)] 

1115 for v, c in expanded: 

1116 if compat_level < c or v in seen: 

1117 continue 

1118 seen.add(v) 

1119 final_features.append(v) 

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

1121 final_features.remove("dh-glob") 

1122 

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

1124 _implies( 

1125 final_features, 

1126 seen, 

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

1128 "dh-glob-after-execute", 

1129 ) 

1130 return sorted(final_features) 

1131 

1132 

1133class InstallPatternDHCompatRule(DebputyParsedContent): 

1134 install_pattern: NotRequired[str] 

1135 add_config_features: NotRequired[List[KnownPackagingConfigFeature]] 

1136 starting_with_compat_level: NotRequired[int] 

1137 

1138 

1139class KnownPackagingFileInfo(DebputyParsedContent): 

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

1141 path: NotRequired[str] 

1142 pkgfile: NotRequired[str] 

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

1144 file_categories: NotRequired[List[KnownPackagingFileCategories]] 

1145 documentation_uris: NotRequired[List[str]] 

1146 debputy_cmd_templates: NotRequired[List[List[str]]] 

1147 debhelper_commands: NotRequired[List[str]] 

1148 config_features: NotRequired[List[KnownPackagingConfigFeature]] 

1149 install_pattern: NotRequired[str] 

1150 dh_compat_rules: NotRequired[List[InstallPatternDHCompatRule]] 

1151 default_priority: NotRequired[int] 

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

1153 packageless_is_fallback_for_all_packages: NotRequired[bool] 

1154 

1155 

1156@dataclasses.dataclass(slots=True) 

1157class PluginProvidedKnownPackagingFile: 

1158 info: KnownPackagingFileInfo 

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

1160 detection_value: str 

1161 plugin_metadata: DebputyPluginMetadata 

1162 

1163 

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

1165class PluginProvidedTypeMapping: 

1166 mapped_type: TypeMapping[Any, Any] 

1167 reference_documentation: Optional[TypeMappingDocumentation] 

1168 plugin_metadata: DebputyPluginMetadata 

1169 

1170 

1171class PackageDataTable: 

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

1173 self._package_data_table = package_data_table 

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

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

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

1177 self.enable_cross_package_checks = False 

1178 

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

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

1181 

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

1183 return self._package_data_table[item] 

1184 

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

1186 return item in self._package_data_table 

1187 

1188 

1189class PackageProcessingContextProvider(PackageProcessingContext): 

1190 __slots__ = ( 

1191 "_manifest", 

1192 "_binary_package", 

1193 "_related_udeb_package", 

1194 "_package_data_table", 

1195 "_cross_check_cache", 

1196 ) 

1197 

1198 def __init__( 

1199 self, 

1200 manifest: "HighLevelManifest", 

1201 binary_package: BinaryPackage, 

1202 related_udeb_package: Optional[BinaryPackage], 

1203 package_data_table: PackageDataTable, 

1204 ) -> None: 

1205 self._manifest = manifest 

1206 self._binary_package = binary_package 

1207 self._related_udeb_package = related_udeb_package 

1208 self._package_data_table = ref(package_data_table) 

1209 self._cross_check_cache: Optional[ 

1210 Sequence[Tuple[BinaryPackage, "VirtualPath"]] 

1211 ] = None 

1212 

1213 def _package_state_for( 

1214 self, 

1215 package: BinaryPackage, 

1216 ) -> "PackageTransformationDefinition": 

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

1218 

1219 def _package_version_for( 

1220 self, 

1221 package: BinaryPackage, 

1222 ) -> str: 

1223 package_state = self._package_state_for(package) 

1224 version = package_state.binary_version 

1225 if version is not None: 

1226 return version 

1227 return self._manifest.source_version( 

1228 include_binnmu_version=not package.is_arch_all 

1229 ) 

1230 

1231 @property 

1232 def binary_package(self) -> BinaryPackage: 

1233 return self._binary_package 

1234 

1235 @property 

1236 def related_udeb_package(self) -> Optional[BinaryPackage]: 

1237 return self._related_udeb_package 

1238 

1239 @property 

1240 def binary_package_version(self) -> str: 

1241 return self._package_version_for(self._binary_package) 

1242 

1243 @property 

1244 def related_udeb_package_version(self) -> Optional[str]: 

1245 udeb = self._related_udeb_package 

1246 if udeb is None: 

1247 return None 

1248 return self._package_version_for(udeb) 

1249 

1250 def accessible_package_roots(self) -> Iterable[Tuple[BinaryPackage, "VirtualPath"]]: 

1251 package_table = self._package_data_table() 

1252 if package_table is None: 

1253 raise ReferenceError( 

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

1255 ) 

1256 if not package_table.enable_cross_package_checks: 

1257 raise PluginAPIViolationError( 

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

1259 ) 

1260 cache = self._cross_check_cache 

1261 if cache is None: 

1262 matches = [] 

1263 pkg = self.binary_package 

1264 for pkg_data in package_table: 

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

1266 continue 

1267 res = package_cross_check_precheck(pkg, pkg_data.binary_package) 

1268 if not res[0]: 

1269 continue 

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

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

1272 self._cross_check_cache = cache 

1273 return cache 

1274 

1275 

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

1277class PluginProvidedTrigger: 

1278 dpkg_trigger_type: DpkgTriggerType 

1279 dpkg_trigger_target: str 

1280 provider: DebputyPluginMetadata 

1281 provider_source_id: str 

1282 

1283 def serialized_format(self) -> str: 

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