Coverage for src/debputy/plugin/api/experimental.py: 57%
29 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
1import re
2from typing import Any, cast
3from collections.abc import Callable, Iterable, Iterator
5from debputy.exceptions import PluginInitializationError
6from debputy.plugin.api import VirtualPath
7from debputy.plugin.api.impl import DebputyPluginInitializerProvider
8from debputy.plugin.api.spec import (
9 DebputyPluginDefinition,
10 PackageProcessor,
11 PackageTypeSelector,
12 DebputyPluginInitializer,
13)
16class ExperimentalDebputyPluginDefinition(DebputyPluginDefinition):
17 """Plugin definition entity for plugins that use experimental API
19 The plugin definition provides a way for the plugin code to register the features it provides via
20 accessors. This particular instance also provides accessors for experimental or yet-to-be-final
21 APIs. These APIs generally requires the plugin to be pre-registered with `debputy`, so `debputy`
22 can add `Breaks` on the plugin provider if/when the API changes.
23 """
25 def package_processor(
26 self,
27 func: PackageProcessor | None = None,
28 *,
29 processor_id: str | None = None,
30 depends_on_processor: Iterable[str] = tuple(),
31 package_type: PackageTypeSelector = "deb",
32 ) -> Callable[[PackageProcessor], PackageProcessor] | PackageProcessor:
33 """Provide a pre-assembly hook that can affect the contents of the binary ("deb") packages
35 The provided hook will be run once per binary package to be assembled, and it can see all the content
36 ("data.tar") planned to be included in the deb. It has read-write access to the data contents.
38 The hook will be run unconditionally for all binary packages built. When the hook does not apply to all
39 packages, it must provide its own (internal) logic for detecting whether it is relevant and reduce itself
40 to a no-op if it should not apply to the current package.
42 Hooks between plugins run in "some implementation defined order" and should not rely on being run before
43 or after any other hook. However, the hooks can depend on other hooks from the same plugin by using
44 the `depends_on_processor`.
46 The hooks are only applied to packages defined in `debian/control`. Notably, the processor will
47 not apply to auto-generated `-dbgsym` packages (as those are not listed explicitly in `debian/control`).
49 >>> plugin_definition = define_debputy_plugin_experimental_api()
50 >>> def _la_files(fs_root: VirtualPath) -> Iterator[VirtualPath]:
51 ... ... # Implementation skipped here for brevity
52 >>>
53 >>> @plugin_definition.package_processor
54 ... def clean_la_files(
55 ... fs_root: "VirtualPath",
56 ... _: Any, # Not yet defined/provided
57 ... context: "PackageProcessingContext",
58 ... ) -> None:
59 ... for path in _la_files(fs_root):
60 ... buffer = []
61 ... with path.open(byte_io=True) as fd:
62 ... replace_file = False
63 ... for line in fd:
64 ... if line.startswith(b"dependency_libs"):
65 ... replacement = re.sub(b"<real_pattern_here>", b"''", line)
66 ... if replacement != line:
67 ... replace_file = True
68 ... line = replacement
69 ... buffer.append(line)
70 ... if not replace_file:
71 ... continue
72 ... with path.replace_fs_path_content() as fs_path, open(fs_path, "wb") as wfd:
73 ... wfd.writelines(buffer)
75 :param func: The function to be decorated/registered.
76 :param processor_id: A plugin-wide unique ID for this detector. Packagers may use this ID for disabling
77 the detector and accordingly the ID is part of the plugin's API toward the packager.
78 :param depends_on_processor: Denote processors from the same plugin that must run before this processor.
79 It is not possible to depend on processors from other plugins.
80 :param package_type: Which kind of packages this metadata detector applies to. The package type is generally
81 defined by `Package-Type` field in the binary package. The default is to only run for regular `deb` packages
82 and ignore `udeb` packages.
83 """
85 def _decorate(f: PackageProcessor) -> PackageProcessor:
87 final_id = self._name2id(processor_id, f.__name__)
89 def _init(api: "DebputyPluginInitializer") -> None:
90 assert isinstance(api, DebputyPluginInitializerProvider)
91 api.package_processor(
92 final_id,
93 f,
94 depends_on_processor=depends_on_processor,
95 package_type=package_type,
96 )
98 self._generic_initializers.append(_init)
100 return f
102 if func: 102 ↛ 104line 102 didn't jump to line 104 because the condition on line 102 was always true
103 return _decorate(func)
104 return _decorate
106 def initialize(self, api: "DebputyPluginInitializer") -> None:
107 """Initialize the plugin from this definition
109 Most plugins will not need this function as the plugin loading will call this function
110 when relevant. However, a plugin can call this manually from a function-based initializer.
111 This is mostly useful if the function-based initializer need to set up a few features
112 that the `DebputyPluginDefinition` cannot do and the mix/match approach is not too
113 distracting for plugin maintenance.
115 :param api: The plugin initializer provided by the `debputy` plugin system
116 """
118 initializers = self._generic_initializers
119 assert isinstance(api, DebputyPluginInitializerProvider)
120 if not initializers:
121 plugin_name = api.plugin_metadata.plugin_name
122 raise PluginInitializationError(
123 f"Initialization of {plugin_name}: The plugin definition was never used to register any features."
124 " If you want to conditionally register features, please use an initializer functon instead."
125 )
127 for initializer in initializers:
128 initializer(api)
131def define_debputy_plugin_experimental_api() -> ExperimentalDebputyPluginDefinition:
132 return ExperimentalDebputyPluginDefinition()