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

522 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-19 09:13 +0000

1import collections 

2import dataclasses 

3import os.path 

4import typing 

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

6from importlib.resources.abc import Traversable 

7from pathlib import Path 

8from typing import ( 

9 Optional, 

10 TYPE_CHECKING, 

11 TypeVar, 

12 cast, 

13 Any, 

14 TypedDict, 

15 NotRequired, 

16 Literal, 

17 Protocol, 

18) 

19from weakref import ref 

20 

21from debian.debian_support import DpkgArchTable 

22 

23from debputy._deb_options_profiles import DebBuildOptionsAndProfiles 

24from debputy.exceptions import ( 

25 DebputyFSIsROError, 

26 PluginAPIViolationError, 

27 PluginConflictError, 

28 UnhandledOrUnexpectedErrorFromPluginError, 

29 PluginBaseError, 

30 PluginInitializationError, 

31) 

32from debputy.filesystem_scan import as_path_def 

33from debputy.manifest_conditions import ConditionContext 

34from debputy.manifest_parser.exceptions import ManifestParseException 

35from debputy.manifest_parser.tagging_types import DebputyParsedContent, TypeMapping 

36from debputy.manifest_parser.util import AttributePath, check_integration_mode 

37from debputy.packages import BinaryPackage, SourcePackage 

38from debputy.plugin.api import ( 

39 VirtualPath, 

40 BinaryCtrlAccessor, 

41 PackageProcessingContext, 

42) 

43from debputy.plugin.api.spec import ( 

44 DebputyPluginInitializer, 

45 MetadataAutoDetector, 

46 DpkgTriggerType, 

47 ParserDocumentation, 

48 PackageProcessor, 

49 PathDef, 

50 ParserAttributeDocumentation, 

51 undocumented_attr, 

52 documented_attr, 

53 reference_documentation, 

54 PackagerProvidedFileReferenceDocumentation, 

55 TypeMappingDocumentation, 

56 DebputyIntegrationMode, 

57) 

58from debputy.plugin.plugin_state import ( 

59 run_in_context_of_plugin, 

60) 

61from debputy.substitution import VariableContext 

62from debputy.util import ( 

63 _error, 

64 _normalize_path, 

65 package_cross_check_precheck, 

66 PackageTypeSelector, 

67) 

68 

69if TYPE_CHECKING: 

70 from debputy.lsp.diagnostics import LintSeverity 

71 from debputy.plugin.api.spec import ( 

72 ServiceDetector, 

73 ServiceIntegrator, 

74 ) 

75 from debputy.manifest_parser.parser_data import ParserContextData 

76 from debputy.highlevel_manifest import ( 

77 HighLevelManifest, 

78 PackageTransformationDefinition, 

79 BinaryPackageData, 

80 ) 

81 from debputy.plugins.debputy.to_be_api_types import ( 

82 BuildRuleParsedFormat, 

83 BuildSystemRule, 

84 ) 

85 

86 

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

88PF = TypeVar("PF") 

89SF = TypeVar("SF") 

90TP = TypeVar("TP") 

91TTP = type[TP] 

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

93 

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

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

96 

97 

98@dataclasses.dataclass(slots=True) 

99class DebputyPluginMetadata: 

100 plugin_name: str 

101 api_compat_version: int 

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

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

104 plugin_path: str 

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

106 is_from_python_path: bool = False 

107 _is_initialized: bool = False 

108 _is_doc_path_resolved: bool = False 

109 _plugin_doc_path: Traversable | Path | None = None 

110 

111 @property 

112 def is_bundled(self) -> bool: 

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

114 

115 @property 

116 def is_loaded(self) -> bool: 

117 return self.plugin_initializer is not None 

118 

119 @property 

120 def is_initialized(self) -> bool: 

121 return self._is_initialized 

122 

123 @property 

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

125 if not self._is_doc_path_resolved: 

126 self._plugin_doc_path = self.plugin_doc_path_resolver() 

127 self._is_doc_path_resolved = True 

128 return self._plugin_doc_path 

129 

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

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

132 raise RuntimeError("Cannot load plugins twice") 

133 if not self.is_loaded: 

134 self.load_plugin() 

135 plugin_initializer = self.plugin_initializer 

136 assert plugin_initializer is not None 

137 plugin_initializer(api) 

138 self._is_initialized = True 

139 

140 def load_plugin(self) -> None: 

141 plugin_loader = self.plugin_loader 

142 assert plugin_loader is not None 

143 try: 

144 self.plugin_initializer = run_in_context_of_plugin( 

145 self.plugin_name, 

146 plugin_loader, 

147 ) 

148 except PluginBaseError: 

149 raise 

150 except Exception as e: 

151 raise PluginInitializationError( 

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

153 ) from e 

154 assert self.plugin_initializer is not None 

155 

156 

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

158class PluginProvidedParser[PF, TP]: 

159 parser: "DeclarativeInputParser[PF]" 

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

161 plugin_metadata: DebputyPluginMetadata 

162 

163 def parse( 

164 self, 

165 name: str, 

166 value: object, 

167 attribute_path: AttributePath, 

168 *, 

169 parser_context: "ParserContextData", 

170 ) -> TP: 

171 parsed_value = self.parser.parse_input( 

172 value, 

173 attribute_path, 

174 parser_context=parser_context, 

175 ) 

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

177 

178 

179class PPFFormatParam(TypedDict): 

180 priority: int | None 

181 name: str 

182 owning_package: str 

183 

184 

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

186class PackagerProvidedFileClassSpec: 

187 debputy_plugin_metadata: DebputyPluginMetadata 

188 stem: str 

189 installed_as_format: str 

190 default_mode: int 

191 default_priority: int | None 

192 allow_name_segment: bool 

193 allow_architecture_segment: bool 

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

195 packageless_is_fallback_for_all_packages: bool 

196 package_types: PackageTypeSelector 

197 reservation_only: bool 

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

199 reference_documentation: PackagerProvidedFileReferenceDocumentation | None = None 

200 bug_950723: bool = False 

201 has_active_command: bool = True 

202 

203 @property 

204 def supports_priority(self) -> bool: 

205 return self.default_priority is not None 

206 

207 def compute_dest( 

208 self, 

209 assigned_name: str, 

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

211 *, 

212 owning_package: str | None = None, 

213 assigned_priority: int | None = None, 

214 path: VirtualPath | None = None, 

215 ) -> tuple[str, str]: 

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

217 raise ValueError( 

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

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

220 " do not use priority at all." 

221 ) 

222 

223 path_format = self.installed_as_format 

224 if self.supports_priority and assigned_priority is None: 

225 assigned_priority = self.default_priority 

226 

227 if owning_package is None: 

228 owning_package = assigned_name 

229 

230 params: PPFFormatParam = { 

231 "priority": assigned_priority, 

232 "name": assigned_name, 

233 "owning_package": owning_package, 

234 } 

235 

236 if self.formatting_callback is not None: 

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

238 raise ValueError( 

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

240 ) 

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

242 else: 

243 dest_path = path_format.format(**params) 

244 

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

246 dirname = _normalize_path(dirname) 

247 

248 if self.post_formatting_rewrite: 

249 basename = self.post_formatting_rewrite(basename) 

250 return dirname, basename 

251 

252 

253@dataclasses.dataclass(slots=True) 

254class MetadataOrMaintscriptDetector: 

255 plugin_metadata: DebputyPluginMetadata 

256 detector_id: str 

257 detector: MetadataAutoDetector 

258 applies_to_package_types: PackageTypeSelector 

259 enabled: bool = True 

260 

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

262 return binary_package.package_type in self.applies_to_package_types 

263 

264 def run_detector( 

265 self, 

266 fs_root: "VirtualPath", 

267 ctrl: "BinaryCtrlAccessor", 

268 context: "PackageProcessingContext", 

269 ) -> None: 

270 try: 

271 self.detector(fs_root, ctrl, context) 

272 except DebputyFSIsROError as e: 

273 nv = self.plugin_metadata.plugin_name 

274 raise PluginAPIViolationError( 

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

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

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

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

279 " would be lost)." 

280 ) from e 

281 except UnhandledOrUnexpectedErrorFromPluginError as e: 

282 e.add_note( 

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

284 ) 

285 

286 

287class DeclarativeInputParser[TD]: 

288 @property 

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

290 return None 

291 

292 @property 

293 def expected_debputy_integration_mode( 

294 self, 

295 ) -> Container[DebputyIntegrationMode] | None: 

296 return None 

297 

298 @property 

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

300 doc = self.inline_reference_documentation 

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

302 

303 def parse_input( 

304 self, 

305 value: object, 

306 path: AttributePath, 

307 *, 

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

309 ) -> TD: 

310 raise NotImplementedError 

311 

312 

313class DelegatingDeclarativeInputParser(DeclarativeInputParser[TD]): 

314 __slots__ = ( 

315 "delegate", 

316 "_reference_documentation", 

317 ) 

318 

319 def __init__( 

320 self, 

321 delegate: DeclarativeInputParser[TD], 

322 *, 

323 inline_reference_documentation: ParserDocumentation | None = None, 

324 ) -> None: 

325 self.delegate = delegate 

326 self._reference_documentation = inline_reference_documentation 

327 

328 @property 

329 def expected_debputy_integration_mode( 

330 self, 

331 ) -> Container[DebputyIntegrationMode] | None: 

332 return self.delegate.expected_debputy_integration_mode 

333 

334 @property 

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

336 doc = self._reference_documentation 

337 if doc is None: 

338 return self.delegate.inline_reference_documentation 

339 return doc 

340 

341 

342class AllowNoneDeclarativeInputParser(DelegatingDeclarativeInputParser[TD]): 

343 __slots__ = () 

344 

345 def parse_input( 

346 self, 

347 value: object | None, 

348 path: AttributePath, 

349 *, 

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

351 ) -> TD | None: 

352 check_integration_mode( 

353 path, 

354 parser_context, 

355 self.expected_debputy_integration_mode, 

356 ) 

357 if value is None: 

358 return None 

359 return self.delegate.parse_input( 

360 value, 

361 path, 

362 parser_context=parser_context, 

363 ) 

364 

365 

366class ListWrappedDeclarativeInputParser(DelegatingDeclarativeInputParser[TD]): 

367 __slots__ = ("_expected_debputy_integration_mode",) 

368 

369 def __init__( 

370 self, 

371 delegate: DeclarativeInputParser[TD], 

372 *, 

373 inline_reference_documentation: ParserDocumentation | None = None, 

374 expected_debputy_integration_mode: ( 

375 Container[DebputyIntegrationMode] | None 

376 ) = None, 

377 ) -> None: 

378 super().__init__( 

379 delegate, 

380 inline_reference_documentation=inline_reference_documentation, 

381 ) 

382 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

383 

384 @property 

385 def expected_debputy_integration_mode( 

386 self, 

387 ) -> Container[DebputyIntegrationMode] | None: 

388 if expected_debputy_integration_mode := self._expected_debputy_integration_mode: 

389 return expected_debputy_integration_mode 

390 return super().expected_debputy_integration_mode 

391 

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

393 doc_url = self.reference_documentation_url 

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

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

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

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

398 return "" 

399 

400 def parse_input( 

401 self, 

402 value: object, 

403 path: AttributePath, 

404 *, 

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

406 ) -> TD: 

407 check_integration_mode( 

408 path, parser_context, self.expected_debputy_integration_mode 

409 ) 

410 if not isinstance(value, list): 

411 doc_ref = self._doc_url_error_suffix(see_url_version=True) 

412 raise ManifestParseException( 

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

414 ) 

415 result = [] 

416 delegate = self.delegate 

417 for idx, element in enumerate(value): 

418 element_path = path[idx] 

419 result.append( 

420 delegate.parse_input( 

421 element, 

422 element_path, 

423 parser_context=parser_context, 

424 ) 

425 ) 

426 return result 

427 

428 

429class DispatchingParserBase[TP]: 

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

431 self.manifest_attribute_path_template = manifest_attribute_path_template 

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

433 

434 @property 

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

436 return "error" 

437 

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

439 return keyword in self._parsers 

440 

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

442 yield from self._parsers 

443 

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

445 return self._parsers[keyword] 

446 

447 def register_keyword( 

448 self, 

449 keyword: str | Sequence[str], 

450 handler: DIPKWHandler, 

451 plugin_metadata: DebputyPluginMetadata, 

452 *, 

453 inline_reference_documentation: ParserDocumentation | None = None, 

454 ) -> None: 

455 reference_documentation_url = None 

456 if inline_reference_documentation: 

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

458 raise ValueError( 

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

460 ) 

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

462 raise ValueError( 

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

464 ) 

465 reference_documentation_url = ( 

466 inline_reference_documentation.documentation_reference_url 

467 ) 

468 parser = DeclarativeValuelessKeywordInputParser( 

469 inline_reference_documentation, 

470 documentation_reference=reference_documentation_url, 

471 ) 

472 

473 def _combined_handler( 

474 name: str, 

475 _ignored: Any, 

476 attr_path: AttributePath, 

477 context: "ParserContextData", 

478 ) -> TP: 

479 return handler(name, attr_path, context) 

480 

481 p = PluginProvidedParser( 

482 parser, 

483 _combined_handler, 

484 plugin_metadata, 

485 ) 

486 

487 self._add_parser(keyword, p) 

488 

489 def register_parser( 

490 self, 

491 keyword: str | collections.abc.Sequence[str], 

492 parser: "DeclarativeInputParser[PF]", 

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

494 plugin_metadata: DebputyPluginMetadata, 

495 ) -> None: 

496 p = PluginProvidedParser( 

497 parser, 

498 handler, 

499 plugin_metadata, 

500 ) 

501 self._add_parser(keyword, p) 

502 

503 def _add_parser( 

504 self, 

505 keyword: str | Iterable[str], 

506 ppp: PluginProvidedParser[PF, TP], 

507 ) -> None: 

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

509 for k in ks: 

510 existing_parser = self._parsers.get(k) 

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

512 message = ( 

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

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

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

516 ) 

517 raise PluginConflictError( 

518 message, 

519 existing_parser.plugin_metadata, 

520 ppp.plugin_metadata, 

521 ) 

522 self._new_parser(k, ppp) 

523 

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

525 self._parsers[keyword] = ppp 

526 

527 def parse_input( 

528 self, 

529 orig_value: object, 

530 attribute_path: AttributePath, 

531 *, 

532 parser_context: "ParserContextData", 

533 ) -> TP: 

534 raise NotImplementedError 

535 

536 

537class DispatchingObjectParser( 

538 DispatchingParserBase[Mapping[str, Any]], 

539 DeclarativeInputParser[Mapping[str, Any]], 

540): 

541 def __init__( 

542 self, 

543 manifest_attribute_path_template: str, 

544 *, 

545 parser_documentation: ParserDocumentation | None = None, 

546 expected_debputy_integration_mode: None | ( 

547 Container[DebputyIntegrationMode] 

548 ) = None, 

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

550 allow_unknown_keys: bool = False, 

551 ) -> None: 

552 super().__init__(manifest_attribute_path_template) 

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

554 if parser_documentation is None: 

555 parser_documentation = reference_documentation() 

556 self._parser_documentation = parser_documentation 

557 self._expected_debputy_integration_mode = expected_debputy_integration_mode 

558 self._unknown_keys_diagnostic_severity = unknown_keys_diagnostic_severity 

559 self._allow_unknown_keys = allow_unknown_keys 

560 

561 @property 

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

563 return self._unknown_keys_diagnostic_severity 

564 

565 @property 

566 def expected_debputy_integration_mode( 

567 self, 

568 ) -> Container[DebputyIntegrationMode] | None: 

569 return self._expected_debputy_integration_mode 

570 

571 @property 

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

573 return self._parser_documentation.documentation_reference_url 

574 

575 @property 

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

577 ref_doc = self._parser_documentation 

578 return reference_documentation( 

579 title=ref_doc.title, 

580 description=ref_doc.description, 

581 attributes=self._attribute_documentation, 

582 reference_documentation_url=self.reference_documentation_url, 

583 ) 

584 

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

586 super()._new_parser(keyword, ppp) 

587 doc = ppp.parser.inline_reference_documentation 

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

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

590 else: 

591 self._attribute_documentation.append( 

592 documented_attr(keyword, doc.description) 

593 ) 

594 

595 def register_child_parser( 

596 self, 

597 keyword: str, 

598 parser: "DispatchingObjectParser", 

599 plugin_metadata: DebputyPluginMetadata, 

600 *, 

601 on_end_parse_step: None | ( 

602 Callable[ 

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

604 None, 

605 ] 

606 ) = None, 

607 nested_in_package_context: bool = False, 

608 ) -> None: 

609 def _handler( 

610 name: str, 

611 value: Mapping[str, Any], 

612 path: AttributePath, 

613 parser_context: "ParserContextData", 

614 ) -> Mapping[str, Any]: 

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

616 on_end_parse_step(name, value, path, parser_context) 

617 return value 

618 

619 if nested_in_package_context: 

620 parser = InPackageContextParser( 

621 keyword, 

622 parser, 

623 ) 

624 

625 p = PluginProvidedParser( 

626 parser, 

627 _handler, 

628 plugin_metadata, 

629 ) 

630 self._add_parser(keyword, p) 

631 

632 def parse_input( 

633 self, 

634 orig_value: object, 

635 attribute_path: AttributePath, 

636 *, 

637 parser_context: "ParserContextData", 

638 ) -> TP: 

639 check_integration_mode( 

640 attribute_path, 

641 parser_context, 

642 self.expected_debputy_integration_mode, 

643 ) 

644 doc_ref = "" 

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

646 doc_ref = ( 

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

648 ) 

649 if not isinstance(orig_value, dict): 

650 raise ManifestParseException( 

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

652 ) 

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

654 raise ManifestParseException( 

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

656 ) 

657 result = {} 

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

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

660 first_key = next(iter(unknown_keys)) 

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

662 if not remaining_valid_attributes: 

663 raise ManifestParseException( 

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

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

666 ) 

667 remaining_valid_attribute_names = ", ".join(remaining_valid_attributes) 

668 raise ManifestParseException( 

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

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

671 f" {remaining_valid_attribute_names}.{doc_ref}" 

672 ) 

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

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

675 value = orig_value.get(key) 

676 if value is None: 

677 if isinstance(provided_parser.parser, DispatchingObjectParser): 

678 provided_parser.handler( 

679 key, 

680 {}, 

681 attribute_path[key], 

682 parser_context, 

683 ) 

684 continue 

685 value_path = attribute_path[key] 

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

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

688 raise ManifestParseException( 

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

690 " Valid options at this location are:" 

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

692 ) 

693 parsed_value = provided_parser.parse( 

694 key, value, value_path, parser_context=parser_context 

695 ) 

696 result[key] = parsed_value 

697 return result 

698 

699 

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

701class PackageContextData[TP]: 

702 resolved_package_name: str 

703 value: TP 

704 

705 

706class InPackageContextParser( 

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

708): 

709 __slots__ = () 

710 

711 def __init__( 

712 self, 

713 manifest_attribute_path_template: str, 

714 delegate: DeclarativeInputParser[TP], 

715 *, 

716 parser_documentation: ParserDocumentation | None = None, 

717 ) -> None: 

718 self.manifest_attribute_path_template = manifest_attribute_path_template 

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

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

721 

722 def parse_input( 

723 self, 

724 orig_value: object, 

725 attribute_path: AttributePath, 

726 *, 

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

728 ) -> TP: 

729 assert parser_context is not None 

730 check_integration_mode( 

731 attribute_path, 

732 parser_context, 

733 self.expected_debputy_integration_mode, 

734 ) 

735 doc_ref = "" 

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

737 doc_ref = ( 

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

739 ) 

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

741 raise ManifestParseException( 

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

743 ) 

744 delegate = self.delegate 

745 result = {} 

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

747 

748 definition_source = attribute_path[package_name_raw] 

749 package_name = package_name_raw 

750 if "{{" in package_name: 

751 package_name = parser_context.substitution.substitute( 

752 package_name_raw, 

753 definition_source.path, 

754 ) 

755 package_state: PackageTransformationDefinition 

756 with parser_context.binary_package_context(package_name) as package_state: 

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

758 # Maybe lift (part) of this restriction. 

759 raise ManifestParseException( 

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

761 " auto-generated package." 

762 ) 

763 parsed_value = delegate.parse_input( 

764 value, definition_source, parser_context=parser_context 

765 ) 

766 result[package_name_raw] = PackageContextData( 

767 package_name, parsed_value 

768 ) 

769 return result 

770 

771 

772class DispatchingTableParser( 

773 DispatchingParserBase[TP], 

774 DeclarativeInputParser[TP], 

775): 

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

777 super().__init__(manifest_attribute_path_template) 

778 self.base_type = base_type 

779 

780 def parse_input( 

781 self, 

782 orig_value: object, 

783 attribute_path: AttributePath, 

784 *, 

785 parser_context: "ParserContextData", 

786 ) -> TP: 

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

788 key = orig_value 

789 value = None 

790 value_path = attribute_path 

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

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

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

794 raise ManifestParseException( 

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

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

797 f" possible keys are: {valid_keys}" 

798 ) 

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

800 value_path = attribute_path[key] 

801 else: 

802 raise ManifestParseException( 

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

804 ) 

805 provided_parser = self._parsers.get(key) 

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

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

808 raise ManifestParseException( 

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

810 " Valid actions at this location are:" 

811 f" {valid_keys}" 

812 ) 

813 return provided_parser.parse( 

814 key, value, value_path, parser_context=parser_context 

815 ) 

816 

817 

818@dataclasses.dataclass(slots=True) 

819class DeclarativeValuelessKeywordInputParser(DeclarativeInputParser[None]): 

820 inline_reference_documentation: ParserDocumentation | None = None 

821 documentation_reference: str | None = None 

822 

823 def parse_input( 

824 self, 

825 value: object, 

826 path: AttributePath, 

827 *, 

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

829 ) -> TD: 

830 if value is None: 

831 return cast("TD", value) 

832 if self.documentation_reference is not None: 

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

834 else: 

835 doc_ref = "" 

836 raise ManifestParseException( 

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

838 ) 

839 

840 

841@dataclasses.dataclass(slots=True) 

842class PluginProvidedManifestVariable: 

843 plugin_metadata: DebputyPluginMetadata 

844 variable_name: str 

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

846 is_context_specific_variable: bool 

847 variable_reference_documentation: str | None = None 

848 is_documentation_placeholder: bool = False 

849 is_for_special_case: bool = False 

850 

851 @property 

852 def is_internal(self) -> bool: 

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

854 

855 @property 

856 def is_token(self) -> bool: 

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

858 

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

860 value_resolver = self.variable_value 

861 if isinstance(value_resolver, str): 

862 res = value_resolver 

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

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

865 else: 

866 res = value_resolver(variable_context) 

867 return res 

868 

869 

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

871class AutomaticDiscardRuleExample: 

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

873 description: str | None = None 

874 

875 

876def automatic_discard_rule_example( 

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

878 example_description: str | None = None, 

879) -> AutomaticDiscardRuleExample: 

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

881 

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

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

884 part of a sequence of examples. 

885 

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

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

888 >>> # will be kept. 

889 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

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

892 ... ) 

893 AutomaticDiscardRuleExample(...) 

894 

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

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

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

898 

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

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

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

902 >>> automatic_discard_rule_example( # doctest: +ELLIPSIS 

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

904 ... ".../__pycache__/", 

905 ... ".../__pycache__/...", 

906 ... ".../foo.pyc", 

907 ... ".../foo.pyo", 

908 ... ) 

909 AutomaticDiscardRuleExample(...) 

910 

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

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

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

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

915 

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

917 

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

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

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

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

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

923 

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

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

926 verdict is assumed to be discarded (True). 

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

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

929 """ 

930 example = [] 

931 for d in content: 

932 if not isinstance(d, tuple): 

933 pd = d 

934 verdict = True 

935 else: 

936 pd, verdict = d 

937 

938 path_def = as_path_def(pd) 

939 example.append((path_def, verdict)) 

940 

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

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

943 

944 return AutomaticDiscardRuleExample( 

945 tuple(example), 

946 description=example_description, 

947 ) 

948 

949 

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

951class PluginProvidedPackageProcessor: 

952 processor_id: str 

953 applies_to_package_types: PackageTypeSelector 

954 package_processor: PackageProcessor 

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

956 plugin_metadata: DebputyPluginMetadata 

957 

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

959 return binary_package.package_type in self.applies_to_package_types 

960 

961 @property 

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

963 return self.plugin_metadata.plugin_name, self.processor_id 

964 

965 def run_package_processor( 

966 self, 

967 fs_root: "VirtualPath", 

968 unused: None, 

969 context: "PackageProcessingContext", 

970 ) -> None: 

971 self.package_processor(fs_root, unused, context) 

972 

973 

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

975class PluginProvidedDiscardRule: 

976 name: str 

977 plugin_metadata: DebputyPluginMetadata 

978 discard_check: Callable[[VirtualPath], bool] 

979 reference_documentation: str | None 

980 examples: Sequence[AutomaticDiscardRuleExample] = tuple() 

981 

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

983 return self.discard_check(path) 

984 

985 

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

987class ServiceManagerDetails: 

988 service_manager: str 

989 service_detector: "ServiceDetector" 

990 service_integrator: "ServiceIntegrator" 

991 plugin_metadata: DebputyPluginMetadata 

992 

993 

994class ReferenceValue(TypedDict): 

995 description: str 

996 

997 

998def _reference_data_value( 

999 *, 

1000 description: str, 

1001) -> ReferenceValue: 

1002 return { 

1003 "description": description, 

1004 } 

1005 

1006 

1007KnownPackagingFileCategories = Literal[ 

1008 "generated", 

1009 "generic-template", 

1010 "ppf-file", 

1011 "ppf-control-file", 

1012 "maint-config", 

1013 "pkg-metadata", 

1014 "pkg-helper-config", 

1015 "testing", 

1016 "lint-config", 

1017] 

1018KNOWN_PACKAGING_FILE_CATEGORY_DESCRIPTIONS: Mapping[ 

1019 KnownPackagingFileCategories, ReferenceValue 

1020] = { 

1021 "generated": _reference_data_value( 

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

1023 ), 

1024 "generic-template": _reference_data_value( 

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

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

1027 " language inside it." 

1028 ), 

1029 "ppf-file": _reference_data_value( 

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

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

1032 ), 

1033 "ppf-control-file": _reference_data_value( 

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

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

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

1037 ), 

1038 "maint-config": _reference_data_value( 

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

1040 ), 

1041 "pkg-metadata": _reference_data_value( 

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

1043 ), 

1044 "pkg-helper-config": _reference_data_value( 

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

1046 ), 

1047 "testing": _reference_data_value( 

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

1049 ), 

1050 "lint-config": _reference_data_value( 

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

1052 ), 

1053} 

1054 

1055KnownPackagingConfigFeature = Literal[ 

1056 "dh-filearray", 

1057 "dh-filedoublearray", 

1058 "dh-hash-subst", 

1059 "dh-dollar-subst", 

1060 "dh-glob", 

1061 "dh-partial-glob", 

1062 "dh-late-glob", 

1063 "dh-glob-after-execute", 

1064 "dh-executable-config", 

1065 "dh-custom-format", 

1066 "dh-file-list", 

1067 "dh-install-list", 

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

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

1070 "dh-fixed-dest-dir", 

1071 "dh-exec-rename", 

1072 "dh-docs-only", 

1073] 

1074 

1075KNOWN_PACKAGING_FILE_CONFIG_FEATURE_DESCRIPTION: Mapping[ 

1076 KnownPackagingConfigFeature, ReferenceValue 

1077] = { 

1078 "dh-filearray": _reference_data_value( 

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

1080 ), 

1081 "dh-filedoublearray": _reference_data_value( 

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

1083 ), 

1084 "dh-hash-subst": _reference_data_value( 

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

1086 ), 

1087 "dh-dollar-subst": _reference_data_value( 

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

1089 ), 

1090 "dh-glob": _reference_data_value( 

1091 description="Supports standard debhelper globing", 

1092 ), 

1093 "dh-partial-glob": _reference_data_value( 

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

1095 ), 

1096 "dh-late-glob": _reference_data_value( 

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

1098 ), 

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

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

1101 ), 

1102 "dh-executable-config": _reference_data_value( 

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

1104 ), 

1105 "dh-custom-format": _reference_data_value( 

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

1107 ), 

1108 "dh-file-list": _reference_data_value( 

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

1110 ), 

1111 "dh-install-list": _reference_data_value( 

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

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

1114 ), 

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

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

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

1118 ), 

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

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

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

1122 ), 

1123 "dh-exec-rename": _reference_data_value( 

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

1125 " requested/used", 

1126 ), 

1127 "dh-docs-only": _reference_data_value( 

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

1129 ), 

1130} 

1131 

1132CONFIG_FEATURE_ALIASES: dict[ 

1133 KnownPackagingConfigFeature, list[tuple[KnownPackagingConfigFeature, int]] 

1134] = { 

1135 "dh-filearray": [ 

1136 ("dh-filearray", 0), 

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

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

1139 ], 

1140 "dh-filedoublearray": [ 

1141 ("dh-filedoublearray", 0), 

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

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

1144 ], 

1145} 

1146 

1147 

1148def _implies( 

1149 features: list[KnownPackagingConfigFeature], 

1150 seen: set[KnownPackagingConfigFeature], 

1151 implying: Sequence[KnownPackagingConfigFeature], 

1152 implied: KnownPackagingConfigFeature, 

1153) -> None: 

1154 if implied in seen: 

1155 return 

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

1157 seen.add(implied) 

1158 features.append(implied) 

1159 

1160 

1161def expand_known_packaging_config_features( 

1162 compat_level: int, 

1163 features: list[KnownPackagingConfigFeature], 

1164) -> list[KnownPackagingConfigFeature]: 

1165 final_features: list[KnownPackagingConfigFeature] = [] 

1166 seen = set() 

1167 for feature in features: 

1168 expanded = CONFIG_FEATURE_ALIASES.get(feature) 

1169 if not expanded: 

1170 expanded = [(feature, 0)] 

1171 for v, c in expanded: 

1172 if compat_level < c or v in seen: 

1173 continue 

1174 seen.add(v) 

1175 final_features.append(v) 

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

1177 final_features.remove("dh-glob") 

1178 

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

1180 _implies( 

1181 final_features, 

1182 seen, 

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

1184 "dh-glob-after-execute", 

1185 ) 

1186 return sorted(final_features) 

1187 

1188 

1189class DHCompatibilityBasedRule(DebputyParsedContent): 

1190 install_pattern: NotRequired[str] 

1191 add_config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1192 starting_with_compat_level: NotRequired[int] 

1193 

1194 

1195class KnownPackagingFileInfo(DebputyParsedContent): 

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

1197 path: NotRequired[str] 

1198 pkgfile: NotRequired[str] 

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

1200 file_categories: NotRequired[list[KnownPackagingFileCategories]] 

1201 documentation_uris: NotRequired[list[str]] 

1202 debputy_cmd_templates: NotRequired[list[list[str]]] 

1203 debhelper_commands: NotRequired[list[str]] 

1204 config_features: NotRequired[list[KnownPackagingConfigFeature]] 

1205 install_pattern: NotRequired[str] 

1206 dh_compat_rules: NotRequired[list[DHCompatibilityBasedRule]] 

1207 default_priority: NotRequired[int] 

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

1209 packageless_is_fallback_for_all_packages: NotRequired[bool] 

1210 has_active_command: NotRequired[bool] 

1211 

1212 

1213@dataclasses.dataclass(slots=True) 

1214class PluginProvidedKnownPackagingFile: 

1215 info: KnownPackagingFileInfo 

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

1217 detection_value: str 

1218 plugin_metadata: DebputyPluginMetadata 

1219 

1220 

1221class BuildSystemAutoDetector(Protocol): 

1222 

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

1224 

1225 

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

1227class PluginProvidedTypeMapping: 

1228 mapped_type: TypeMapping[Any, Any] 

1229 reference_documentation: TypeMappingDocumentation | None 

1230 plugin_metadata: DebputyPluginMetadata 

1231 

1232 

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

1234class PluginProvidedBuildSystemAutoDetection[BSR]: 

1235 manifest_keyword: str 

1236 build_system_rule_type: type[BSR] 

1237 detector: BuildSystemAutoDetector 

1238 constructor: Callable[ 

1239 ["BuildRuleParsedFormat", AttributePath, "HighLevelManifest"], 

1240 BSR, 

1241 ] 

1242 auto_detection_shadow_build_systems: frozenset[str] 

1243 plugin_metadata: DebputyPluginMetadata 

1244 

1245 

1246class PackageDataTable: 

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

1248 self._package_data_table = package_data_table 

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

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

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

1252 self.enable_cross_package_checks = False 

1253 

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

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

1256 

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

1258 return self._package_data_table[item] 

1259 

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

1261 return item in self._package_data_table 

1262 

1263 

1264class PackageProcessingContextProvider(PackageProcessingContext): 

1265 __slots__ = ( 

1266 "_manifest", 

1267 "_binary_package", 

1268 "_related_udeb_package", 

1269 "_package_data_table", 

1270 "_cross_check_cache", 

1271 ) 

1272 

1273 def __init__( 

1274 self, 

1275 manifest: "HighLevelManifest", 

1276 binary_package: BinaryPackage, 

1277 related_udeb_package: BinaryPackage | None, 

1278 package_data_table: PackageDataTable, 

1279 ) -> None: 

1280 self._manifest = manifest 

1281 self._binary_package = binary_package 

1282 self._related_udeb_package = related_udeb_package 

1283 self._package_data_table = ref(package_data_table) 

1284 self._cross_check_cache: None | ( 

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

1286 ) = None 

1287 

1288 def _package_state_for( 

1289 self, 

1290 package: BinaryPackage, 

1291 ) -> "PackageTransformationDefinition": 

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

1293 

1294 def _package_version_for( 

1295 self, 

1296 package: BinaryPackage, 

1297 ) -> str: 

1298 package_state = self._package_state_for(package) 

1299 version = package_state.binary_version 

1300 if version is not None: 

1301 return version 

1302 return self._manifest.source_version( 

1303 include_binnmu_version=not package.is_arch_all 

1304 ) 

1305 

1306 @property 

1307 def source_package(self) -> SourcePackage: 

1308 return self._manifest.source_package 

1309 

1310 @property 

1311 def binary_package(self) -> BinaryPackage: 

1312 return self._binary_package 

1313 

1314 @property 

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

1316 return self._related_udeb_package 

1317 

1318 @property 

1319 def binary_package_version(self) -> str: 

1320 return self._package_version_for(self._binary_package) 

1321 

1322 @property 

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

1324 udeb = self._related_udeb_package 

1325 if udeb is None: 

1326 return None 

1327 return self._package_version_for(udeb) 

1328 

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

1330 package_table = self._package_data_table() 

1331 if package_table is None: 

1332 raise ReferenceError( 

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

1334 ) 

1335 if not package_table.enable_cross_package_checks: 

1336 raise PluginAPIViolationError( 

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

1338 ) 

1339 cache = self._cross_check_cache 

1340 if cache is None: 

1341 matches = [] 

1342 pkg = self.binary_package 

1343 for pkg_data in package_table: 

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

1345 continue 

1346 res = package_cross_check_precheck(pkg, pkg_data.binary_package) 

1347 if not res[0]: 

1348 continue 

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

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

1351 self._cross_check_cache = cache 

1352 return cache 

1353 

1354 def manifest_configuration[T]( 

1355 self, 

1356 context_package: SourcePackage | BinaryPackage, 

1357 value_type: type[T], 

1358 ) -> T | None: 

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

1360 

1361 @property 

1362 def dpkg_arch_query_table(self) -> DpkgArchTable: 

1363 return self._manifest.dpkg_arch_query_table 

1364 

1365 @property 

1366 def deb_options_and_profiles(self) -> DebBuildOptionsAndProfiles: 

1367 return self._manifest.deb_options_and_profiles 

1368 

1369 @property 

1370 def source_condition_context(self) -> ConditionContext: 

1371 return self._manifest.source_condition_context 

1372 

1373 def condition_context( 

1374 self, binary_package: BinaryPackage | None 

1375 ) -> ConditionContext: 

1376 return self._manifest.condition_context(binary_package) 

1377 

1378 

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

1380class PluginProvidedTrigger: 

1381 dpkg_trigger_type: DpkgTriggerType 

1382 dpkg_trigger_target: str 

1383 provider: DebputyPluginMetadata 

1384 provider_source_id: str 

1385 

1386 def serialized_format(self) -> str: 

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