Coverage for src/debputy/plugin/api/impl.py: 60%

900 statements  

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

1import contextlib 

2import dataclasses 

3import functools 

4import importlib 

5import importlib.resources 

6import importlib.util 

7import inspect 

8import itertools 

9import json 

10import os 

11import re 

12import subprocess 

13import sys 

14from abc import ABC 

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

16from importlib.resources.abc import Traversable 

17from io import IOBase 

18from json import JSONDecodeError 

19from pathlib import Path 

20from types import NoneType 

21from typing import ( 

22 IO, 

23 AbstractSet, 

24 cast, 

25 Any, 

26 Literal, 

27 TYPE_CHECKING, 

28 is_typeddict, 

29 AnyStr, 

30 overload, 

31) 

32 

33import debputy 

34from debputy.exceptions import ( 

35 DebputySubstitutionError, 

36 PluginConflictError, 

37 PluginMetadataError, 

38 PluginBaseError, 

39 PluginInitializationError, 

40 PluginAPIViolationError, 

41 PluginNotFoundError, 

42 PluginIncorrectRegistrationError, 

43) 

44from debputy.maintscript_snippet import ( 

45 DPKG_DEB_CONTROL_SCRIPTS, 

46 MaintscriptSnippetContainer, 

47 UnboundMaintscriptSnippet, 

48 SnippetResolver, 

49 SnippetAnchor, 

50 PackageMaintscriptSnippetContainer, 

51) 

52from debputy.manifest_parser.exceptions import ManifestParseException 

53from debputy.manifest_parser.parser_data import ParserContextData 

54from debputy.manifest_parser.tagging_types import TypeMapping 

55from debputy.manifest_parser.util import AttributePath 

56from debputy.plugin.api.doc_parsing import ( 

57 DEBPUTY_DOC_REFERENCE_DATA_PARSER, 

58 parser_type_name, 

59 DebputyParsedDoc, 

60) 

61from debputy.plugin.api.feature_set import PluginProvidedFeatureSet 

62from debputy.plugin.api.impl_types import ( 

63 DebputyPluginMetadata, 

64 PackagerProvidedFileClassSpec, 

65 MetadataOrMaintscriptDetector, 

66 PluginProvidedTrigger, 

67 TTP, 

68 DIPHandler, 

69 PF, 

70 SF, 

71 DIPKWHandler, 

72 PluginProvidedManifestVariable, 

73 PluginProvidedPackageProcessor, 

74 PluginProvidedDiscardRule, 

75 AutomaticDiscardRuleExample, 

76 PPFFormatParam, 

77 ServiceManagerDetails, 

78 KnownPackagingFileInfo, 

79 PluginProvidedKnownPackagingFile, 

80 DHCompatibilityBasedRule, 

81 PluginProvidedTypeMapping, 

82 PluginProvidedBuildSystemAutoDetection, 

83 BSR, 

84 TP, 

85) 

86from debputy.plugin.api.plugin_parser import ( 

87 PLUGIN_METADATA_PARSER, 

88 PluginJsonMetadata, 

89 PLUGIN_PPF_PARSER, 

90 PackagerProvidedFileJsonDescription, 

91 PLUGIN_MANIFEST_VARS_PARSER, 

92 PLUGIN_KNOWN_PACKAGING_FILES_PARSER, 

93) 

94from debputy.plugin.api.spec import ( 

95 MaintscriptAccessor, 

96 Maintscript, 

97 DpkgTriggerType, 

98 BinaryCtrlAccessor, 

99 PackageProcessingContext, 

100 MetadataAutoDetector, 

101 PluginInitializationEntryPoint, 

102 DebputyPluginInitializer, 

103 FlushableSubstvars, 

104 ParserDocumentation, 

105 PackageProcessor, 

106 VirtualPath, 

107 ServiceIntegrator, 

108 ServiceDetector, 

109 ServiceRegistry, 

110 ServiceDefinition, 

111 DSD, 

112 ServiceUpgradeRule, 

113 PackagerProvidedFileReferenceDocumentation, 

114 packager_provided_file_reference_documentation, 

115 TypeMappingDocumentation, 

116 DebputyIntegrationMode, 

117 _DEBPUTY_DISPATCH_METADATA_ATTR_NAME, 

118 BuildSystemManifestRuleMetadata, 

119 INTEGRATION_MODE_FULL, 

120 only_integrations, 

121 DebputyPluginDefinition, 

122) 

123from debputy.plugin.api.std_docs import _STD_ATTR_DOCS 

124from debputy.plugin.plugin_state import ( 

125 run_in_context_of_plugin, 

126 run_in_context_of_plugin_wrap_errors, 

127 wrap_plugin_code, 

128 register_manifest_type_value_in_context, 

129) 

130from debputy.plugins.debputy.to_be_api_types import ( 

131 BuildRuleParsedFormat, 

132 BSPF, 

133 debputy_build_system, 

134) 

135from debputy.substitution import ( 

136 Substitution, 

137 VariableNameState, 

138 SUBST_VAR_RE, 

139 VariableContext, 

140) 

141from debputy.util import ( 

142 _normalize_path, 

143 POSTINST_DEFAULT_CONDITION, 

144 _error, 

145 print_command, 

146 _warn, 

147 _debug_log, 

148 PackageTypeSelector, 

149) 

150from debputy.version import debputy_doc_root_dir 

151from debputy.yaml import MANIFEST_YAML 

152 

153if TYPE_CHECKING: 

154 from debputy.highlevel_manifest import HighLevelManifest 

155 

156PLUGIN_TEST_SUFFIX = re.compile(r"_(?:t|test|check)(?:_([a-z0-9_]+))?[.]py$") 

157PLUGIN_PYTHON_RES_PATH = importlib.resources.files(debputy.plugins.__name__) 

158 

159 

160def _validate_known_packaging_file_dh_compat_rules( 

161 dh_compat_rules: list[DHCompatibilityBasedRule] | None, 

162) -> None: 

163 max_compat = None 

164 if not dh_compat_rules: 164 ↛ 167line 164 didn't jump to line 167 because the condition on line 164 was always true

165 return 

166 dh_compat_rule: DHCompatibilityBasedRule 

167 for idx, dh_compat_rule in enumerate(dh_compat_rules): 

168 dh_version = dh_compat_rule.get("starting_with_debhelper_version") 

169 compat = dh_compat_rule.get("starting_with_compat_level") 

170 

171 remaining = dh_compat_rule.keys() - { 

172 "after_debhelper_version", 

173 "starting_with_compat_level", 

174 } 

175 if not remaining: 

176 raise ValueError( 

177 f"The dh compat-rule at index {idx} does not affect anything / not have any rules!? So why have it?" 

178 ) 

179 if dh_version is None and compat is None and idx < len(dh_compat_rules) - 1: 

180 raise ValueError( 

181 f"The dh compat-rule at index {idx} is not the last and is missing either" 

182 " before-debhelper-version or before-compat-level" 

183 ) 

184 if compat is not None and compat < 0: 

185 raise ValueError( 

186 f"There is no compat below 1 but dh compat-rule at {idx} wants to declare some rule" 

187 f" for something that appeared when migrating from {compat} to {compat + 1}." 

188 ) 

189 

190 if max_compat is None: 

191 max_compat = compat 

192 elif compat is not None: 

193 if compat >= max_compat: 

194 raise ValueError( 

195 f"The dh compat-rule at {idx} should be moved earlier than the entry for compat {max_compat}." 

196 ) 

197 max_compat = compat 

198 

199 install_pattern = dh_compat_rule.get("install_pattern") 

200 if ( 

201 install_pattern is not None 

202 and _normalize_path(install_pattern, with_prefix=False) != install_pattern 

203 ): 

204 raise ValueError( 

205 f"The install-pattern in dh compat-rule at {idx} must be normalized as" 

206 f' "{_normalize_path(install_pattern, with_prefix=False)}".' 

207 ) 

208 

209 

210class DebputyPluginInitializerProvider(DebputyPluginInitializer): 

211 __slots__ = ( 

212 "_plugin_metadata", 

213 "_feature_set", 

214 "_plugin_detector_ids", 

215 "_substitution", 

216 "_unloaders", 

217 "_is_doc_cache_resolved", 

218 "_doc_cache", 

219 "_registered_manifest_types", 

220 "_load_started", 

221 ) 

222 

223 def __init__( 

224 self, 

225 plugin_metadata: DebputyPluginMetadata, 

226 feature_set: PluginProvidedFeatureSet, 

227 substitution: Substitution, 

228 ) -> None: 

229 self._plugin_metadata: DebputyPluginMetadata = plugin_metadata 

230 self._feature_set = feature_set 

231 self._plugin_detector_ids: set[str] = set() 

232 self._substitution = substitution 

233 self._unloaders: list[Callable[[], None]] = [] 

234 self._is_doc_cache_resolved: bool = False 

235 self._doc_cache: DebputyParsedDoc | None = None 

236 self._registered_manifest_types: dict[type[Any], DebputyPluginMetadata] = {} 

237 self._load_started = False 

238 

239 @property 

240 def plugin_metadata(self) -> DebputyPluginMetadata: 

241 return self._plugin_metadata 

242 

243 def unload_plugin(self) -> None: 

244 if self._load_started: 

245 for unloader in self._unloaders: 

246 unloader() 

247 del self._feature_set.plugin_data[self._plugin_name] 

248 

249 def load_plugin(self) -> None: 

250 metadata = self._plugin_metadata 

251 if metadata.plugin_name in self._feature_set.plugin_data: 251 ↛ 252line 251 didn't jump to line 252 because the condition on line 251 was never true

252 raise PluginConflictError( 

253 f'The plugin "{metadata.plugin_name}" has already been loaded!?', 

254 metadata, 

255 metadata, 

256 ) 

257 assert ( 

258 metadata.api_compat_version == 1 

259 ), f"Unsupported plugin API compat version {metadata.api_compat_version}" 

260 self._feature_set.plugin_data[metadata.plugin_name] = metadata 

261 self._load_started = True 

262 assert not metadata.is_initialized 

263 try: 

264 metadata.initialize_plugin(self) 

265 except Exception as e: 

266 initializer = metadata.plugin_initializer 

267 if ( 267 ↛ 272line 267 didn't jump to line 272 because the condition on line 267 was never true

268 isinstance(e, TypeError) 

269 and initializer is not None 

270 and not callable(initializer) 

271 ): 

272 raise PluginMetadataError( 

273 f"The specified entry point for plugin {metadata.plugin_name} does not appear to be a" 

274 f" callable (callable returns False). The specified entry point identifies" 

275 f' itself as "{initializer.__qualname__}".' 

276 ) from e 

277 if isinstance(e, PluginBaseError): 277 ↛ 279line 277 didn't jump to line 279 because the condition on line 277 was always true

278 raise 

279 raise PluginInitializationError( 

280 f"Exception while attempting to load plugin {metadata.plugin_name}" 

281 ) from e 

282 

283 def _resolve_docs(self) -> DebputyParsedDoc | None: 

284 doc_cache = self._doc_cache 

285 if doc_cache is not None: 

286 return doc_cache 

287 

288 plugin_doc_path = self._plugin_metadata.plugin_doc_path 

289 if plugin_doc_path is None or self._is_doc_cache_resolved: 

290 self._is_doc_cache_resolved = True 

291 return None 

292 try: 

293 with plugin_doc_path.open("r", encoding="utf-8") as fd: 

294 raw = MANIFEST_YAML.load(fd) 

295 except FileNotFoundError: 

296 _debug_log( 

297 f"No documentation file found for {self._plugin_name}. Expected it at {plugin_doc_path}" 

298 ) 

299 self._is_doc_cache_resolved = True 

300 return None 

301 attr_path = AttributePath.root_path(plugin_doc_path) 

302 try: 

303 ref = DEBPUTY_DOC_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

304 except ManifestParseException as e: 

305 raise ValueError( 

306 f"Could not parse documentation in {plugin_doc_path}: {e.message}" 

307 ) from e 

308 try: 

309 res = DebputyParsedDoc.from_ref_data(ref) 

310 except ValueError as e: 

311 raise ValueError( 

312 f"Could not parse documentation in {plugin_doc_path}: {e.args[0]}" 

313 ) from e 

314 

315 self._doc_cache = res 

316 self._is_doc_cache_resolved = True 

317 return res 

318 

319 def _pluggable_manifest_docs_for( 

320 self, 

321 rule_type: TTP | str, 

322 rule_name: str | list[str], 

323 *, 

324 inline_reference_documentation: ParserDocumentation | None = None, 

325 ) -> ParserDocumentation | None: 

326 ref_data = self._resolve_docs() 

327 if ref_data is not None: 

328 primary_rule_name = ( 

329 rule_name if isinstance(rule_name, str) else rule_name[0] 

330 ) 

331 rule_ref = f"{parser_type_name(rule_type)}::{primary_rule_name}" 

332 resolved_docs = ref_data.pluggable_manifest_rules.get(rule_ref) 

333 if resolved_docs is not None: 

334 if inline_reference_documentation is not None: 334 ↛ 335line 334 didn't jump to line 335 because the condition on line 334 was never true

335 raise ValueError( 

336 f"Conflicting docs for {rule_ref}: Was provided one in the API call and one via" 

337 f" {self._plugin_metadata.plugin_doc_path}. Please remove one of the two, so" 

338 f" there is only one doc reference" 

339 ) 

340 return resolved_docs 

341 return inline_reference_documentation 

342 

343 def packager_provided_file( 

344 self, 

345 stem: str, 

346 installed_path: str, 

347 *, 

348 default_mode: int = 0o0644, 

349 default_priority: int | None = None, 

350 allow_name_segment: bool = True, 

351 allow_architecture_segment: bool = False, 

352 post_formatting_rewrite: Callable[[str], str] | None = None, 

353 packageless_is_fallback_for_all_packages: bool = False, 

354 package_types: PackageTypeSelector = PackageTypeSelector.ALL, 

355 reservation_only: bool = False, 

356 format_callback: None | ( 

357 Callable[[str, PPFFormatParam, VirtualPath], str] 

358 ) = None, 

359 reference_documentation: None | ( 

360 PackagerProvidedFileReferenceDocumentation 

361 ) = None, 

362 ) -> None: 

363 packager_provided_files = self._feature_set.packager_provided_files 

364 existing = packager_provided_files.get(stem) 

365 

366 if format_callback is not None and self._plugin_name != "debputy": 366 ↛ 367line 366 didn't jump to line 367 because the condition on line 366 was never true

367 raise ValueError( 

368 "Sorry; Using format_callback is a debputy-internal" 

369 f" API. Triggered by plugin {self._plugin_name}" 

370 ) 

371 

372 if installed_path.endswith("/"): 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true

373 raise ValueError( 

374 f'The installed_path ends with "/" indicating it is a directory, but it must be a file.' 

375 f" Triggered by plugin {self._plugin_name}." 

376 ) 

377 

378 installed_path = _normalize_path(installed_path) 

379 

380 has_name_var = "{name}" in installed_path 

381 

382 if installed_path.startswith("./DEBIAN") or reservation_only: 

383 # Special-case, used for control files. 

384 if self._plugin_name != "debputy": 384 ↛ 385line 384 didn't jump to line 385 because the condition on line 384 was never true

385 raise ValueError( 

386 "Sorry; Using DEBIAN as install path or/and reservation_only is a debputy-internal" 

387 f" API. Triggered by plugin {self._plugin_name}" 

388 ) 

389 elif not has_name_var and "{owning_package}" not in installed_path: 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

390 raise ValueError( 

391 'The installed_path must contain a "{name}" (preferred) or a "{owning_package}"' 

392 " substitution (or have installed_path end with a slash). Otherwise, the installed" 

393 f" path would caused file-conflicts. Triggered by plugin {self._plugin_name}" 

394 ) 

395 

396 if allow_name_segment and not has_name_var: 396 ↛ 397line 396 didn't jump to line 397 because the condition on line 396 was never true

397 raise ValueError( 

398 'When allow_name_segment is True, the installed_path must have a "{name}" substitution' 

399 " variable. Otherwise, the name segment will not work properly. Triggered by" 

400 f" plugin {self._plugin_name}" 

401 ) 

402 

403 if ( 403 ↛ 408line 403 didn't jump to line 408 because the condition on line 403 was never true

404 default_priority is not None 

405 and "{priority}" not in installed_path 

406 and "{priority:02}" not in installed_path 

407 ): 

408 raise ValueError( 

409 'When default_priority is not None, the installed_path should have a "{priority}"' 

410 ' or a "{priority:02}" substitution variable. Otherwise, the priority would be lost.' 

411 f" Triggered by plugin {self._plugin_name}" 

412 ) 

413 

414 if existing is not None: 

415 if existing.debputy_plugin_metadata.plugin_name != self._plugin_name: 415 ↛ 422line 415 didn't jump to line 422 because the condition on line 415 was always true

416 message = ( 

417 f'The stem "{stem}" is registered twice for packager provided files.' 

418 f" Once by {existing.debputy_plugin_metadata.plugin_name} and once" 

419 f" by {self._plugin_name}" 

420 ) 

421 else: 

422 message = ( 

423 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

424 f' stem "{stem}" twice for packager provided files.' 

425 ) 

426 raise PluginConflictError( 

427 message, existing.debputy_plugin_metadata, self._plugin_metadata 

428 ) 

429 packager_provided_files[stem] = PackagerProvidedFileClassSpec( 

430 self._plugin_metadata, 

431 stem, 

432 installed_path, 

433 default_mode=default_mode, 

434 default_priority=default_priority, 

435 allow_name_segment=allow_name_segment, 

436 allow_architecture_segment=allow_architecture_segment, 

437 post_formatting_rewrite=post_formatting_rewrite, 

438 packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages, 

439 package_types=package_types, 

440 reservation_only=reservation_only, 

441 formatting_callback=format_callback, 

442 reference_documentation=reference_documentation, 

443 ) 

444 

445 def _unload() -> None: 

446 del packager_provided_files[stem] 

447 

448 self._unloaders.append(_unload) 

449 

450 def metadata_or_maintscript_detector( 

451 self, 

452 auto_detector_id: str, 

453 auto_detector: MetadataAutoDetector, 

454 *, 

455 package_types: PackageTypeSelector = PackageTypeSelector.DEB, 

456 ) -> None: 

457 if auto_detector_id in self._plugin_detector_ids: 457 ↛ 458line 457 didn't jump to line 458 because the condition on line 457 was never true

458 raise ValueError( 

459 f"The plugin {self._plugin_name} tried to register" 

460 f' "{auto_detector_id}" twice' 

461 ) 

462 self._plugin_detector_ids.add(auto_detector_id) 

463 all_detectors = self._feature_set.metadata_maintscript_detectors 

464 if self._plugin_name not in all_detectors: 

465 all_detectors[self._plugin_name] = [] 

466 all_detectors[self._plugin_name].append( 

467 MetadataOrMaintscriptDetector( 

468 detector_id=auto_detector_id, 

469 detector=wrap_plugin_code(self._plugin_name, auto_detector), 

470 plugin_metadata=self._plugin_metadata, 

471 applies_to_package_types=package_types, 

472 enabled=True, 

473 ) 

474 ) 

475 

476 def _unload() -> None: 

477 if self._plugin_name in all_detectors: 

478 del all_detectors[self._plugin_name] 

479 

480 self._unloaders.append(_unload) 

481 

482 def document_builtin_variable( 

483 self, 

484 variable_name: str, 

485 variable_reference_documentation: str, 

486 *, 

487 is_context_specific: bool = False, 

488 is_for_special_case: bool = False, 

489 ) -> None: 

490 manifest_variables = self._feature_set.manifest_variables 

491 self._restricted_api() 

492 state = self._substitution.variable_state(variable_name) 

493 if state == VariableNameState.UNDEFINED: 493 ↛ 494line 493 didn't jump to line 494 because the condition on line 493 was never true

494 raise ValueError( 

495 f"The plugin {self._plugin_name} attempted to document built-in {variable_name}," 

496 f" but it is not known to be a variable" 

497 ) 

498 

499 assert variable_name not in manifest_variables 

500 

501 manifest_variables[variable_name] = PluginProvidedManifestVariable( 

502 self._plugin_metadata, 

503 variable_name, 

504 None, 

505 is_context_specific_variable=is_context_specific, 

506 variable_reference_documentation=variable_reference_documentation, 

507 is_documentation_placeholder=True, 

508 is_for_special_case=is_for_special_case, 

509 ) 

510 

511 def _unload() -> None: 

512 del manifest_variables[variable_name] 

513 

514 self._unloaders.append(_unload) 

515 

516 def manifest_variable_provider( 

517 self, 

518 provider: Callable[[VariableContext], Mapping[str, str]], 

519 variables: Sequence[str] | Mapping[str, str | None], 

520 ) -> None: 

521 self._restricted_api() 

522 cached_provider = functools.lru_cache(None)(provider) 

523 permitted_variables = frozenset(variables) 

524 variables_iter: Iterable[tuple[str, str | None]] 

525 if not isinstance(variables, Mapping): 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true

526 variables_iter = zip(variables, itertools.repeat(None)) 

527 else: 

528 variables_iter = variables.items() 

529 

530 checked_vars = False 

531 manifest_variables = self._feature_set.manifest_variables 

532 plugin_name = self._plugin_name 

533 

534 def _value_resolver_generator( 

535 variable_name: str, 

536 ) -> Callable[[VariableContext], str]: 

537 def _value_resolver(variable_context: VariableContext) -> str: 

538 res = cached_provider(variable_context) 

539 nonlocal checked_vars 

540 if not checked_vars: 540 ↛ 551line 540 didn't jump to line 551 because the condition on line 540 was always true

541 if permitted_variables != res.keys(): 541 ↛ 542line 541 didn't jump to line 542 because the condition on line 541 was never true

542 expected = ", ".join(sorted(permitted_variables)) 

543 actual = ", ".join(sorted(res)) 

544 raise PluginAPIViolationError( 

545 f"The plugin {plugin_name} claimed to provide" 

546 f" the following variables {expected}," 

547 f" but when resolving the variables, the plugin provided" 

548 f" {actual}. These two lists should have been the same." 

549 ) 

550 checked_vars = False 

551 return res[variable_name] 

552 

553 return _value_resolver 

554 

555 for varname, vardoc in variables_iter: 

556 self._check_variable_name(varname) 

557 manifest_variables[varname] = PluginProvidedManifestVariable( 

558 self._plugin_metadata, 

559 varname, 

560 _value_resolver_generator(varname), 

561 is_context_specific_variable=False, 

562 variable_reference_documentation=vardoc, 

563 ) 

564 

565 def _unload() -> None: 

566 raise PluginInitializationError( 

567 "Cannot unload manifest_variable_provider (not implemented)" 

568 ) 

569 

570 self._unloaders.append(_unload) 

571 

572 def _check_variable_name(self, variable_name: str) -> None: 

573 manifest_variables = self._feature_set.manifest_variables 

574 existing = manifest_variables.get(variable_name) 

575 

576 if existing is not None: 

577 if existing.plugin_metadata.plugin_name == self._plugin_name: 577 ↛ 583line 577 didn't jump to line 583 because the condition on line 577 was always true

578 message = ( 

579 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

580 f' manifest variable "{variable_name}" twice.' 

581 ) 

582 else: 

583 message = ( 

584 f"The plugins {existing.plugin_metadata.plugin_name} and {self._plugin_name}" 

585 f" both tried to provide the manifest variable {variable_name}" 

586 ) 

587 raise PluginConflictError( 

588 message, existing.plugin_metadata, self._plugin_metadata 

589 ) 

590 if not SUBST_VAR_RE.match("{{" + variable_name + "}}"): 

591 raise ValueError( 

592 f"The plugin {self._plugin_name} attempted to declare {variable_name}," 

593 f" which is not a valid variable name" 

594 ) 

595 

596 namespace = "" 

597 variable_basename = variable_name 

598 if ":" in variable_name: 

599 namespace, variable_basename = variable_name.rsplit(":", 1) 

600 assert namespace != "" 

601 assert variable_name != "" 

602 

603 if namespace != "" and namespace not in ("token", "path"): 

604 raise ValueError( 

605 f"The plugin {self._plugin_name} attempted to declare {variable_name}," 

606 f" which is in the reserved namespace {namespace}" 

607 ) 

608 

609 variable_name_upper = variable_name.upper() 

610 if ( 

611 variable_name_upper.startswith(("DEB_", "DPKG_", "DEBPUTY")) 

612 or variable_basename.startswith("_") 

613 or variable_basename.upper().startswith("DEBPUTY") 

614 ) and self._plugin_name != "debputy": 

615 raise ValueError( 

616 f"The plugin {self._plugin_name} attempted to declare {variable_name}," 

617 f" which is a variable name reserved by debputy" 

618 ) 

619 

620 state = self._substitution.variable_state(variable_name) 

621 if state != VariableNameState.UNDEFINED and self._plugin_name != "debputy": 

622 raise ValueError( 

623 f"The plugin {self._plugin_name} attempted to declare {variable_name}," 

624 f" which would shadow a built-in variable" 

625 ) 

626 

627 def package_processor( 

628 self, 

629 processor_id: str, 

630 processor: PackageProcessor, 

631 *, 

632 depends_on_processor: Iterable[str] = (), 

633 package_types: PackageTypeSelector = PackageTypeSelector.DEB, 

634 ) -> None: 

635 self._restricted_api(allowed_plugins={"lua", "debputy-self-hosting"}) 

636 package_processors = self._feature_set.all_package_processors 

637 dependencies = set() 

638 processor_key = (self._plugin_name, processor_id) 

639 

640 if processor_key in package_processors: 640 ↛ 641line 640 didn't jump to line 641 because the condition on line 640 was never true

641 raise PluginConflictError( 

642 f"The plugin {self._plugin_name} already registered a processor with id {processor_id}", 

643 self._plugin_metadata, 

644 self._plugin_metadata, 

645 ) 

646 

647 for depends_ref in depends_on_processor: 

648 if isinstance(depends_ref, str): 648 ↛ 662line 648 didn't jump to line 662 because the condition on line 648 was always true

649 if (self._plugin_name, depends_ref) in package_processors: 649 ↛ 651line 649 didn't jump to line 651 because the condition on line 649 was always true

650 depends_key = (self._plugin_name, depends_ref) 

651 elif ("debputy", depends_ref) in package_processors: 

652 depends_key = ("debputy", depends_ref) 

653 else: 

654 raise ValueError( 

655 f'Could not resolve dependency "{depends_ref}" for' 

656 f' "{processor_id}". It was not provided by the plugin itself' 

657 f" ({self._plugin_name}) nor debputy." 

658 ) 

659 else: 

660 # TODO: Add proper dependencies first, at which point we should probably resolve "name" 

661 # via the direct dependencies. 

662 assert False 

663 

664 existing_processor = package_processors.get(depends_key) 

665 if existing_processor is None: 665 ↛ 668line 665 didn't jump to line 668 because the condition on line 665 was never true

666 # We currently require the processor to be declared already. If this ever changes, 

667 # PluginProvidedFeatureSet.package_processors_in_order will need an update 

668 dplugin_name, dprocessor_name = depends_key 

669 available_processors = ", ".join( 

670 n for p, n in package_processors.keys() if p == dplugin_name 

671 ) 

672 raise ValueError( 

673 f"The plugin {dplugin_name} does not provide a processor called" 

674 f" {dprocessor_name}. Available processors for that plugin are:" 

675 f" {available_processors}" 

676 ) 

677 dependencies.add(depends_key) 

678 

679 package_processors[processor_key] = PluginProvidedPackageProcessor( 

680 processor_id, 

681 package_types, 

682 wrap_plugin_code(self._plugin_name, processor), 

683 frozenset(dependencies), 

684 self._plugin_metadata, 

685 ) 

686 

687 def _unload() -> None: 

688 del package_processors[processor_key] 

689 

690 self._unloaders.append(_unload) 

691 

692 def automatic_discard_rule( 

693 self, 

694 name: str, 

695 should_discard: Callable[[VirtualPath], bool], 

696 *, 

697 rule_reference_documentation: str | None = None, 

698 examples: ( 

699 AutomaticDiscardRuleExample | Sequence[AutomaticDiscardRuleExample] 

700 ) = (), 

701 ) -> None: 

702 """Register an automatic discard rule 

703 

704 An automatic discard rule is basically applied to *every* path about to be installed in to any package. 

705 If any discard rule concludes that a path should not be installed, then the path is not installed. 

706 In the case where the discard path is a: 

707 

708 * directory: Then the entire directory is excluded along with anything beneath it. 

709 * symlink: Then the symlink itself (but not its target) is excluded. 

710 * hardlink: Then the current hardlink will not be installed, but other instances of it will be. 

711 

712 Note: Discarded files are *never* deleted by `debputy`. They just make `debputy` skip the file. 

713 

714 Automatic discard rules should be written with the assumption that directories will be tested 

715 before their content *when it is relevant* for the discard rule to examine whether the directory 

716 can be excluded. 

717 

718 The packager can via the manifest overrule automatic discard rules by explicitly listing the path 

719 without any globs. As example: 

720 

721 installations: 

722 - install: 

723 sources: 

724 - usr/lib/libfoo.la # <-- This path is always installed 

725 # (Discard rules are never asked in this case) 

726 # 

727 - usr/lib/*.so* # <-- Discard rules applies to any path beneath usr/lib and can exclude matches 

728 # Though, they will not examine `libfoo.la` as it has already been installed 

729 # 

730 # Note: usr/lib itself is never tested in this case (it is assumed to be 

731 # explicitly requested). But any subdir of usr/lib will be examined. 

732 

733 When an automatic discard rule is evaluated, it can see the source path currently being considered 

734 for installation. While it can look at "surrounding" context (like parent directory), it will not 

735 know whether those paths are to be installed or will be installed. 

736 

737 :param name: A user visible name discard rule. It can be used on the command line, so avoid shell 

738 metacharacters and spaces. 

739 :param should_discard: A callable that is the implementation of the automatic discard rule. It will receive 

740 a VirtualPath representing the *source* path about to be installed. If callable returns `True`, then the 

741 path is discarded. If it returns `False`, the path is not discarded (by this rule at least). 

742 A source path will either be from the root of the source tree or the root of a search directory such as 

743 `debian/tmp`. Where the path will be installed is not available at the time the discard rule is 

744 evaluated. 

745 :param rule_reference_documentation: Optionally, the reference documentation to be shown when a user 

746 looks up this automatic discard rule. 

747 :param examples: Provide examples for the rule. Use the automatic_discard_rule_example function to 

748 generate the examples. 

749 

750 """ 

751 self._restricted_api() 

752 auto_discard_rules = self._feature_set.auto_discard_rules 

753 existing = auto_discard_rules.get(name) 

754 if existing is not None: 754 ↛ 755line 754 didn't jump to line 755 because the condition on line 754 was never true

755 if existing.plugin_metadata.plugin_name == self._plugin_name: 

756 message = ( 

757 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

758 f' automatic discard rule "{name}" twice.' 

759 ) 

760 else: 

761 message = ( 

762 f"The plugins {existing.plugin_metadata.plugin_name} and {self._plugin_name}" 

763 f" both tried to provide the automatic discard rule {name}" 

764 ) 

765 raise PluginConflictError( 

766 message, existing.plugin_metadata, self._plugin_metadata 

767 ) 

768 examples = ( 

769 (examples,) 

770 if isinstance(examples, AutomaticDiscardRuleExample) 

771 else tuple(examples) 

772 ) 

773 auto_discard_rules[name] = PluginProvidedDiscardRule( 

774 name, 

775 self._plugin_metadata, 

776 should_discard, 

777 rule_reference_documentation, 

778 examples, 

779 ) 

780 

781 def _unload() -> None: 

782 del auto_discard_rules[name] 

783 

784 self._unloaders.append(_unload) 

785 

786 def service_provider( 

787 self, 

788 service_manager: str, 

789 detector: ServiceDetector, 

790 integrator: ServiceIntegrator, 

791 ) -> None: 

792 self._restricted_api() 

793 service_managers = self._feature_set.service_managers 

794 existing = service_managers.get(service_manager) 

795 if existing is not None: 795 ↛ 796line 795 didn't jump to line 796 because the condition on line 795 was never true

796 if existing.plugin_metadata.plugin_name == self._plugin_name: 

797 message = ( 

798 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

799 f' service manager "{service_manager}" twice.' 

800 ) 

801 else: 

802 message = ( 

803 f"The plugins {existing.plugin_metadata.plugin_name} and {self._plugin_name}" 

804 f' both tried to provide the service manager "{service_manager}"' 

805 ) 

806 raise PluginConflictError( 

807 message, existing.plugin_metadata, self._plugin_metadata 

808 ) 

809 service_managers[service_manager] = ServiceManagerDetails( 

810 service_manager, 

811 wrap_plugin_code(self._plugin_name, detector), 

812 wrap_plugin_code(self._plugin_name, integrator), 

813 self._plugin_metadata, 

814 ) 

815 

816 def _unload() -> None: 

817 del service_managers[service_manager] 

818 

819 self._unloaders.append(_unload) 

820 

821 def manifest_variable( 

822 self, 

823 variable_name: str, 

824 value: str, 

825 *, 

826 variable_reference_documentation: str | None = None, 

827 ) -> None: 

828 self._check_variable_name(variable_name) 

829 manifest_variables = self._feature_set.manifest_variables 

830 try: 

831 resolved_value = self._substitution.substitute( 

832 value, "Plugin initialization" 

833 ) 

834 depends_on_variable = resolved_value != value 

835 except DebputySubstitutionError: 

836 depends_on_variable = True 

837 if depends_on_variable: 

838 raise ValueError( 

839 f"The plugin {self._plugin_name} attempted to declare {variable_name} with value {value!r}." 

840 f" This value depends on another variable, which is not supported. This restriction may be" 

841 f" lifted in the future." 

842 ) 

843 

844 manifest_variables[variable_name] = PluginProvidedManifestVariable( 

845 self._plugin_metadata, 

846 variable_name, 

847 value, 

848 is_context_specific_variable=False, 

849 variable_reference_documentation=variable_reference_documentation, 

850 ) 

851 

852 def _unload() -> None: 

853 # We need to check it was never resolved 

854 raise PluginInitializationError( 

855 "Cannot unload manifest_variable (not implemented)" 

856 ) 

857 

858 self._unloaders.append(_unload) 

859 

860 @property 

861 def _plugin_name(self) -> str: 

862 return self._plugin_metadata.plugin_name 

863 

864 def provide_manifest_keyword( 

865 self, 

866 rule_type: TTP, 

867 rule_name: str | list[str], 

868 handler: DIPKWHandler, 

869 *, 

870 inline_reference_documentation: ParserDocumentation | None = None, 

871 ) -> None: 

872 self._restricted_api() 

873 parser_generator = self._feature_set.manifest_parser_generator 

874 if rule_type not in parser_generator.dispatchable_table_parsers: 874 ↛ 875line 874 didn't jump to line 875 because the condition on line 874 was never true

875 types = ", ".join( 

876 sorted(x.__name__ for x in parser_generator.dispatchable_table_parsers) 

877 ) 

878 raise ValueError( 

879 f"The rule_type was not a supported type. It must be one of {types}" 

880 ) 

881 

882 inline_reference_documentation = self._pluggable_manifest_docs_for( 

883 rule_type, 

884 rule_name, 

885 inline_reference_documentation=inline_reference_documentation, 

886 ) 

887 

888 dispatching_parser = parser_generator.dispatchable_table_parsers[rule_type] 

889 dispatching_parser.register_keyword( 

890 rule_name, 

891 wrap_plugin_code(self._plugin_name, handler), 

892 self._plugin_metadata, 

893 inline_reference_documentation=inline_reference_documentation, 

894 ) 

895 

896 def _unload() -> None: 

897 raise PluginInitializationError( 

898 "Cannot unload provide_manifest_keyword (not implemented)" 

899 ) 

900 

901 self._unloaders.append(_unload) 

902 

903 def pluggable_object_parser( 

904 self, 

905 rule_type: str, 

906 rule_name: str, 

907 *, 

908 object_parser_key: str | None = None, 

909 on_end_parse_step: None | ( 

910 Callable[ 

911 [str, Mapping[str, Any] | None, AttributePath, ParserContextData], 

912 None, 

913 ] 

914 ) = None, 

915 nested_in_package_context: bool = False, 

916 ) -> None: 

917 self._restricted_api() 

918 if object_parser_key is None: 918 ↛ 919line 918 didn't jump to line 919 because the condition on line 918 was never true

919 object_parser_key = rule_name 

920 

921 parser_generator = self._feature_set.manifest_parser_generator 

922 dispatchable_object_parsers = parser_generator.dispatchable_object_parsers 

923 if rule_type not in dispatchable_object_parsers: 923 ↛ 924line 923 didn't jump to line 924 because the condition on line 923 was never true

924 types = ", ".join(sorted(dispatchable_object_parsers)) 

925 raise ValueError( 

926 f"The rule_type was not a supported type. It must be one of {types}" 

927 ) 

928 if object_parser_key not in dispatchable_object_parsers: 928 ↛ 929line 928 didn't jump to line 929 because the condition on line 928 was never true

929 types = ", ".join(sorted(dispatchable_object_parsers)) 

930 raise ValueError( 

931 f"The object_parser_key was not a supported type. It must be one of {types}" 

932 ) 

933 parent_dispatcher = dispatchable_object_parsers[rule_type] 

934 child_dispatcher = dispatchable_object_parsers[object_parser_key] 

935 

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

937 on_end_parse_step = wrap_plugin_code(self._plugin_name, on_end_parse_step) 

938 

939 parent_dispatcher.register_child_parser( 

940 rule_name, 

941 child_dispatcher, 

942 self._plugin_metadata, 

943 on_end_parse_step=on_end_parse_step, 

944 nested_in_package_context=nested_in_package_context, 

945 ) 

946 

947 def _unload() -> None: 

948 raise PluginInitializationError( 

949 "Cannot unload pluggable_object_parser (not implemented)" 

950 ) 

951 

952 self._unloaders.append(_unload) 

953 

954 def pluggable_manifest_rule( 

955 self, 

956 rule_type: TTP | str, 

957 rule_name: str | Sequence[str], 

958 parsed_format: type[PF], 

959 handler: DIPHandler, 

960 *, 

961 as_keyword_handler: DIPKWHandler | None = None, 

962 source_format: SF | None = None, 

963 inline_reference_documentation: ParserDocumentation | None = None, 

964 expected_debputy_integration_mode: None | ( 

965 Container[DebputyIntegrationMode] 

966 ) = None, 

967 apply_standard_attribute_documentation: bool = False, 

968 register_value: bool = True, 

969 ) -> None: 

970 # When changing this, consider which types will be unrestricted 

971 self._restricted_api() 

972 if apply_standard_attribute_documentation and sys.version_info < (3, 12): 972 ↛ 973line 972 didn't jump to line 973 because the condition on line 972 was never true

973 _error( 

974 f"The plugin {self._plugin_metadata.plugin_name} requires python 3.12 due to" 

975 f" its use of apply_standard_attribute_documentation" 

976 ) 

977 feature_set = self._feature_set 

978 parser_generator = feature_set.manifest_parser_generator 

979 if isinstance(rule_type, str): 

980 if rule_type not in parser_generator.dispatchable_object_parsers: 980 ↛ 981line 980 didn't jump to line 981 because the condition on line 980 was never true

981 types = ", ".join(sorted(parser_generator.dispatchable_object_parsers)) 

982 raise ValueError( 

983 f"The rule_type was not a supported type. It must be one of {types}" 

984 ) 

985 dispatching_parser = parser_generator.dispatchable_object_parsers[rule_type] 

986 signature = inspect.signature(handler) 

987 if ( 987 ↛ 991line 987 didn't jump to line 991 because the condition on line 987 was never true

988 signature.return_annotation is signature.empty 

989 or signature.return_annotation == NoneType 

990 ): 

991 raise ValueError( 

992 "The handler must have a return type (that is not None)" 

993 ) 

994 register_as_type = signature.return_annotation 

995 else: 

996 # Dispatchable types cannot be resolved 

997 register_as_type = None 

998 if rule_type not in parser_generator.dispatchable_table_parsers: 998 ↛ 999line 998 didn't jump to line 999 because the condition on line 998 was never true

999 types = ", ".join( 

1000 sorted( 

1001 x.__name__ for x in parser_generator.dispatchable_table_parsers 

1002 ) 

1003 ) 

1004 raise ValueError( 

1005 f"The rule_type was not a supported type. It must be one of {types}" 

1006 ) 

1007 dispatching_parser = parser_generator.dispatchable_table_parsers[rule_type] 

1008 

1009 if register_as_type is not None and not register_value: 

1010 register_as_type = None 

1011 

1012 if register_as_type is not None: 

1013 existing_registration = self._registered_manifest_types.get( 

1014 register_as_type 

1015 ) 

1016 if existing_registration is not None: 1016 ↛ 1017line 1016 didn't jump to line 1017 because the condition on line 1016 was never true

1017 raise ValueError( 

1018 f"Cannot register rule {rule_name!r} for plugin {self._plugin_name}. The plugin {existing_registration.plugin_name} already registered a manifest rule with type {register_as_type!r}" 

1019 ) 

1020 self._registered_manifest_types[register_as_type] = self._plugin_metadata 

1021 

1022 inline_reference_documentation = self._pluggable_manifest_docs_for( 

1023 rule_type, 

1024 rule_name, 

1025 inline_reference_documentation=inline_reference_documentation, 

1026 ) 

1027 

1028 if apply_standard_attribute_documentation: 1028 ↛ 1029line 1028 didn't jump to line 1029 because the condition on line 1028 was never true

1029 docs = _STD_ATTR_DOCS 

1030 else: 

1031 docs = None 

1032 

1033 parser = feature_set.manifest_parser_generator.generate_parser( 

1034 parsed_format, 

1035 source_content=source_format, 

1036 inline_reference_documentation=inline_reference_documentation, 

1037 expected_debputy_integration_mode=expected_debputy_integration_mode, 

1038 automatic_docs=docs, 

1039 allow_none_value=as_keyword_handler is not None, 

1040 ) 

1041 

1042 def _registering_handler( 

1043 name: str, 

1044 parsed_data: PF | None, 

1045 attribute_path: AttributePath, 

1046 parser_context: ParserContextData, 

1047 ) -> TP: 

1048 if parsed_data is not None: 1048 ↛ 1051line 1048 didn't jump to line 1051 because the condition on line 1048 was always true

1049 value = handler(name, parsed_data, attribute_path, parser_context) 

1050 else: 

1051 if as_keyword_handler is None: 

1052 raise AssertionError( 

1053 f"{name} was used and allowed as a keyword-only, but there was no handler for it" 

1054 ) 

1055 value = as_keyword_handler(name, attribute_path, parser_context) 

1056 if register_as_type is not None: 

1057 register_manifest_type_value_in_context(register_as_type, value) 

1058 return value 

1059 

1060 dispatching_parser.register_parser( 

1061 rule_name, 

1062 parser, 

1063 wrap_plugin_code(self._plugin_name, _registering_handler), 

1064 self._plugin_metadata, 

1065 ) 

1066 

1067 def _unload() -> None: 

1068 raise PluginInitializationError( 

1069 "Cannot unload pluggable_manifest_rule (not implemented)" 

1070 ) 

1071 

1072 self._unloaders.append(_unload) 

1073 

1074 def register_build_system( 

1075 self, 

1076 build_system_definition: type[BSPF], 

1077 ) -> None: 

1078 self._restricted_api() 

1079 if not is_typeddict(build_system_definition): 1079 ↛ 1080line 1079 didn't jump to line 1080 because the condition on line 1079 was never true

1080 raise PluginInitializationError( 

1081 f"Expected build_system_definition to be a subclass of {BuildRuleParsedFormat.__name__}," 

1082 f" but got {build_system_definition.__name__} instead" 

1083 ) 

1084 metadata = getattr( 

1085 build_system_definition, 

1086 _DEBPUTY_DISPATCH_METADATA_ATTR_NAME, 

1087 None, 

1088 ) 

1089 if not isinstance(metadata, BuildSystemManifestRuleMetadata): 1089 ↛ 1090line 1089 didn't jump to line 1090 because the condition on line 1089 was never true

1090 raise PluginIncorrectRegistrationError( 

1091 f"The {build_system_definition.__qualname__} type should have been annotated with" 

1092 f" @{debputy_build_system.__name__}." 

1093 ) 

1094 assert len(metadata.manifest_keywords) == 1 

1095 build_system_impl = metadata.build_system_impl 

1096 assert build_system_impl is not None 

1097 manifest_keyword = next(iter(metadata.manifest_keywords)) 

1098 self.pluggable_manifest_rule( 

1099 metadata.dispatched_type, 

1100 metadata.manifest_keywords, 

1101 build_system_definition, 

1102 # pluggable_manifest_rule does the wrapping 

1103 metadata.unwrapped_constructor, 

1104 source_format=metadata.source_format, 

1105 inline_reference_documentation=metadata.online_reference_documentation, 

1106 expected_debputy_integration_mode=only_integrations(INTEGRATION_MODE_FULL), 

1107 ) 

1108 self._auto_detectable_build_system( 

1109 manifest_keyword, 

1110 build_system_impl, 

1111 constructor=wrap_plugin_code( 

1112 self._plugin_name, 

1113 build_system_impl, 

1114 ), 

1115 shadowing_build_systems_when_active=metadata.auto_detection_shadow_build_systems, 

1116 ) 

1117 

1118 def _auto_detectable_build_system( 

1119 self, 

1120 manifest_keyword: str, 

1121 rule_type: type[BSR], 

1122 *, 

1123 shadowing_build_systems_when_active: frozenset[str] = frozenset(), 

1124 constructor: None | ( 

1125 Callable[[BuildRuleParsedFormat, AttributePath, "HighLevelManifest"], BSR] 

1126 ) = None, 

1127 ) -> None: 

1128 self._restricted_api() 

1129 feature_set = self._feature_set 

1130 existing = feature_set.auto_detectable_build_systems.get(rule_type) 

1131 if existing is not None: 1131 ↛ 1132line 1131 didn't jump to line 1132 because the condition on line 1131 was never true

1132 bs_name = rule_type.__class__.__name__ 

1133 if existing.plugin_metadata.plugin_name == self._plugin_name: 

1134 message = ( 

1135 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

1136 f' auto-detection of the build system "{bs_name}" twice.' 

1137 ) 

1138 else: 

1139 message = ( 

1140 f"The plugins {existing.plugin_metadata.plugin_name} and {self._plugin_name}" 

1141 f' both tried to provide auto-detection of the build system "{bs_name}"' 

1142 ) 

1143 raise PluginConflictError( 

1144 message, existing.plugin_metadata, self._plugin_metadata 

1145 ) 

1146 

1147 if constructor is None: 1147 ↛ 1149line 1147 didn't jump to line 1149 because the condition on line 1147 was never true

1148 

1149 def impl( 

1150 attributes: BuildRuleParsedFormat, 

1151 attribute_path: AttributePath, 

1152 manifest: "HighLevelManifest", 

1153 ) -> BSR: 

1154 return rule_type(attributes, attribute_path, manifest) 

1155 

1156 else: 

1157 impl = constructor 

1158 

1159 feature_set.auto_detectable_build_systems[rule_type] = ( 

1160 PluginProvidedBuildSystemAutoDetection( 

1161 manifest_keyword, 

1162 rule_type, 

1163 wrap_plugin_code(self._plugin_name, rule_type.auto_detect_build_system), 

1164 impl, 

1165 shadowing_build_systems_when_active, 

1166 self._plugin_metadata, 

1167 ) 

1168 ) 

1169 

1170 def _unload() -> None: 

1171 try: 

1172 del feature_set.auto_detectable_build_systems[rule_type] 

1173 except KeyError: 

1174 pass 

1175 

1176 self._unloaders.append(_unload) 

1177 

1178 def known_packaging_files( 

1179 self, 

1180 packaging_file_details: KnownPackagingFileInfo, 

1181 ) -> None: 

1182 known_packaging_files = self._feature_set.known_packaging_files 

1183 detection_method = packaging_file_details.get( 

1184 "detection_method", cast("Literal['path']", "path") 

1185 ) 

1186 path = packaging_file_details.get("path") 

1187 dhpkgfile = packaging_file_details.get("pkgfile") 

1188 

1189 packaging_file_details = packaging_file_details.copy() 

1190 

1191 if detection_method == "path": 1191 ↛ 1207line 1191 didn't jump to line 1207 because the condition on line 1191 was always true

1192 if dhpkgfile is not None: 1192 ↛ 1193line 1192 didn't jump to line 1193 because the condition on line 1192 was never true

1193 raise ValueError( 

1194 'The "pkgfile" attribute cannot be used when detection-method is "path" (or omitted)' 

1195 ) 

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

1197 raise ValueError( 

1198 'The "path" attribute must be present when detection-method is "path" (or omitted)' 

1199 ) 

1200 if path != _normalize_path(path, with_prefix=False): 1200 ↛ 1201line 1200 didn't jump to line 1201 because the condition on line 1200 was never true

1201 raise ValueError( 

1202 f"The path for known packaging files must be normalized. Please replace" 

1203 f' "{path}" with "{_normalize_path(path, with_prefix=False)}"' 

1204 ) 

1205 detection_value = path 

1206 else: 

1207 assert detection_method == "dh.pkgfile" 

1208 if path is not None: 

1209 raise ValueError( 

1210 'The "path" attribute cannot be used when detection-method is "dh.pkgfile"' 

1211 ) 

1212 if dhpkgfile is None: 

1213 raise ValueError( 

1214 'The "pkgfile" attribute must be present when detection-method is "dh.pkgfile"' 

1215 ) 

1216 if "/" in dhpkgfile: 

1217 raise ValueError( 

1218 'The "pkgfile" attribute ḿust be a name stem such as "install" (no "/" are allowed)' 

1219 ) 

1220 detection_value = dhpkgfile 

1221 key = f"{detection_method}::{detection_value}" 

1222 existing = known_packaging_files.get(key) 

1223 if existing is not None: 1223 ↛ 1224line 1223 didn't jump to line 1224 because the condition on line 1223 was never true

1224 if existing.plugin_metadata.plugin_name != self._plugin_name: 

1225 message = ( 

1226 f'The key "{key}" is registered twice for known packaging files.' 

1227 f" Once by {existing.plugin_metadata.plugin_name} and once by {self._plugin_name}" 

1228 ) 

1229 else: 

1230 message = ( 

1231 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

1232 f' key "{key}" twice for known packaging files.' 

1233 ) 

1234 raise PluginConflictError( 

1235 message, existing.plugin_metadata, self._plugin_metadata 

1236 ) 

1237 _validate_known_packaging_file_dh_compat_rules( 

1238 packaging_file_details.get("dh_compat_rules") 

1239 ) 

1240 known_packaging_files[key] = PluginProvidedKnownPackagingFile( 

1241 packaging_file_details, 

1242 detection_method, 

1243 detection_value, 

1244 self._plugin_metadata, 

1245 ) 

1246 

1247 def _unload() -> None: 

1248 del known_packaging_files[key] 

1249 

1250 self._unloaders.append(_unload) 

1251 

1252 def register_mapped_type( 

1253 self, 

1254 type_mapping: TypeMapping, 

1255 *, 

1256 reference_documentation: TypeMappingDocumentation | None = None, 

1257 ) -> None: 

1258 self._restricted_api() 

1259 target_type = type_mapping.target_type 

1260 mapped_types = self._feature_set.mapped_types 

1261 existing = mapped_types.get(target_type) 

1262 if existing is not None: 1262 ↛ 1263line 1262 didn't jump to line 1263 because the condition on line 1262 was never true

1263 if existing.plugin_metadata.plugin_name != self._plugin_name: 

1264 message = ( 

1265 f'The key "{target_type.__name__}" is registered twice for known packaging files.' 

1266 f" Once by {existing.plugin_metadata.plugin_name} and once by {self._plugin_name}" 

1267 ) 

1268 else: 

1269 message = ( 

1270 f"Bug in the plugin {self._plugin_name}: It tried to register the" 

1271 f' key "{target_type.__name__}" twice for known packaging files.' 

1272 ) 

1273 raise PluginConflictError( 

1274 message, existing.plugin_metadata, self._plugin_metadata 

1275 ) 

1276 parser_generator = self._feature_set.manifest_parser_generator 

1277 # TODO: Wrap the mapper in the plugin context 

1278 mapped_types[target_type] = PluginProvidedTypeMapping( 

1279 type_mapping, reference_documentation, self._plugin_metadata 

1280 ) 

1281 parser_generator.register_mapped_type(type_mapping) 

1282 

1283 def _restricted_api( 

1284 self, 

1285 *, 

1286 allowed_plugins: set[str] | frozenset[str] = frozenset(), 

1287 ) -> None: 

1288 if self._plugin_name != "debputy" and self._plugin_name not in allowed_plugins: 1288 ↛ 1289line 1288 didn't jump to line 1289 because the condition on line 1288 was never true

1289 raise PluginAPIViolationError( 

1290 f"Plugin {self._plugin_name} attempted to access a debputy-only API." 

1291 " If you are the maintainer of this plugin and want access to this" 

1292 " API, please file a feature request to make this public." 

1293 " (The API is currently private as it is unstable.)" 

1294 ) 

1295 

1296 

1297class MaintscriptAccessorProviderBase(MaintscriptAccessor, ABC): 

1298 __slots__ = () 

1299 

1300 def _append_script( 

1301 self, 

1302 caller_name: str, 

1303 maintscript: Maintscript, 

1304 full_script: str, 

1305 /, 

1306 perform_substitution: bool = True, 

1307 ) -> None: 

1308 raise NotImplementedError 

1309 

1310 @classmethod 

1311 def _apply_condition_to_script( 

1312 cls, 

1313 condition: str, 

1314 run_snippet: str, 

1315 /, 

1316 indent: bool | None = None, 

1317 ) -> str: 

1318 if indent is None: 

1319 # We auto-determine this based on heredocs currently 

1320 indent = "<<" not in run_snippet 

1321 

1322 if indent: 

1323 run_snippet = "".join(" " + x for x in run_snippet.splitlines(True)) 

1324 if not run_snippet.endswith("\n"): 

1325 run_snippet += "\n" 

1326 condition_line = f"if {condition}; then\n" 

1327 end_line = "fi\n" 

1328 return "".join((condition_line, run_snippet, end_line)) 

1329 

1330 def on_configure( 

1331 self, 

1332 run_snippet: str, 

1333 /, 

1334 indent: bool | None = None, 

1335 perform_substitution: bool = True, 

1336 skip_on_rollback: bool = False, 

1337 ) -> None: 

1338 condition = POSTINST_DEFAULT_CONDITION 

1339 if skip_on_rollback: 1339 ↛ 1340line 1339 didn't jump to line 1340 because the condition on line 1339 was never true

1340 condition = '[ "$1" = "configure" ]' 

1341 return self._append_script( 

1342 "on_configure", 

1343 "postinst", 

1344 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1345 perform_substitution=perform_substitution, 

1346 ) 

1347 

1348 def on_initial_install( 

1349 self, 

1350 run_snippet: str, 

1351 /, 

1352 indent: bool | None = None, 

1353 perform_substitution: bool = True, 

1354 ) -> None: 

1355 condition = '[ "$1" = "configure" -a -z "$2" ]' 

1356 return self._append_script( 

1357 "on_initial_install", 

1358 "postinst", 

1359 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1360 perform_substitution=perform_substitution, 

1361 ) 

1362 

1363 def on_upgrade( 

1364 self, 

1365 run_snippet: str, 

1366 /, 

1367 indent: bool | None = None, 

1368 perform_substitution: bool = True, 

1369 ) -> None: 

1370 condition = '[ "$1" = "configure" -a -n "$2" ]' 

1371 return self._append_script( 

1372 "on_upgrade", 

1373 "postinst", 

1374 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1375 perform_substitution=perform_substitution, 

1376 ) 

1377 

1378 def on_upgrade_from( 

1379 self, 

1380 version: str, 

1381 run_snippet: str, 

1382 /, 

1383 indent: bool | None = None, 

1384 perform_substitution: bool = True, 

1385 ) -> None: 

1386 condition = '[ "$1" = "configure" ] && dpkg --compare-versions le-nl "$2"' 

1387 return self._append_script( 

1388 "on_upgrade_from", 

1389 "postinst", 

1390 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1391 perform_substitution=perform_substitution, 

1392 ) 

1393 

1394 def on_before_removal( 

1395 self, 

1396 run_snippet: str, 

1397 /, 

1398 indent: bool | None = None, 

1399 perform_substitution: bool = True, 

1400 ) -> None: 

1401 condition = '[ "$1" = "remove" ]' 

1402 return self._append_script( 

1403 "on_before_removal", 

1404 "prerm", 

1405 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1406 perform_substitution=perform_substitution, 

1407 ) 

1408 

1409 def on_removed( 

1410 self, 

1411 run_snippet: str, 

1412 /, 

1413 indent: bool | None = None, 

1414 perform_substitution: bool = True, 

1415 ) -> None: 

1416 condition = '[ "$1" = "remove" ]' 

1417 return self._append_script( 

1418 "on_removed", 

1419 "postrm", 

1420 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1421 perform_substitution=perform_substitution, 

1422 ) 

1423 

1424 def on_purge( 

1425 self, 

1426 run_snippet: str, 

1427 /, 

1428 indent: bool | None = None, 

1429 perform_substitution: bool = True, 

1430 ) -> None: 

1431 condition = '[ "$1" = "purge" ]' 

1432 return self._append_script( 

1433 "on_purge", 

1434 "postrm", 

1435 self._apply_condition_to_script(condition, run_snippet, indent=indent), 

1436 perform_substitution=perform_substitution, 

1437 ) 

1438 

1439 def unconditionally_in_script( 

1440 self, 

1441 maintscript: Maintscript, 

1442 run_snippet: str, 

1443 /, 

1444 perform_substitution: bool = True, 

1445 ) -> None: 

1446 if maintscript not in DPKG_DEB_CONTROL_SCRIPTS: 1446 ↛ 1447line 1446 didn't jump to line 1447 because the condition on line 1446 was never true

1447 raise ValueError( 

1448 f'Unknown script "{maintscript}". Should have been one of:' 

1449 f' {", ".join(sorted(DPKG_DEB_CONTROL_SCRIPTS))}' 

1450 ) 

1451 return self._append_script( 

1452 "unconditionally_in_script", 

1453 maintscript, 

1454 run_snippet, 

1455 perform_substitution=perform_substitution, 

1456 ) 

1457 

1458 

1459class MaintscriptAccessorProvider(MaintscriptAccessorProviderBase): 

1460 __slots__ = ( 

1461 "_plugin_metadata", 

1462 "_maintscript_snippets", 

1463 "_plugin_source_id", 

1464 "_package_substitution", 

1465 "_default_snippet_anchor", 

1466 ) 

1467 

1468 def __init__( 

1469 self, 

1470 plugin_metadata: DebputyPluginMetadata, 

1471 plugin_source_id: str, 

1472 maintscript_snippets: PackageMaintscriptSnippetContainer, 

1473 package_substitution: Substitution, 

1474 *, 

1475 default_snippet_anchor: SnippetAnchor = SnippetAnchor._BETWEEN_CONFIGURATION_MANAGEMENT_AND_SERVICE, 

1476 ): 

1477 self._plugin_metadata = plugin_metadata 

1478 self._plugin_source_id = plugin_source_id 

1479 self._maintscript_snippets = maintscript_snippets 

1480 self._package_substitution = package_substitution 

1481 self._default_snippet_anchor = default_snippet_anchor 

1482 

1483 def _append_script( 

1484 self, 

1485 caller_name: str, 

1486 maintscript: Maintscript, 

1487 full_script: str, 

1488 /, 

1489 perform_substitution: bool = True, 

1490 ) -> None: 

1491 def_source = f"{self._plugin_metadata.plugin_name} ({self._plugin_source_id})" 

1492 if perform_substitution: 

1493 full_script = self._package_substitution.substitute(full_script, def_source) 

1494 

1495 snippet = UnboundMaintscriptSnippet( 

1496 snippet=SnippetResolver.snippet(full_script), 

1497 definition_source=def_source, 

1498 snippet_anchor=self._default_snippet_anchor, 

1499 ) 

1500 self._maintscript_snippets[maintscript].append(snippet) 

1501 

1502 

1503class BinaryCtrlAccessorProviderBase(BinaryCtrlAccessor): 

1504 __slots__ = ( 

1505 "_plugin_metadata", 

1506 "_plugin_source_id", 

1507 "_package_metadata_context", 

1508 "_triggers", 

1509 "_substvars", 

1510 "_maintscript", 

1511 "_shlibs_details", 

1512 ) 

1513 

1514 def __init__( 

1515 self, 

1516 plugin_metadata: DebputyPluginMetadata, 

1517 plugin_source_id: str, 

1518 package_metadata_context: PackageProcessingContext, 

1519 triggers: dict[tuple[DpkgTriggerType, str], PluginProvidedTrigger], 

1520 substvars: FlushableSubstvars, 

1521 shlibs_details: tuple[str | None, list[str] | None], 

1522 ) -> None: 

1523 self._plugin_metadata = plugin_metadata 

1524 self._plugin_source_id = plugin_source_id 

1525 self._package_metadata_context = package_metadata_context 

1526 self._triggers = triggers 

1527 self._substvars = substvars 

1528 self._maintscript: MaintscriptAccessor | None = None 

1529 self._shlibs_details = shlibs_details 

1530 

1531 def _create_maintscript_accessor(self) -> MaintscriptAccessor: 

1532 raise NotImplementedError 

1533 

1534 def dpkg_trigger(self, trigger_type: DpkgTriggerType, trigger_target: str) -> None: 

1535 """Register a declarative dpkg level trigger 

1536 

1537 The provided trigger will be added to the package's metadata (the triggers file of the control.tar). 

1538 

1539 If the trigger has already been added previously, a second call with the same trigger data will be ignored. 

1540 """ 

1541 key = (trigger_type, trigger_target) 

1542 if key in self._triggers: 1542 ↛ 1543line 1542 didn't jump to line 1543 because the condition on line 1542 was never true

1543 return 

1544 self._triggers[key] = PluginProvidedTrigger( 

1545 dpkg_trigger_type=trigger_type, 

1546 dpkg_trigger_target=trigger_target, 

1547 provider=self._plugin_metadata, 

1548 provider_source_id=self._plugin_source_id, 

1549 ) 

1550 

1551 @property 

1552 def maintscript(self) -> MaintscriptAccessor: 

1553 maintscript = self._maintscript 

1554 if maintscript is None: 

1555 maintscript = self._create_maintscript_accessor() 

1556 self._maintscript = maintscript 

1557 return maintscript 

1558 

1559 @property 

1560 def substvars(self) -> FlushableSubstvars: 

1561 return self._substvars 

1562 

1563 def dpkg_shlibdeps(self, paths: Sequence[VirtualPath]) -> None: 

1564 binary_package = self._package_metadata_context.binary_package 

1565 with self.substvars.flush() as substvars_file: 

1566 dpkg_cmd = ["dpkg-shlibdeps", f"-T{substvars_file}"] 

1567 if binary_package.is_udeb: 

1568 dpkg_cmd.append("-tudeb") 

1569 if binary_package.is_essential: 1569 ↛ 1570line 1569 didn't jump to line 1570 because the condition on line 1569 was never true

1570 dpkg_cmd.append("-dPre-Depends") 

1571 shlibs_local, shlib_dirs = self._shlibs_details 

1572 if shlibs_local is not None: 1572 ↛ 1573line 1572 didn't jump to line 1573 because the condition on line 1572 was never true

1573 dpkg_cmd.append(f"-L{shlibs_local}") 

1574 if shlib_dirs: 1574 ↛ 1575line 1574 didn't jump to line 1575 because the condition on line 1574 was never true

1575 dpkg_cmd.extend(f"-l{sd}" for sd in shlib_dirs) 

1576 dpkg_cmd.extend(p.fs_path for p in paths) 

1577 print_command(*dpkg_cmd) 

1578 try: 

1579 subprocess.check_call(dpkg_cmd) 

1580 except subprocess.CalledProcessError: 

1581 _error( 

1582 f"Attempting to auto-detect dependencies via dpkg-shlibdeps for {binary_package.name} failed. Please" 

1583 " review the output from dpkg-shlibdeps above to understand what went wrong." 

1584 ) 

1585 

1586 

1587class BinaryCtrlAccessorProvider(BinaryCtrlAccessorProviderBase): 

1588 __slots__ = ( 

1589 "_maintscript", 

1590 "_maintscript_snippets", 

1591 "_package_substitution", 

1592 ) 

1593 

1594 def __init__( 

1595 self, 

1596 plugin_metadata: DebputyPluginMetadata, 

1597 plugin_source_id: str, 

1598 package_metadata_context: PackageProcessingContext, 

1599 triggers: dict[tuple[DpkgTriggerType, str], PluginProvidedTrigger], 

1600 substvars: FlushableSubstvars, 

1601 maintscript_snippets: PackageMaintscriptSnippetContainer, 

1602 package_substitution: Substitution, 

1603 shlibs_details: tuple[str | None, list[str] | None], 

1604 *, 

1605 default_snippet_anchor: SnippetAnchor = SnippetAnchor._BETWEEN_CONFIGURATION_MANAGEMENT_AND_SERVICE, 

1606 ) -> None: 

1607 super().__init__( 

1608 plugin_metadata, 

1609 plugin_source_id, 

1610 package_metadata_context, 

1611 triggers, 

1612 substvars, 

1613 shlibs_details, 

1614 ) 

1615 self._maintscript_snippets = maintscript_snippets 

1616 self._package_substitution = package_substitution 

1617 self._maintscript = MaintscriptAccessorProvider( 

1618 plugin_metadata, 

1619 plugin_source_id, 

1620 maintscript_snippets, 

1621 package_substitution, 

1622 default_snippet_anchor=default_snippet_anchor, 

1623 ) 

1624 

1625 def _create_maintscript_accessor(self) -> MaintscriptAccessor: 

1626 return MaintscriptAccessorProvider( 

1627 self._plugin_metadata, 

1628 self._plugin_source_id, 

1629 self._maintscript_snippets, 

1630 self._package_substitution, 

1631 ) 

1632 

1633 

1634class BinaryCtrlAccessorProviderCreator: 

1635 def __init__( 

1636 self, 

1637 package_metadata_context: PackageProcessingContext, 

1638 substvars: FlushableSubstvars, 

1639 maintscript_snippets: PackageMaintscriptSnippetContainer, 

1640 substitution: Substitution, 

1641 ) -> None: 

1642 self._package_metadata_context = package_metadata_context 

1643 self._substvars = substvars 

1644 self._maintscript_snippets = maintscript_snippets 

1645 self._substitution = substitution 

1646 self._triggers: dict[tuple[DpkgTriggerType, str], PluginProvidedTrigger] = {} 

1647 self.shlibs_details: tuple[str | None, list[str] | None] = None, None 

1648 

1649 def for_plugin( 

1650 self, 

1651 plugin_metadata: DebputyPluginMetadata, 

1652 plugin_source_id: str, 

1653 *, 

1654 default_snippet_anchor: SnippetAnchor | None = None, 

1655 ) -> BinaryCtrlAccessor: 

1656 return BinaryCtrlAccessorProvider( 

1657 plugin_metadata, 

1658 plugin_source_id, 

1659 self._package_metadata_context, 

1660 self._triggers, 

1661 self._substvars, 

1662 self._maintscript_snippets, 

1663 self._substitution, 

1664 self.shlibs_details, 

1665 default_snippet_anchor=SnippetAnchor._BETWEEN_CONFIGURATION_MANAGEMENT_AND_SERVICE, 

1666 ) 

1667 

1668 def generated_triggers(self) -> Iterable[PluginProvidedTrigger]: 

1669 return self._triggers.values() 

1670 

1671 

1672def _resolve_bundled_plugin_docs_path( 

1673 plugin_name: str, 

1674 loader: PluginInitializationEntryPoint | None, 

1675) -> Traversable | Path | None: 

1676 plugin_module = getattr(loader, "__module__") 

1677 assert plugin_module is not None 

1678 plugin_package_name = sys.modules[plugin_module].__package__ 

1679 return importlib.resources.files(plugin_package_name).joinpath( 

1680 f"{plugin_name}_docs.yaml" 

1681 ) 

1682 

1683 

1684def plugin_metadata_for_debputys_own_plugin( 

1685 loader: PluginInitializationEntryPoint | None = None, 

1686) -> DebputyPluginMetadata: 

1687 if loader is None: 

1688 from debputy.plugins.debputy.debputy_plugin import ( 

1689 initialize_debputy_features, 

1690 ) 

1691 

1692 loader = initialize_debputy_features 

1693 plugin_name = "debputy" 

1694 return DebputyPluginMetadata( 

1695 plugin_name="debputy", 

1696 api_compat_version=1, 

1697 plugin_initializer=loader, 

1698 plugin_loader=None, 

1699 plugin_doc_path_resolver=lambda: _resolve_bundled_plugin_docs_path( 

1700 plugin_name, 

1701 loader, 

1702 ), 

1703 plugin_path="<bundled>", 

1704 ) 

1705 

1706 

1707def load_plugin_features( 

1708 plugin_search_dirs: Sequence[str], 

1709 substitution: Substitution, 

1710 requested_plugins_only: Sequence[str] | None = None, 

1711 required_plugins: set[str] | None = None, 

1712 plugin_feature_set: PluginProvidedFeatureSet | None = None, 

1713 debug_mode: bool = False, 

1714) -> PluginProvidedFeatureSet: 

1715 if plugin_feature_set is None: 

1716 plugin_feature_set = PluginProvidedFeatureSet() 

1717 plugins = [plugin_metadata_for_debputys_own_plugin()] 

1718 unloadable_plugins = set() 

1719 if required_plugins: 

1720 plugins.extend( 

1721 find_json_plugins( 

1722 plugin_search_dirs, 

1723 required_plugins, 

1724 ) 

1725 ) 

1726 if requested_plugins_only is not None: 

1727 plugins.extend( 

1728 find_json_plugins( 

1729 plugin_search_dirs, 

1730 requested_plugins_only, 

1731 ) 

1732 ) 

1733 else: 

1734 auto_loaded = _find_all_json_plugins( 

1735 plugin_search_dirs, 

1736 required_plugins if required_plugins is not None else frozenset(), 

1737 debug_mode=debug_mode, 

1738 ) 

1739 for plugin_metadata in auto_loaded: 

1740 plugins.append(plugin_metadata) 

1741 unloadable_plugins.add(plugin_metadata.plugin_name) 

1742 

1743 for plugin_metadata in plugins: 

1744 api = DebputyPluginInitializerProvider( 

1745 plugin_metadata, plugin_feature_set, substitution 

1746 ) 

1747 try: 

1748 api.load_plugin() 

1749 except PluginBaseError as e: 

1750 if plugin_metadata.plugin_name not in unloadable_plugins: 

1751 raise 

1752 if debug_mode: 

1753 _warn( 

1754 f"The optional plugin {plugin_metadata.plugin_name} failed during load. Re-raising due" 

1755 f" to --debug/-d or DEBPUTY_DEBUG=1" 

1756 ) 

1757 raise 

1758 try: 

1759 api.unload_plugin() 

1760 except Exception: 

1761 _warn( 

1762 f"Failed to load optional {plugin_metadata.plugin_name} and an error was raised when trying to" 

1763 " clean up after the half-initialized plugin. Re-raising load error as the partially loaded" 

1764 " module might have tainted the feature set." 

1765 ) 

1766 raise e from None 

1767 _warn( 

1768 f"The optional plugin {plugin_metadata.plugin_name} failed during load. The plugin was" 

1769 f" deactivated. Use debug mode (--debug/DEBPUTY_DEBUG=1) to show the stacktrace" 

1770 f" (the warning will become an error)" 

1771 ) 

1772 

1773 return plugin_feature_set 

1774 

1775 

1776def find_json_plugin( 

1777 search_dirs: Sequence[str], 

1778 requested_plugin: str, 

1779) -> DebputyPluginMetadata: 

1780 r = list(find_json_plugins(search_dirs, [requested_plugin])) 

1781 assert len(r) == 1 

1782 return r[0] 

1783 

1784 

1785def find_related_implementation_files_for_plugin( 

1786 plugin_metadata: DebputyPluginMetadata, 

1787) -> list[str]: 

1788 if plugin_metadata.is_bundled: 

1789 plugin_name = plugin_metadata.plugin_name 

1790 _error( 

1791 f"Cannot run find related files for {plugin_name}: The plugin seems to be bundled" 

1792 " or loaded via a mechanism that does not support detecting its tests." 

1793 ) 

1794 

1795 if plugin_metadata.is_from_python_path: 

1796 plugin_name = plugin_metadata.plugin_name 

1797 # Maybe they could be, but that is for another day. 

1798 _error( 

1799 f"Cannot run find related files for {plugin_name}: The plugin is installed into python path" 

1800 " and these are not supported." 

1801 ) 

1802 files = [] 

1803 module_name, module_file = _find_plugin_implementation_file( 

1804 plugin_metadata.plugin_name, 

1805 plugin_metadata.plugin_path, 

1806 ) 

1807 if os.path.isfile(module_file): 

1808 files.append(module_file) 

1809 else: 

1810 if not plugin_metadata.is_loaded: 

1811 plugin_metadata.load_plugin() 

1812 if module_name in sys.modules: 

1813 _error( 

1814 f'The plugin {plugin_metadata.plugin_name} uses the "module"" key in its' 

1815 f" JSON metadata file ({plugin_metadata.plugin_path}) and cannot be " 

1816 f" installed via this method. The related Python would not be installed" 

1817 f" (which would result in a plugin that would fail to load)" 

1818 ) 

1819 

1820 return files 

1821 

1822 

1823def find_tests_for_plugin( 

1824 plugin_metadata: DebputyPluginMetadata, 

1825) -> list[str]: 

1826 plugin_name = plugin_metadata.plugin_name 

1827 plugin_path = plugin_metadata.plugin_path 

1828 

1829 if plugin_metadata.is_bundled: 

1830 _error( 

1831 f"Cannot run tests for {plugin_name}: The plugin seems to be bundled or loaded via a" 

1832 " mechanism that does not support detecting its tests." 

1833 ) 

1834 

1835 if plugin_metadata.is_from_python_path: 

1836 plugin_name = plugin_metadata.plugin_name 

1837 # Maybe they could be, but that is for another day. 

1838 _error( 

1839 f"Cannot run find related files for {plugin_name}: The plugin is installed into python path" 

1840 " and these are not supported." 

1841 ) 

1842 

1843 plugin_dir = os.path.dirname(plugin_path) 

1844 test_basename_prefix = plugin_metadata.plugin_name.replace("-", "_") 

1845 tests = [] 

1846 with os.scandir(plugin_dir) as dir_iter: 

1847 for p in dir_iter: 

1848 if ( 

1849 p.is_file() 

1850 and p.name.startswith(test_basename_prefix) 

1851 and PLUGIN_TEST_SUFFIX.search(p.name) 

1852 ): 

1853 tests.append(p.path) 

1854 return tests 

1855 

1856 

1857def find_json_plugins( 

1858 search_dirs: Sequence[str], 

1859 requested_plugins: Iterable[str], 

1860) -> Iterable[DebputyPluginMetadata]: 

1861 for plugin_name_or_path in requested_plugins: 1861 ↛ exitline 1861 didn't return from function 'find_json_plugins' because the loop on line 1861 didn't complete

1862 if "/" in plugin_name_or_path: 1862 ↛ 1863line 1862 didn't jump to line 1863 because the condition on line 1862 was never true

1863 if not os.path.isfile(plugin_name_or_path): 

1864 raise PluginNotFoundError( 

1865 f"Unable to load the plugin {plugin_name_or_path}: The path is not a file." 

1866 ' (Because the plugin name contains "/", it is assumed to be a path and search path' 

1867 " is not used." 

1868 ) 

1869 yield parse_json_plugin_desc(plugin_name_or_path) 

1870 return 

1871 for search_dir in search_dirs: 1871 ↛ 1880line 1871 didn't jump to line 1880 because the loop on line 1871 didn't complete

1872 path = os.path.join( 

1873 search_dir, "debputy", "plugins", f"{plugin_name_or_path}.json" 

1874 ) 

1875 if not os.path.isfile(path): 1875 ↛ 1876line 1875 didn't jump to line 1876 because the condition on line 1875 was never true

1876 continue 

1877 yield parse_json_plugin_desc(path) 

1878 return 

1879 

1880 path_root = PLUGIN_PYTHON_RES_PATH 

1881 pp_path = path_root.joinpath(f"{plugin_name_or_path}.json") 

1882 if pp_path or pp_path.is_file(): 

1883 with pp_path.open() as fd: 

1884 yield parse_json_plugin_desc( 

1885 f"PYTHONPATH:debputy/plugins/{pp_path.name}", 

1886 fd=fd, 

1887 is_from_python_path=True, 

1888 ) 

1889 return 

1890 

1891 search_dir_str = ":".join(search_dirs) 

1892 raise PluginNotFoundError( 

1893 f"Unable to load the plugin {plugin_name_or_path}: Could not find {plugin_name_or_path}.json in the" 

1894 f" debputy/plugins subdir of any of the search dirs ({search_dir_str})" 

1895 ) 

1896 

1897 

1898def _find_all_json_plugins( 

1899 search_dirs: Sequence[str], 

1900 required_plugins: AbstractSet[str], 

1901 debug_mode: bool = False, 

1902) -> Iterable[DebputyPluginMetadata]: 

1903 seen = set(required_plugins) 

1904 error_seen = False 

1905 for search_dir in search_dirs: 

1906 try: 

1907 dir_fd = os.scandir(os.path.join(search_dir, "debputy", "plugins")) 

1908 except FileNotFoundError: 

1909 continue 

1910 with dir_fd: 

1911 for entry in dir_fd: 

1912 if ( 

1913 not entry.is_file(follow_symlinks=True) 

1914 or not entry.name.endswith(".json") 

1915 or entry.name in seen 

1916 ): 

1917 continue 

1918 seen.add(entry.name) 

1919 try: 

1920 plugin_metadata = parse_json_plugin_desc(entry.path) 

1921 except PluginBaseError as e: 

1922 if debug_mode: 

1923 raise 

1924 if not error_seen: 

1925 error_seen = True 

1926 _warn( 

1927 f"Failed to load the plugin in {entry.path} due to the following error: {e.message}" 

1928 ) 

1929 else: 

1930 _warn( 

1931 f"Failed to load plugin in {entry.path} due to errors (not shown)." 

1932 ) 

1933 else: 

1934 yield plugin_metadata 

1935 

1936 for pp_entry in PLUGIN_PYTHON_RES_PATH.iterdir(): 

1937 if ( 

1938 not pp_entry.name.endswith(".json") 

1939 or not pp_entry.is_file() 

1940 or pp_entry.name in seen 

1941 ): 

1942 continue 

1943 seen.add(pp_entry.name) 

1944 with pp_entry.open() as fd: 

1945 yield parse_json_plugin_desc( 

1946 f"PYTHONPATH:debputy/plugins/{pp_entry.name}", 

1947 fd=fd, 

1948 is_from_python_path=True, 

1949 ) 

1950 

1951 

1952def _find_plugin_implementation_file( 

1953 plugin_name: str, 

1954 json_file_path: str, 

1955) -> tuple[str, str]: 

1956 guessed_module_basename = plugin_name.replace("-", "_") 

1957 module_name = f"debputy.plugins.{guessed_module_basename}" 

1958 module_fs_path = os.path.join( 

1959 os.path.dirname(json_file_path), f"{guessed_module_basename}.py" 

1960 ) 

1961 return module_name, module_fs_path 

1962 

1963 

1964def _resolve_module_initializer( 

1965 plugin_name: str, 

1966 plugin_initializer_name: str, 

1967 module_name: str | None, 

1968 json_file_path: str, 

1969) -> PluginInitializationEntryPoint: 

1970 module = None 

1971 module_fs_path = None 

1972 if module_name is None: 1972 ↛ 2000line 1972 didn't jump to line 2000 because the condition on line 1972 was always true

1973 module_name, module_fs_path = _find_plugin_implementation_file( 

1974 plugin_name, json_file_path 

1975 ) 

1976 if os.path.isfile(module_fs_path): 1976 ↛ 2000line 1976 didn't jump to line 2000 because the condition on line 1976 was always true

1977 spec = importlib.util.spec_from_file_location(module_name, module_fs_path) 

1978 if spec is None: 1978 ↛ 1979line 1978 didn't jump to line 1979 because the condition on line 1978 was never true

1979 raise PluginInitializationError( 

1980 f"Failed to load {plugin_name} (path: {module_fs_path})." 

1981 " The spec_from_file_location function returned None." 

1982 ) 

1983 mod = importlib.util.module_from_spec(spec) 

1984 loader = spec.loader 

1985 if loader is None: 1985 ↛ 1986line 1985 didn't jump to line 1986 because the condition on line 1985 was never true

1986 raise PluginInitializationError( 

1987 f"Failed to load {plugin_name} (path: {module_fs_path})." 

1988 " Python could not find a suitable loader (spec.loader was None)" 

1989 ) 

1990 sys.modules[module_name] = mod 

1991 try: 

1992 run_in_context_of_plugin(plugin_name, loader.exec_module, mod) 

1993 except (Exception, GeneratorExit) as e: 

1994 raise PluginInitializationError( 

1995 f"Failed to load {plugin_name} (path: {module_fs_path})." 

1996 " The module threw an exception while being loaded." 

1997 ) from e 

1998 module = mod 

1999 

2000 if module is None: 2000 ↛ 2001line 2000 didn't jump to line 2001 because the condition on line 2000 was never true

2001 try: 

2002 module = run_in_context_of_plugin( 

2003 plugin_name, importlib.import_module, module_name 

2004 ) 

2005 except ModuleNotFoundError as e: 

2006 if module_fs_path is None: 

2007 raise PluginMetadataError( 

2008 f'The plugin defined in "{json_file_path}" wanted to load the module "{module_name}", but' 

2009 " this module is not available in the python search path" 

2010 ) from e 

2011 raise PluginInitializationError( 

2012 f"Failed to load {plugin_name}. Tried loading it from" 

2013 f' "{module_fs_path}" (which did not exist) and PYTHONPATH as' 

2014 f" {module_name} (where it was not found either). Please ensure" 

2015 " the module code is installed in the correct spot or provide an" 

2016 f' explicit "module" definition in {json_file_path}.' 

2017 ) from e 

2018 

2019 plugin_initializer = run_in_context_of_plugin_wrap_errors( 

2020 plugin_name, 

2021 getattr, 

2022 module, 

2023 plugin_initializer_name, 

2024 None, 

2025 ) 

2026 

2027 if plugin_initializer is None: 2027 ↛ 2028line 2027 didn't jump to line 2028 because the condition on line 2027 was never true

2028 raise PluginMetadataError( 

2029 f'The plugin defined in {json_file_path} claimed that module "{module_name}" would have an' 

2030 f' attribute called "{plugin_initializer_name}" to initialize the plugin. However, that attribute' 

2031 " does not exist or cannot be resolved. Please correct the plugin metadata or initializer name" 

2032 " in the Python module." 

2033 ) 

2034 if isinstance(plugin_initializer, DebputyPluginDefinition): 

2035 return plugin_initializer.initialize 

2036 if not callable(plugin_initializer): 2036 ↛ 2037line 2036 didn't jump to line 2037 because the condition on line 2036 was never true

2037 raise PluginMetadataError( 

2038 f'The plugin defined in {json_file_path} claimed that module "{module_name}" would have an' 

2039 f' attribute called "{plugin_initializer_name}" for initializing the plugin. While that' 

2040 " attribute exists, it is neither a `DebputyPluginDefinition`" 

2041 " (`plugin_definition = define_debputy_plugin()`) nor is it `callable`" 

2042 " (`def initialize(api: DebputyPluginInitializer) -> None:`)." 

2043 ) 

2044 return cast("PluginInitializationEntryPoint", plugin_initializer) 

2045 

2046 

2047def _json_plugin_loader( 

2048 plugin_name: str, 

2049 plugin_json_metadata: PluginJsonMetadata, 

2050 json_file_path: str, 

2051 attribute_path: AttributePath, 

2052) -> Callable[["DebputyPluginInitializer"], None]: 

2053 api_compat = plugin_json_metadata["api_compat_version"] 

2054 module_name = plugin_json_metadata.get("module") 

2055 plugin_initializer_name = plugin_json_metadata.get("plugin_initializer") 

2056 packager_provided_files_raw = plugin_json_metadata.get( 

2057 "packager_provided_files", [] 

2058 ) 

2059 manifest_variables_raw = plugin_json_metadata.get("manifest_variables") 

2060 known_packaging_files_raw = plugin_json_metadata.get("known_packaging_files") 

2061 if api_compat != 1: 2061 ↛ 2062line 2061 didn't jump to line 2062 because the condition on line 2061 was never true

2062 raise PluginMetadataError( 

2063 f'The plugin defined in "{json_file_path}" requires API compat level {api_compat}, but this' 

2064 f" version of debputy only supports API compat version of 1" 

2065 ) 

2066 if plugin_initializer_name is not None and "." in plugin_initializer_name: 2066 ↛ 2067line 2066 didn't jump to line 2067 because the condition on line 2066 was never true

2067 p = attribute_path["plugin_initializer"] 

2068 raise PluginMetadataError( 

2069 f'The "{p}" attribute must not contain ".". Problematic file is "{json_file_path}".' 

2070 ) 

2071 

2072 plugin_initializers = [] 

2073 

2074 if plugin_initializer_name is not None: 

2075 plugin_initializer = _resolve_module_initializer( 

2076 plugin_name, 

2077 plugin_initializer_name, 

2078 module_name, 

2079 json_file_path, 

2080 ) 

2081 plugin_initializers.append(plugin_initializer) 

2082 

2083 if known_packaging_files_raw: 

2084 kpf_root_path = attribute_path["known_packaging_files"] 

2085 known_packaging_files = [] 

2086 for k, v in enumerate(known_packaging_files_raw): 

2087 kpf_path = kpf_root_path[k] 

2088 p = v.get("path") 

2089 if isinstance(p, str): 2089 ↛ 2091line 2089 didn't jump to line 2091 because the condition on line 2089 was always true

2090 kpf_path.path_hint = p 

2091 if plugin_name.startswith("debputy-") and isinstance(v, dict): 2091 ↛ 2103line 2091 didn't jump to line 2103 because the condition on line 2091 was always true

2092 docs = v.get("documentation-uris") 

2093 if docs is not None and isinstance(docs, list): 

2094 docs = [ 

2095 ( 

2096 d.replace("@DEBPUTY_DOC_ROOT_DIR@", debputy_doc_root_dir()) 

2097 if isinstance(d, str) 

2098 else d 

2099 ) 

2100 for d in docs 

2101 ] 

2102 v["documentation-uris"] = docs 

2103 known_packaging_file: KnownPackagingFileInfo = ( 

2104 PLUGIN_KNOWN_PACKAGING_FILES_PARSER.parse_input( 

2105 v, 

2106 kpf_path, 

2107 ) 

2108 ) 

2109 known_packaging_files.append((kpf_path, known_packaging_file)) 

2110 

2111 def _initialize_json_provided_known_packaging_files( 

2112 api: DebputyPluginInitializerProvider, 

2113 ) -> None: 

2114 for p, details in known_packaging_files: 

2115 try: 

2116 api.known_packaging_files(details) 

2117 except ValueError as ex: 

2118 raise PluginMetadataError( 

2119 f"Error while processing {p.path} defined in {json_file_path}: {ex.args[0]}" 

2120 ) 

2121 

2122 plugin_initializers.append(_initialize_json_provided_known_packaging_files) 

2123 

2124 if manifest_variables_raw: 

2125 manifest_var_path = attribute_path["manifest_variables"] 

2126 manifest_variables = [ 

2127 PLUGIN_MANIFEST_VARS_PARSER.parse_input(p, manifest_var_path[i]) 

2128 for i, p in enumerate(manifest_variables_raw) 

2129 ] 

2130 

2131 def _initialize_json_provided_manifest_vars( 

2132 api: DebputyPluginInitializer, 

2133 ) -> None: 

2134 for idx, manifest_variable in enumerate(manifest_variables): 

2135 name = manifest_variable["name"] 

2136 value = manifest_variable["value"] 

2137 doc = manifest_variable.get("reference_documentation") 

2138 try: 

2139 api.manifest_variable( 

2140 name, value, variable_reference_documentation=doc 

2141 ) 

2142 except ValueError as ex: 

2143 var_path = manifest_var_path[idx] 

2144 raise PluginMetadataError( 

2145 f"Error while processing {var_path.path} defined in {json_file_path}: {ex.args[0]}" 

2146 ) 

2147 

2148 plugin_initializers.append(_initialize_json_provided_manifest_vars) 

2149 

2150 if packager_provided_files_raw: 

2151 ppf_path = attribute_path["packager_provided_files"] 

2152 ppfs = [ 

2153 PLUGIN_PPF_PARSER.parse_input(p, ppf_path[i]) 

2154 for i, p in enumerate(packager_provided_files_raw) 

2155 ] 

2156 

2157 def _initialize_json_provided_ppfs(api: DebputyPluginInitializer) -> None: 

2158 ppf: PackagerProvidedFileJsonDescription 

2159 for idx, ppf in enumerate(ppfs): 

2160 c = dict(ppf) 

2161 stem = ppf["stem"] 

2162 installed_path = ppf["installed_path"] 

2163 default_mode = ppf.get("default_mode") 

2164 ref_doc_dict = ppf.get("reference_documentation") 

2165 if default_mode is not None: 2165 ↛ 2168line 2165 didn't jump to line 2168 because the condition on line 2165 was always true

2166 c["default_mode"] = default_mode.octal_mode 

2167 

2168 if ref_doc_dict is not None: 2168 ↛ 2173line 2168 didn't jump to line 2173 because the condition on line 2168 was always true

2169 ref_doc = packager_provided_file_reference_documentation( 

2170 **ref_doc_dict 

2171 ) 

2172 else: 

2173 ref_doc = None 

2174 

2175 for k in [ 

2176 "stem", 

2177 "installed_path", 

2178 "reference_documentation", 

2179 ]: 

2180 try: 

2181 del c[k] 

2182 except KeyError: 

2183 pass 

2184 

2185 try: 

2186 api.packager_provided_file(stem, installed_path, reference_documentation=ref_doc, **c) # type: ignore 

2187 except ValueError as ex: 

2188 p_path = ppf_path[idx] 

2189 raise PluginMetadataError( 

2190 f"Error while processing {p_path.path} defined in {json_file_path}: {ex.args[0]}" 

2191 ) 

2192 

2193 plugin_initializers.append(_initialize_json_provided_ppfs) 

2194 

2195 if not plugin_initializers: 2195 ↛ 2196line 2195 didn't jump to line 2196 because the condition on line 2195 was never true

2196 raise PluginMetadataError( 

2197 f"The plugin defined in {json_file_path} does not seem to provide features" 

2198 f" known by this version of `debputy`, such as module + plugin-initializer" 

2199 f" or packager-provided-files. The plugin might be missing content, or" 

2200 f" this version of `debputy` might not have the feature set to support it." 

2201 ) 

2202 

2203 if len(plugin_initializers) == 1: 

2204 return plugin_initializers[0] 

2205 

2206 def _chain_loader(api: DebputyPluginInitializer) -> None: 

2207 for initializer in plugin_initializers: 

2208 initializer(api) 

2209 

2210 return _chain_loader 

2211 

2212 

2213@overload 

2214@contextlib.contextmanager 

2215def _open( 2215 ↛ exitline 2215 didn't return from function '_open' because

2216 path: str, 

2217 fd: IO[AnyStr] | IOBase = ..., 

2218) -> Iterator[IO[AnyStr] | IOBase]: ... 

2219 

2220 

2221@overload 

2222@contextlib.contextmanager 

2223def _open(path: str, fd: None = None) -> Iterator[IO[bytes]]: ... 2223 ↛ exitline 2223 didn't return from function '_open' because

2224 

2225 

2226@contextlib.contextmanager 

2227def _open( 

2228 path: str, fd: IO[AnyStr] | IOBase | None = None 

2229) -> Iterator[IO[AnyStr] | IOBase]: 

2230 if fd is not None: 

2231 yield fd 

2232 else: 

2233 with open(path, "rb") as fd: 

2234 yield fd 

2235 

2236 

2237def _resolve_json_plugin_docs_path( 

2238 plugin_name: str, 

2239 plugin_path: str, 

2240) -> Traversable | Path | None: 

2241 plugin_dir = os.path.dirname(plugin_path) 

2242 return Path(os.path.join(plugin_dir, plugin_name + "_docs.yaml")) 

2243 

2244 

2245def parse_json_plugin_desc( 

2246 path: str, 

2247 *, 

2248 fd: IO[AnyStr] | IOBase | None = None, 

2249 is_from_python_path: bool = False, 

2250) -> DebputyPluginMetadata: 

2251 with _open(path, fd=fd) as rfd: 

2252 try: 

2253 raw = json.load(rfd) 

2254 except JSONDecodeError as e: 

2255 raise PluginMetadataError( 

2256 f'The plugin defined in "{path}" could not be parsed as valid JSON: {e.args[0]}' 

2257 ) from e 

2258 plugin_name = os.path.basename(path) 

2259 if plugin_name.endswith(".json"): 

2260 plugin_name = plugin_name[:-5] 

2261 elif plugin_name.endswith(".json.in"): 

2262 plugin_name = plugin_name[:-8] 

2263 

2264 if plugin_name == "debputy": 2264 ↛ 2266line 2264 didn't jump to line 2266 because the condition on line 2264 was never true

2265 # Provide a better error message than "The plugin has already loaded!?" 

2266 raise PluginMetadataError( 

2267 f'The plugin named {plugin_name} must be bundled with `debputy`. Please rename "{path}" so it does not' 

2268 f" clash with the bundled plugin of same name." 

2269 ) 

2270 

2271 attribute_path = AttributePath.root_path(raw) 

2272 

2273 try: 

2274 plugin_json_metadata = PLUGIN_METADATA_PARSER.parse_input( 

2275 raw, 

2276 attribute_path, 

2277 ) 

2278 except ManifestParseException as e: 

2279 raise PluginMetadataError( 

2280 f'The plugin defined in "{path}" was valid JSON but could not be parsed: {e.message}' 

2281 ) from e 

2282 api_compat = plugin_json_metadata["api_compat_version"] 

2283 

2284 return DebputyPluginMetadata( 

2285 plugin_name=plugin_name, 

2286 plugin_loader=lambda: _json_plugin_loader( 

2287 plugin_name, 

2288 plugin_json_metadata, 

2289 path, 

2290 attribute_path, 

2291 ), 

2292 api_compat_version=api_compat, 

2293 plugin_doc_path_resolver=lambda: _resolve_json_plugin_docs_path( 

2294 plugin_name, path 

2295 ), 

2296 plugin_initializer=None, 

2297 plugin_path=path, 

2298 is_from_python_path=is_from_python_path, 

2299 ) 

2300 

2301 

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

2303class ServiceDefinitionImpl(ServiceDefinition[DSD]): 

2304 name: str 

2305 names: Sequence[str] 

2306 path: VirtualPath 

2307 type_of_service: str 

2308 service_scope: str 

2309 auto_enable_on_install: bool 

2310 auto_start_on_install: bool 

2311 on_upgrade: ServiceUpgradeRule 

2312 definition_source: str 

2313 is_plugin_provided_definition: bool 

2314 service_context: DSD | None 

2315 

2316 def replace(self, **changes: Any) -> "ServiceDefinitionImpl[DSD]": 

2317 return dataclasses.replace(self, **changes) 

2318 

2319 

2320class ServiceRegistryImpl(ServiceRegistry[DSD]): 

2321 __slots__ = ("_service_manager_details", "_service_definitions", "_seen_services") 

2322 

2323 def __init__(self, service_manager_details: ServiceManagerDetails) -> None: 

2324 self._service_manager_details = service_manager_details 

2325 self._service_definitions: list[ServiceDefinition[DSD]] = [] 

2326 self._seen_services: set[tuple[str, str, str]] = set() 

2327 

2328 @property 

2329 def detected_services(self) -> Sequence[ServiceDefinition[DSD]]: 

2330 return self._service_definitions 

2331 

2332 def register_service( 

2333 self, 

2334 path: VirtualPath, 

2335 name: str | list[str], 

2336 *, 

2337 type_of_service: str = "service", # "timer", etc. 

2338 service_scope: str = "system", 

2339 enable_by_default: bool = True, 

2340 start_by_default: bool = True, 

2341 default_upgrade_rule: ServiceUpgradeRule = "restart", 

2342 service_context: DSD | None = None, 

2343 ) -> None: 

2344 names = name if isinstance(name, list) else [name] 

2345 if len(names) < 1: 

2346 raise ValueError( 

2347 f"The service must have at least one name - {path.absolute} did not have any" 

2348 ) 

2349 for n in names: 

2350 key = (n, type_of_service, service_scope) 

2351 if key in self._seen_services: 

2352 raise PluginAPIViolationError( 

2353 f"The service manager (from {self._service_manager_details.plugin_metadata.plugin_name}) used" 

2354 f" the service name {n} (type: {type_of_service}, scope: {service_scope}) twice. This is not" 

2355 " allowed by the debputy plugin API." 

2356 ) 

2357 # TODO: We cannot create a service definition immediate once the manifest is involved 

2358 self._service_definitions.append( 

2359 ServiceDefinitionImpl( 

2360 names[0], 

2361 names, 

2362 path, 

2363 type_of_service, 

2364 service_scope, 

2365 enable_by_default, 

2366 start_by_default, 

2367 default_upgrade_rule, 

2368 f"Auto-detected by plugin {self._service_manager_details.plugin_metadata.plugin_name}", 

2369 True, 

2370 service_context, 

2371 ) 

2372 )