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

586 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-02-14 10:41 +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 TYPE_CHECKING, 

9 TypeVar, 

10 cast, 

11 Any, 

12 TypedDict, 

13 NotRequired, 

14 Literal, 

15 Protocol, 

16) 

17from weakref import ref 

18 

19from debian.debian_support import DpkgArchTable 

20 

21from debputy._deb_options_profiles import DebBuildOptionsAndProfiles 

22from debputy.exceptions import ( 

23 DebputyFSIsROError, 

24 PluginAPIViolationError, 

25 PluginConflictError, 

26 UnhandledOrUnexpectedErrorFromPluginError, 

27 PluginBaseError, 

28 PluginInitializationError, 

29) 

30from debputy.filesystem_scan import as_path_def 

31from debputy.manifest_conditions import ConditionContext 

32from debputy.manifest_parser.exceptions import ManifestParseException 

33from debputy.manifest_parser.tagging_types import DebputyParsedContent, TypeMapping 

34from debputy.manifest_parser.util import AttributePath, check_integration_mode 

35from debputy.packages import BinaryPackage, SourcePackage 

36from debputy.plugin.api import ( 

37 VirtualPath, 

38 BinaryCtrlAccessor, 

39 PackageProcessingContext, 

40) 

41from debputy.plugin.api.spec import ( 

42 DebputyPluginInitializer, 

43 MetadataAutoDetector, 

44 DpkgTriggerType, 

45 ParserDocumentation, 

46 PackageProcessor, 

47 PathDef, 

48 ParserAttributeDocumentation, 

49 undocumented_attr, 

50 documented_attr, 

51 reference_documentation, 

52 PackagerProvidedFileReferenceDocumentation, 

53 TypeMappingDocumentation, 

54 DebputyIntegrationMode, 

55) 

56from debputy.plugin.plugin_state import ( 

57 run_in_context_of_plugin, 

58) 

59from debputy.substitution import VariableContext 

60from debputy.util import ( 

61 _error, 

62 _normalize_path, 

63 package_cross_check_precheck, 

64 PackageTypeSelector, 

65) 

66 

67if TYPE_CHECKING: 

68 from debputy.lsp.diagnostics import LintSeverity 

69 from debputy.plugin.api.spec import ( 

70 ServiceDetector, 

71 ServiceIntegrator, 

72 ) 

73 from debputy.manifest_parser.parser_data import ParserContextData 

74 from debputy.highlevel_manifest import ( 

75 HighLevelManifest, 

76 PackageTransformationDefinition, 

77 BinaryPackageData, 

78 ) 

79 from debputy.plugins.debputy.to_be_api_types import ( 

80 BuildRuleParsedFormat, 

81 BuildSystemRule, 

82 ) 

83 

84 

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

86PF = TypeVar("PF") 

87SF = TypeVar("SF") 

88TP = TypeVar("TP") 

89TTP = type[TP] 

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

91 

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

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

94 

95 

96@dataclasses.dataclass(slots=True) 

97class DebputyPluginMetadata: 

98 plugin_name: str 

99 api_compat_version: int 

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

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

102 plugin_path: str 

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

104 is_from_python_path: bool = False 

105 _is_initialized: bool = False 

106 _is_doc_path_resolved: bool = False 

107 _plugin_doc_path: Traversable | Path | None = None 

108 

109 @property 

110 def is_bundled(self) -> bool: 

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

112 

113 @property 

114 def is_loaded(self) -> bool: 

115 return self.plugin_initializer is not None 

116 

117 @property 

118 def is_initialized(self) -> bool: 

119 return self._is_initialized 

120 

121 @property 

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

123 if not self._is_doc_path_resolved: 

124 self._plugin_doc_path = self.plugin_doc_path_resolver() 

125 self._is_doc_path_resolved = True 

126 return self._plugin_doc_path 

127 

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

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

130 raise RuntimeError("Cannot load plugins twice") 

131 if not self.is_loaded: 

132 self.load_plugin() 

133 plugin_initializer = self.plugin_initializer 

134 assert plugin_initializer is not None 

135 plugin_initializer(api) 

136 self._is_initialized = True 

137 

138 def load_plugin(self) -> None: 

139 plugin_loader = self.plugin_loader 

140 assert plugin_loader is not None 

141 try: 

142 self.plugin_initializer = run_in_context_of_plugin( 

143 self.plugin_name, 

144 plugin_loader, 

145 ) 

146 except PluginBaseError: 

147 raise 

148 except Exception as e: 

149 raise PluginInitializationError( 

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

151 ) from e 

152 assert self.plugin_initializer is not None 

153 

154 

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

156class PluginProvidedParser[PF, TP]: 

157 parser: "DeclarativeInputParser[PF]" 

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

159 plugin_metadata: DebputyPluginMetadata 

160 

161 def parse( 

162 self, 

163 name: str, 

164 value: object, 

165 attribute_path: AttributePath, 

166 *, 

167 parser_context: "ParserContextData", 

168 ) -> TP: 

169 parsed_value = self.parser.parse_input( 

170 value, 

171 attribute_path, 

172 parser_context=parser_context, 

173 ) 

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

175 

176 

177class PPFFormatParam(TypedDict): 

178 priority: int | None 

179 name: str 

180 owning_package: str 

181 

182 

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

184class PackagerProvidedFileClassSpec: 

185 debputy_plugin_metadata: DebputyPluginMetadata 

186 stem: str 

187 installed_as_format: str 

188 default_mode: int 

189 default_priority: int | None 

190 allow_name_segment: bool 

191 allow_architecture_segment: bool 

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

193 packageless_is_fallback_for_all_packages: bool 

194 reservation_only: bool 

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

196 reference_documentation: PackagerProvidedFileReferenceDocumentation | None = None 

197 bug_950723: bool = False 

198 has_active_command: bool = True 

199 

200 @property 

201 def supports_priority(self) -> bool: 

202 return self.default_priority is not None 

203 

204 def compute_dest( 

205 self, 

206 assigned_name: str, 

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

208 *, 

209 owning_package: str | None = None, 

210 assigned_priority: int | None = None, 

211 path: VirtualPath | None = None, 

212 ) -> tuple[str, str]: 

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

214 raise ValueError( 

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

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

217 " do not use priority at all." 

218 ) 

219 

220 path_format = self.installed_as_format 

221 if self.supports_priority and assigned_priority is None: 

222 assigned_priority = self.default_priority 

223 

224 if owning_package is None: 

225 owning_package = assigned_name 

226 

227 params: PPFFormatParam = { 

228 "priority": assigned_priority, 

229 "name": assigned_name, 

230 "owning_package": owning_package, 

231 } 

232 

233 if self.formatting_callback is not None: 

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

235 raise ValueError( 

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

237 ) 

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

239 else: 

240 dest_path = path_format.format(**params) 

241 

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

243 dirname = _normalize_path(dirname) 

244 

245 if self.post_formatting_rewrite: 

246 basename = self.post_formatting_rewrite(basename) 

247 return dirname, basename 

248 

249 

250@dataclasses.dataclass(slots=True) 

251class MetadataOrMaintscriptDetector: 

252 plugin_metadata: DebputyPluginMetadata 

253 detector_id: str 

254 detector: MetadataAutoDetector 

255 applies_to_package_types: PackageTypeSelector 

256 enabled: bool = True 

257 

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

259 return binary_package.package_type in self.applies_to_package_types 

260 

261 def run_detector( 

262 self, 

263 fs_root: "VirtualPath", 

264 ctrl: "BinaryCtrlAccessor", 

265 context: "PackageProcessingContext", 

266 ) -> None: 

267 try: 

268 self.detector(fs_root, ctrl, context) 

269 except DebputyFSIsROError as e: 

270 nv = self.plugin_metadata.plugin_name 

271 raise PluginAPIViolationError( 

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

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

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

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

276 " would be lost)." 

277 ) from e 

278 except UnhandledOrUnexpectedErrorFromPluginError as e: 

279 e.add_note( 

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

281 ) 

282 

283 

284class DeclarativeInputParser[TD]: 

285 @property 

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

287 return None 

288 

289 @property 

290 def expected_debputy_integration_mode( 

291 self, 

292 ) -> Container[DebputyIntegrationMode] | None: 

293 return None 

294 

295 @property 

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

297 doc = self.inline_reference_documentation 

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

299 

300 def parse_input( 

301 self, 

302 value: object, 

303 path: AttributePath, 

304 *, 

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

306 ) -> TD: 

307 raise NotImplementedError 

308 

309 

310class DelegatingDeclarativeInputParser(DeclarativeInputParser[TD]): 

311 __slots__ = ( 

312 "delegate", 

313 "_reference_documentation", 

314 "_expected_debputy_integration_mode", 

315 ) 

316 

317 def __init__( 

318 self, 

319 delegate: DeclarativeInputParser[TD], 

320 *, 

321 inline_reference_documentation: ParserDocumentation | None = None, 

322 expected_debputy_integration_mode: None | ( 

323 Container[DebputyIntegrationMode] 

324 ) = None, 

325 ) -> None: 

326 self.delegate = delegate 

327 self._reference_documentation = inline_reference_documentation 

328 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

329 

330 @property 

331 def expected_debputy_integration_mode( 

332 self, 

333 ) -> Container[DebputyIntegrationMode] | None: 

334 return self._expected_debputy_integration_mode 

335 

336 @property 

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

338 doc = self._reference_documentation 

339 if doc is None: 

340 return self.delegate.inline_reference_documentation 

341 return doc 

342 

343 

344class ListWrappedDeclarativeInputParser(DelegatingDeclarativeInputParser[TD]): 

345 __slots__ = () 

346 

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

348 doc_url = self.reference_documentation_url 

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

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

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

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

353 return "" 

354 

355 def parse_input( 

356 self, 

357 value: object, 

358 path: AttributePath, 

359 *, 

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

361 ) -> TD: 

362 check_integration_mode( 

363 path, parser_context, self._expected_debputy_integration_mode 

364 ) 

365 if not isinstance(value, list): 

366 doc_ref = self._doc_url_error_suffix(see_url_version=True) 

367 raise ManifestParseException( 

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

369 ) 

370 result = [] 

371 delegate = self.delegate 

372 for idx, element in enumerate(value): 

373 element_path = path[idx] 

374 result.append( 

375 delegate.parse_input( 

376 element, 

377 element_path, 

378 parser_context=parser_context, 

379 ) 

380 ) 

381 return result 

382 

383 

384class DispatchingParserBase[TP]: 

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

386 self.manifest_attribute_path_template = manifest_attribute_path_template 

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

388 

389 @property 

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

391 return "error" 

392 

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

394 return keyword in self._parsers 

395 

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

397 yield from self._parsers 

398 

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

400 return self._parsers[keyword] 

401 

402 def register_keyword( 

403 self, 

404 keyword: str | Sequence[str], 

405 handler: DIPKWHandler, 

406 plugin_metadata: DebputyPluginMetadata, 

407 *, 

408 inline_reference_documentation: ParserDocumentation | None = None, 

409 ) -> None: 

410 reference_documentation_url = None 

411 if inline_reference_documentation: 

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

413 raise ValueError( 

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

415 ) 

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

417 raise ValueError( 

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

419 ) 

420 reference_documentation_url = ( 

421 inline_reference_documentation.documentation_reference_url 

422 ) 

423 parser = DeclarativeValuelessKeywordInputParser( 

424 inline_reference_documentation, 

425 documentation_reference=reference_documentation_url, 

426 ) 

427 

428 def _combined_handler( 

429 name: str, 

430 _ignored: Any, 

431 attr_path: AttributePath, 

432 context: "ParserContextData", 

433 ) -> TP: 

434 return handler(name, attr_path, context) 

435 

436 p = PluginProvidedParser( 

437 parser, 

438 _combined_handler, 

439 plugin_metadata, 

440 ) 

441 

442 self._add_parser(keyword, p) 

443 

444 def register_parser( 

445 self, 

446 keyword: str | list[str], 

447 parser: "DeclarativeInputParser[PF]", 

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

449 plugin_metadata: DebputyPluginMetadata, 

450 ) -> None: 

451 p = PluginProvidedParser( 

452 parser, 

453 handler, 

454 plugin_metadata, 

455 ) 

456 self._add_parser(keyword, p) 

457 

458 def _add_parser( 

459 self, 

460 keyword: str | Iterable[str], 

461 ppp: PluginProvidedParser[PF, TP], 

462 ) -> None: 

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

464 for k in ks: 

465 existing_parser = self._parsers.get(k) 

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

467 message = ( 

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

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

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

471 ) 

472 raise PluginConflictError( 

473 message, 

474 existing_parser.plugin_metadata, 

475 ppp.plugin_metadata, 

476 ) 

477 self._new_parser(k, ppp) 

478 

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

480 self._parsers[keyword] = ppp 

481 

482 def parse_input( 

483 self, 

484 orig_value: object, 

485 attribute_path: AttributePath, 

486 *, 

487 parser_context: "ParserContextData", 

488 ) -> TP: 

489 raise NotImplementedError 

490 

491 

492class DispatchingObjectParser( 

493 DispatchingParserBase[Mapping[str, Any]], 

494 DeclarativeInputParser[Mapping[str, Any]], 

495): 

496 def __init__( 

497 self, 

498 manifest_attribute_path_template: str, 

499 *, 

500 parser_documentation: ParserDocumentation | None = None, 

501 expected_debputy_integration_mode: None | ( 

502 Container[DebputyIntegrationMode] 

503 ) = None, 

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

505 allow_unknown_keys: bool = False, 

506 ) -> None: 

507 super().__init__(manifest_attribute_path_template) 

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

509 if parser_documentation is None: 

510 parser_documentation = reference_documentation() 

511 self._parser_documentation = parser_documentation 

512 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

513 self._unknown_keys_diagnostic_severity = unknown_keys_diagnostic_severity 

514 self._allow_unknown_keys = allow_unknown_keys 

515 

516 @property 

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

518 return self._unknown_keys_diagnostic_severity 

519 

520 @property 

521 def expected_debputy_integration_mode( 

522 self, 

523 ) -> Container[DebputyIntegrationMode] | None: 

524 return self._expected_debputy_integration_mode 

525 

526 @property 

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

528 return self._parser_documentation.documentation_reference_url 

529 

530 @property 

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

532 ref_doc = self._parser_documentation 

533 return reference_documentation( 

534 title=ref_doc.title, 

535 description=ref_doc.description, 

536 attributes=self._attribute_documentation, 

537 reference_documentation_url=self.reference_documentation_url, 

538 ) 

539 

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

541 super()._new_parser(keyword, ppp) 

542 doc = ppp.parser.inline_reference_documentation 

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

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

545 else: 

546 self._attribute_documentation.append( 

547 documented_attr(keyword, doc.description) 

548 ) 

549 

550 def register_child_parser( 

551 self, 

552 keyword: str, 

553 parser: "DispatchingObjectParser", 

554 plugin_metadata: DebputyPluginMetadata, 

555 *, 

556 on_end_parse_step: None | ( 

557 Callable[ 

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

559 None, 

560 ] 

561 ) = None, 

562 nested_in_package_context: bool = False, 

563 ) -> None: 

564 def _handler( 

565 name: str, 

566 value: Mapping[str, Any], 

567 path: AttributePath, 

568 parser_context: "ParserContextData", 

569 ) -> Mapping[str, Any]: 

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

571 on_end_parse_step(name, value, path, parser_context) 

572 return value 

573 

574 if nested_in_package_context: 

575 parser = InPackageContextParser( 

576 keyword, 

577 parser, 

578 ) 

579 

580 p = PluginProvidedParser( 

581 parser, 

582 _handler, 

583 plugin_metadata, 

584 ) 

585 self._add_parser(keyword, p) 

586 

587 def parse_input( 

588 self, 

589 orig_value: object, 

590 attribute_path: AttributePath, 

591 *, 

592 parser_context: "ParserContextData", 

593 ) -> TP: 

594 check_integration_mode( 

595 attribute_path, 

596 parser_context, 

597 self._expected_debputy_integration_mode, 

598 ) 

599 doc_ref = "" 

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

601 doc_ref = ( 

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

603 ) 

604 if not isinstance(orig_value, dict): 

605 raise ManifestParseException( 

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

607 ) 

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

609 raise ManifestParseException( 

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

611 ) 

612 result = {} 

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

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

615 first_key = next(iter(unknown_keys)) 

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

617 if not remaining_valid_attributes: 

618 raise ManifestParseException( 

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

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

621 ) 

622 remaining_valid_attribute_names = ", ".join(remaining_valid_attributes) 

623 raise ManifestParseException( 

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

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

626 f" {remaining_valid_attribute_names}.{doc_ref}" 

627 ) 

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

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

630 value = orig_value.get(key) 

631 if value is None: 

632 if isinstance(provided_parser.parser, DispatchingObjectParser): 

633 provided_parser.handler( 

634 key, 

635 {}, 

636 attribute_path[key], 

637 parser_context, 

638 ) 

639 continue 

640 value_path = attribute_path[key] 

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

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

643 raise ManifestParseException( 

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

645 " Valid options at this location are:" 

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

647 ) 

648 parsed_value = provided_parser.parse( 

649 key, value, value_path, parser_context=parser_context 

650 ) 

651 result[key] = parsed_value 

652 return result 

653 

654 

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

656class PackageContextData[TP]: 

657 resolved_package_name: str 

658 value: TP 

659 

660 

661class InPackageContextParser( 

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

663): 

664 __slots__ = () 

665 

666 def __init__( 

667 self, 

668 manifest_attribute_path_template: str, 

669 delegate: DeclarativeInputParser[TP], 

670 *, 

671 parser_documentation: ParserDocumentation | None = None, 

672 ) -> None: 

673 self.manifest_attribute_path_template = manifest_attribute_path_template 

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

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

676 

677 def parse_input( 

678 self, 

679 orig_value: object, 

680 attribute_path: AttributePath, 

681 *, 

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

683 ) -> TP: 

684 assert parser_context is not None 

685 check_integration_mode( 

686 attribute_path, 

687 parser_context, 

688 self._expected_debputy_integration_mode, 

689 ) 

690 doc_ref = "" 

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

692 doc_ref = ( 

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

694 ) 

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

696 raise ManifestParseException( 

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

698 ) 

699 delegate = self.delegate 

700 result = {} 

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

702 

703 definition_source = attribute_path[package_name_raw] 

704 package_name = package_name_raw 

705 if "{{" in package_name: 

706 package_name = parser_context.substitution.substitute( 

707 package_name_raw, 

708 definition_source.path, 

709 ) 

710 package_state: PackageTransformationDefinition 

711 with parser_context.binary_package_context(package_name) as package_state: 

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

713 # Maybe lift (part) of this restriction. 

714 raise ManifestParseException( 

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

716 " auto-generated package." 

717 ) 

718 parsed_value = delegate.parse_input( 

719 value, definition_source, parser_context=parser_context 

720 ) 

721 result[package_name_raw] = PackageContextData( 

722 package_name, parsed_value 

723 ) 

724 return result 

725 

726 

727class DispatchingTableParser( 

728 DispatchingParserBase[TP], 

729 DeclarativeInputParser[TP], 

730): 

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

732 super().__init__(manifest_attribute_path_template) 

733 self.base_type = base_type 

734 

735 def parse_input( 

736 self, 

737 orig_value: object, 

738 attribute_path: AttributePath, 

739 *, 

740 parser_context: "ParserContextData", 

741 ) -> TP: 

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

743 key = orig_value 

744 value = None 

745 value_path = attribute_path 

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

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

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

749 raise ManifestParseException( 

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

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

752 f" possible keys are: {valid_keys}" 

753 ) 

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

755 value_path = attribute_path[key] 

756 else: 

757 raise ManifestParseException( 

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

759 ) 

760 provided_parser = self._parsers.get(key) 

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

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

763 raise ManifestParseException( 

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

765 " Valid actions at this location are:" 

766 f" {valid_keys}" 

767 ) 

768 return provided_parser.parse( 

769 key, value, value_path, parser_context=parser_context 

770 ) 

771 

772 

773@dataclasses.dataclass(slots=True) 

774class DeclarativeValuelessKeywordInputParser(DeclarativeInputParser[None]): 

775 inline_reference_documentation: ParserDocumentation | None = None 

776 documentation_reference: str | None = None 

777 

778 def parse_input( 

779 self, 

780 value: object, 

781 path: AttributePath, 

782 *, 

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

784 ) -> TD: 

785 if value is None: 

786 return cast("TD", value) 

787 if self.documentation_reference is not None: 

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

789 else: 

790 doc_ref = "" 

791 raise ManifestParseException( 

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

793 ) 

794 

795 

796@dataclasses.dataclass(slots=True) 

797class PluginProvidedManifestVariable: 

798 plugin_metadata: DebputyPluginMetadata 

799 variable_name: str 

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

801 is_context_specific_variable: bool 

802 variable_reference_documentation: str | None = None 

803 is_documentation_placeholder: bool = False 

804 is_for_special_case: bool = False 

805 

806 @property 

807 def is_internal(self) -> bool: 

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

809 

810 @property 

811 def is_token(self) -> bool: 

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

813 

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

815 value_resolver = self.variable_value 

816 if isinstance(value_resolver, str): 

817 res = value_resolver 

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

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

820 else: 

821 res = value_resolver(variable_context) 

822 return res 

823 

824 

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

826class AutomaticDiscardRuleExample: 

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

828 description: str | None = None 

829 

830 

831def automatic_discard_rule_example( 

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

833 example_description: str | None = None, 

834) -> AutomaticDiscardRuleExample: 

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

836 

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

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

839 part of a sequence of examples. 

840 

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

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

843 >>> # will be kept. 

844 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

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

847 ... ) 

848 AutomaticDiscardRuleExample(...) 

849 

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

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

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

853 

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

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

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

857 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

859 ... ".../__pycache__/", 

860 ... ".../__pycache__/...", 

861 ... ".../foo.pyc", 

862 ... ".../foo.pyo", 

863 ... ) 

864 AutomaticDiscardRuleExample(...) 

865 

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

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

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

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

870 

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

872 

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

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

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

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

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

878 

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

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

881 verdict is assumed to be discarded (True). 

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

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

884 """ 

885 example = [] 

886 for d in content: 

887 if not isinstance(d, tuple): 

888 pd = d 

889 verdict = True 

890 else: 

891 pd, verdict = d 

892 

893 path_def = as_path_def(pd) 

894 example.append((path_def, verdict)) 

895 

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

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

898 

899 return AutomaticDiscardRuleExample( 

900 tuple(example), 

901 description=example_description, 

902 ) 

903 

904 

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

906class PluginProvidedPackageProcessor: 

907 processor_id: str 

908 applies_to_package_types: PackageTypeSelector 

909 package_processor: PackageProcessor 

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

911 plugin_metadata: DebputyPluginMetadata 

912 

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

914 return binary_package.package_type in self.applies_to_package_types 

915 

916 @property 

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

918 return self.plugin_metadata.plugin_name, self.processor_id 

919 

920 def run_package_processor( 

921 self, 

922 fs_root: "VirtualPath", 

923 unused: None, 

924 context: "PackageProcessingContext", 

925 ) -> None: 

926 self.package_processor(fs_root, unused, context) 

927 

928 

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

930class PluginProvidedDiscardRule: 

931 name: str 

932 plugin_metadata: DebputyPluginMetadata 

933 discard_check: Callable[[VirtualPath], bool] 

934 reference_documentation: str | None 

935 examples: Sequence[AutomaticDiscardRuleExample] = tuple() 

936 

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

938 return self.discard_check(path) 

939 

940 

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

942class ServiceManagerDetails: 

943 service_manager: str 

944 service_detector: "ServiceDetector" 

945 service_integrator: "ServiceIntegrator" 

946 plugin_metadata: DebputyPluginMetadata 

947 

948 

949class ReferenceValue(TypedDict): 

950 description: str 

951 

952 

953def _reference_data_value( 

954 *, 

955 description: str, 

956) -> ReferenceValue: 

957 return { 

958 "description": description, 

959 } 

960 

961 

962KnownPackagingFileCategories = Literal[ 

963 "generated", 

964 "generic-template", 

965 "ppf-file", 

966 "ppf-control-file", 

967 "maint-config", 

968 "pkg-metadata", 

969 "pkg-helper-config", 

970 "testing", 

971 "lint-config", 

972] 

973KNOWN_PACKAGING_FILE_CATEGORY_DESCRIPTIONS: Mapping[ 

974 KnownPackagingFileCategories, ReferenceValue 

975] = { 

976 "generated": _reference_data_value( 

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

978 ), 

979 "generic-template": _reference_data_value( 

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

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

982 " language inside it." 

983 ), 

984 "ppf-file": _reference_data_value( 

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

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

987 ), 

988 "ppf-control-file": _reference_data_value( 

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

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

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

992 ), 

993 "maint-config": _reference_data_value( 

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

995 ), 

996 "pkg-metadata": _reference_data_value( 

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

998 ), 

999 "pkg-helper-config": _reference_data_value( 

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

1001 ), 

1002 "testing": _reference_data_value( 

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

1004 ), 

1005 "lint-config": _reference_data_value( 

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

1007 ), 

1008} 

1009 

1010KnownPackagingConfigFeature = Literal[ 

1011 "dh-filearray", 

1012 "dh-filedoublearray", 

1013 "dh-hash-subst", 

1014 "dh-dollar-subst", 

1015 "dh-glob", 

1016 "dh-partial-glob", 

1017 "dh-late-glob", 

1018 "dh-glob-after-execute", 

1019 "dh-executable-config", 

1020 "dh-custom-format", 

1021 "dh-file-list", 

1022 "dh-install-list", 

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

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

1025 "dh-fixed-dest-dir", 

1026 "dh-exec-rename", 

1027 "dh-docs-only", 

1028] 

1029 

1030KNOWN_PACKAGING_FILE_CONFIG_FEATURE_DESCRIPTION: Mapping[ 

1031 KnownPackagingConfigFeature, ReferenceValue 

1032] = { 

1033 "dh-filearray": _reference_data_value( 

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

1035 ), 

1036 "dh-filedoublearray": _reference_data_value( 

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

1038 ), 

1039 "dh-hash-subst": _reference_data_value( 

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

1041 ), 

1042 "dh-dollar-subst": _reference_data_value( 

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

1044 ), 

1045 "dh-glob": _reference_data_value( 

1046 description="Supports standard debhelper globing", 

1047 ), 

1048 "dh-partial-glob": _reference_data_value( 

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

1050 ), 

1051 "dh-late-glob": _reference_data_value( 

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

1053 ), 

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

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

1056 ), 

1057 "dh-executable-config": _reference_data_value( 

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

1059 ), 

1060 "dh-custom-format": _reference_data_value( 

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

1062 ), 

1063 "dh-file-list": _reference_data_value( 

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

1065 ), 

1066 "dh-install-list": _reference_data_value( 

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

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

1069 ), 

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

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

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

1073 ), 

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

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

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

1077 ), 

1078 "dh-exec-rename": _reference_data_value( 

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

1080 " requested/used", 

1081 ), 

1082 "dh-docs-only": _reference_data_value( 

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

1084 ), 

1085} 

1086 

1087CONFIG_FEATURE_ALIASES: dict[ 

1088 KnownPackagingConfigFeature, list[tuple[KnownPackagingConfigFeature, int]] 

1089] = { 

1090 "dh-filearray": [ 

1091 ("dh-filearray", 0), 

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

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

1094 ], 

1095 "dh-filedoublearray": [ 

1096 ("dh-filedoublearray", 0), 

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

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

1099 ], 

1100} 

1101 

1102 

1103def _implies( 

1104 features: list[KnownPackagingConfigFeature], 

1105 seen: set[KnownPackagingConfigFeature], 

1106 implying: Sequence[KnownPackagingConfigFeature], 

1107 implied: KnownPackagingConfigFeature, 

1108) -> None: 

1109 if implied in seen: 

1110 return 

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

1112 seen.add(implied) 

1113 features.append(implied) 

1114 

1115 

1116def expand_known_packaging_config_features( 

1117 compat_level: int, 

1118 features: list[KnownPackagingConfigFeature], 

1119) -> list[KnownPackagingConfigFeature]: 

1120 final_features: list[KnownPackagingConfigFeature] = [] 

1121 seen = set() 

1122 for feature in features: 

1123 expanded = CONFIG_FEATURE_ALIASES.get(feature) 

1124 if not expanded: 

1125 expanded = [(feature, 0)] 

1126 for v, c in expanded: 

1127 if compat_level < c or v in seen: 

1128 continue 

1129 seen.add(v) 

1130 final_features.append(v) 

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

1132 final_features.remove("dh-glob") 

1133 

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

1135 _implies( 

1136 final_features, 

1137 seen, 

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

1139 "dh-glob-after-execute", 

1140 ) 

1141 return sorted(final_features) 

1142 

1143 

1144class DHCompatibilityBasedRule(DebputyParsedContent): 

1145 install_pattern: NotRequired[str] 

1146 add_config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1147 starting_with_compat_level: NotRequired[int] 

1148 

1149 

1150class KnownPackagingFileInfo(DebputyParsedContent): 

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

1152 path: NotRequired[str] 

1153 pkgfile: NotRequired[str] 

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

1155 file_categories: NotRequired[list[KnownPackagingFileCategories]] 

1156 documentation_uris: NotRequired[list[str]] 

1157 debputy_cmd_templates: NotRequired[list[list[str]]] 

1158 debhelper_commands: NotRequired[list[str]] 

1159 config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1160 install_pattern: NotRequired[str] 

1161 dh_compat_rules: NotRequired[list[DHCompatibilityBasedRule]] 

1162 default_priority: NotRequired[int] 

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

1164 packageless_is_fallback_for_all_packages: NotRequired[bool] 

1165 has_active_command: NotRequired[bool] 

1166 

1167 

1168@dataclasses.dataclass(slots=True) 

1169class PluginProvidedKnownPackagingFile: 

1170 info: KnownPackagingFileInfo 

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

1172 detection_value: str 

1173 plugin_metadata: DebputyPluginMetadata 

1174 

1175 

1176class BuildSystemAutoDetector(Protocol): 

1177 

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

1179 

1180 

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

1182class PluginProvidedTypeMapping: 

1183 mapped_type: TypeMapping[Any, Any] 

1184 reference_documentation: TypeMappingDocumentation | None 

1185 plugin_metadata: DebputyPluginMetadata 

1186 

1187 

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

1189class PluginProvidedBuildSystemAutoDetection[BSR]: 

1190 manifest_keyword: str 

1191 build_system_rule_type: type[BSR] 

1192 detector: BuildSystemAutoDetector 

1193 constructor: Callable[ 

1194 ["BuildRuleParsedFormat", AttributePath, "HighLevelManifest"], 

1195 BSR, 

1196 ] 

1197 auto_detection_shadow_build_systems: frozenset[str] 

1198 plugin_metadata: DebputyPluginMetadata 

1199 

1200 

1201class PackageDataTable: 

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

1203 self._package_data_table = package_data_table 

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

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

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

1207 self.enable_cross_package_checks = False 

1208 

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

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

1211 

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

1213 return self._package_data_table[item] 

1214 

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

1216 return item in self._package_data_table 

1217 

1218 

1219class PackageProcessingContextProvider(PackageProcessingContext): 

1220 __slots__ = ( 

1221 "_manifest", 

1222 "_binary_package", 

1223 "_related_udeb_package", 

1224 "_package_data_table", 

1225 "_cross_check_cache", 

1226 ) 

1227 

1228 def __init__( 

1229 self, 

1230 manifest: "HighLevelManifest", 

1231 binary_package: BinaryPackage, 

1232 related_udeb_package: BinaryPackage | None, 

1233 package_data_table: PackageDataTable, 

1234 ) -> None: 

1235 self._manifest = manifest 

1236 self._binary_package = binary_package 

1237 self._related_udeb_package = related_udeb_package 

1238 self._package_data_table = ref(package_data_table) 

1239 self._cross_check_cache: None | ( 

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

1241 ) = None 

1242 

1243 def _package_state_for( 

1244 self, 

1245 package: BinaryPackage, 

1246 ) -> "PackageTransformationDefinition": 

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

1248 

1249 def _package_version_for( 

1250 self, 

1251 package: BinaryPackage, 

1252 ) -> str: 

1253 package_state = self._package_state_for(package) 

1254 version = package_state.binary_version 

1255 if version is not None: 

1256 return version 

1257 return self._manifest.source_version( 

1258 include_binnmu_version=not package.is_arch_all 

1259 ) 

1260 

1261 @property 

1262 def source_package(self) -> SourcePackage: 

1263 return self._manifest.source_package 

1264 

1265 @property 

1266 def binary_package(self) -> BinaryPackage: 

1267 return self._binary_package 

1268 

1269 @property 

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

1271 return self._related_udeb_package 

1272 

1273 @property 

1274 def binary_package_version(self) -> str: 

1275 return self._package_version_for(self._binary_package) 

1276 

1277 @property 

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

1279 udeb = self._related_udeb_package 

1280 if udeb is None: 

1281 return None 

1282 return self._package_version_for(udeb) 

1283 

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

1285 package_table = self._package_data_table() 

1286 if package_table is None: 

1287 raise ReferenceError( 

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

1289 ) 

1290 if not package_table.enable_cross_package_checks: 

1291 raise PluginAPIViolationError( 

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

1293 ) 

1294 cache = self._cross_check_cache 

1295 if cache is None: 

1296 matches = [] 

1297 pkg = self.binary_package 

1298 for pkg_data in package_table: 

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

1300 continue 

1301 res = package_cross_check_precheck(pkg, pkg_data.binary_package) 

1302 if not res[0]: 

1303 continue 

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

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

1306 self._cross_check_cache = cache 

1307 return cache 

1308 

1309 def manifest_configuration[T]( 

1310 self, 

1311 context_package: SourcePackage | BinaryPackage, 

1312 value_type: type[T], 

1313 ) -> T | None: 

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

1315 

1316 @property 

1317 def dpkg_arch_query_table(self) -> DpkgArchTable: 

1318 return self._manifest.dpkg_arch_query_table 

1319 

1320 @property 

1321 def deb_options_and_profiles(self) -> DebBuildOptionsAndProfiles: 

1322 return self._manifest.deb_options_and_profiles 

1323 

1324 @property 

1325 def source_condition_context(self) -> ConditionContext: 

1326 return self._manifest.source_condition_context 

1327 

1328 def condition_context( 

1329 self, binary_package: BinaryPackage | None 

1330 ) -> ConditionContext: 

1331 return self._manifest.condition_context(binary_package) 

1332 

1333 

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

1335class PluginProvidedTrigger: 

1336 dpkg_trigger_type: DpkgTriggerType 

1337 dpkg_trigger_target: str 

1338 provider: DebputyPluginMetadata 

1339 provider_source_id: str 

1340 

1341 def serialized_format(self) -> str: 

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