|
13 | 13 | This repo is to support |
14 | 14 | https://github.com/scikit-build/scikit-build-core/issues/230. |
15 | 15 |
|
| 16 | +> [!WARNING] |
| 17 | +> |
| 18 | +> This plugin is still a WiP! |
| 19 | +
|
| 20 | +## For users |
| 21 | + |
| 22 | +Every external plugin must specify a "provider", which is a module that provides |
| 23 | +the API listed in the next section. |
| 24 | + |
| 25 | +```toml |
| 26 | +[tool.dynamic-metadata] |
| 27 | +<field-name>.provider = "<module>" |
| 28 | +``` |
| 29 | + |
| 30 | +There is an optional field: "provider-path", which specifies a local path to |
| 31 | +load a plugin from, allowing plugins to reside inside your own project. |
| 32 | + |
| 33 | +All other fields are passed on to the plugin, allowing plugins to specify custom |
| 34 | +configuration per field. Plugins can, if desired, use their own `tool.*` |
| 35 | +sections as well; plugins only supporting one metadata field are more likely to |
| 36 | +do this. |
| 37 | + |
| 38 | +### Example: regex |
| 39 | + |
| 40 | +An example regex plugin is provided in this package. It is used like this: |
| 41 | + |
| 42 | +```toml |
| 43 | +[build-system] |
| 44 | +requires = ["...", "dynamic-metadata"] |
| 45 | +build-backend = "..." |
| 46 | + |
| 47 | +[project] |
| 48 | +dynamic = ["version"] |
| 49 | + |
| 50 | +[tool.dynamic-metadata.version] |
| 51 | +provider = "dynamic_metadata.plugins.regex" |
| 52 | +input = "src/my_package/__init__.py" |
| 53 | +regex = '(?i)^(__version__|VERSION) *= *([\'"])v?(?P<value>.+?)\2' |
| 54 | +``` |
| 55 | + |
| 56 | +In this case, since the plugin lives inside `dynamic-metadata`, you have to |
| 57 | +include that in your requirements. Make sure the version is marked dynamic in |
| 58 | +your project table. And then you specify `version.provider`. The other options |
| 59 | +are defined by the plugin; this one takes a required `input` file and an |
| 60 | +optional `regex` (which defaults to the expression you see above). The regex |
| 61 | +optional `regex` (which defaults to the expression you see above). The regex |
| 62 | +needs to have a `"value"` named group (`?P<value>`), which it will set. |
| 63 | + |
| 64 | +## For plugin authors |
| 65 | + |
| 66 | +**You do not need to depend on dynamic-metadata to write a plugin.** This |
| 67 | +library provides testing and static typing helpers that are not needed at |
| 68 | +runtime. |
| 69 | + |
| 70 | +Like PEP 517's hooks, `dynamic-metadata` defines a set of hooks that you can |
| 71 | +implement; one required hook and two optional hooks. The required hook is: |
| 72 | + |
| 73 | +```python |
| 74 | +def dynamic_metadata( |
| 75 | + field: str, |
| 76 | + settings: dict[str, object] | None = None, |
| 77 | +) -> str | dict[str, str | None]: |
| 78 | + ... # return the value of the metadata |
| 79 | +``` |
| 80 | + |
| 81 | +The backend will call this hook in the same directory as PEP 517's hooks. |
| 82 | + |
| 83 | +There are two optional hooks. |
| 84 | + |
| 85 | +A plugin can return METADATA 2.2 dynamic status: |
| 86 | + |
| 87 | +```python |
| 88 | +def dynamic_wheel(field: str, settings: Mapping[str, Any] | None = None) -> bool: |
| 89 | + ... # Return true if metadata can change from SDist to wheel (METADATA 2.2 feature) |
| 90 | +``` |
| 91 | + |
| 92 | +If this hook is not implemented, it will default to "false". Note that "version" |
| 93 | +must always return "false". This hook is called after the main hook, so you do |
| 94 | +not need to validate the input here. |
| 95 | + |
| 96 | +A plugin can also decide at runtime if it needs extra dependencies: |
| 97 | + |
| 98 | +```python |
| 99 | +def get_requires_for_dynamic_metadata( |
| 100 | + settings: Mapping[str, Any] | None = None, |
| 101 | +) -> list[str]: |
| 102 | + ... # return list of packages to require |
| 103 | +``` |
| 104 | + |
| 105 | +This is mostly used to provide wrappers for existing non-compatible plugins and |
| 106 | +for plugins that require a CLI tool that has an optional compiled component. |
| 107 | + |
| 108 | +### Example: regex |
| 109 | + |
| 110 | +Here is the regex plugin example implementation: |
| 111 | + |
| 112 | +```python |
| 113 | +def dynamic_metadata( |
| 114 | + field: str, |
| 115 | + settings: Mapping[str, Any], |
| 116 | +) -> str: |
| 117 | + # Input validation |
| 118 | + if field not in {"version", "description", "requires-python"}: |
| 119 | + raise RuntimeError("Only string feilds supported by this plugin") |
| 120 | + if settings > {"input", "regex"}: |
| 121 | + raise RuntimeError("Only 'input' and 'regex' settings allowed by this plugin") |
| 122 | + if "input" not in settings: |
| 123 | + raise RuntimeError("Must contain the 'input' setting to perform a regex on") |
| 124 | + if not all(isinstance(x, str) for x in settings.values()): |
| 125 | + raise RuntimeError("Must set 'input' and/or 'regex' to strings") |
| 126 | + |
| 127 | + input = settings["input"] |
| 128 | + # If not explicitly specified in the `tool.dynamic-metadata.<field-name>` table, |
| 129 | + # the default regex provided below is used. |
| 130 | + regex = settings.get( |
| 131 | + "regex", r'(?i)^(__version__|VERSION) *= *([\'"])v?(?P<value>.+?)\2' |
| 132 | + ) |
| 133 | + |
| 134 | + with Path(input).open(encoding="utf-8") as f: |
| 135 | + match = re.search(regex, f.read()) |
| 136 | + |
| 137 | + if not match: |
| 138 | + raise RuntimeError(f"Couldn't find {regex!r} in {file}") |
| 139 | + |
| 140 | + return match.groups("value") |
| 141 | +``` |
| 142 | + |
| 143 | +## For backend authors |
| 144 | + |
| 145 | +**You do not need to depend on dynamic-metadata to support plugins.** This |
| 146 | +library provides some helper functions you can use if you want, but you can |
| 147 | +implement them yourself following the standard provided or vendor the helper |
| 148 | +file (which will be tested and supported). |
| 149 | + |
| 150 | +You should collect the contents of `tool.dynamic-metadata` and load each, |
| 151 | +something like this: |
| 152 | + |
| 153 | +```python |
| 154 | +def load_provider( |
| 155 | + provider: str, |
| 156 | + provider_path: str | None = None, |
| 157 | +) -> DynamicMetadataProtocol: |
| 158 | + if provider_path is None: |
| 159 | + return importlib.import_module(provider) |
| 160 | + |
| 161 | + if not Path(provider_path).is_dir(): |
| 162 | + msg = "provider-path must be an existing directory" |
| 163 | + raise AssertionError(msg) |
| 164 | + |
| 165 | + try: |
| 166 | + sys.path.insert(0, provider_path) |
| 167 | + return importlib.import_module(provider) |
| 168 | + finally: |
| 169 | + sys.path.pop(0) |
| 170 | + |
| 171 | + |
| 172 | +for dynamic_metadata in settings.metadata.values(): |
| 173 | + if "provider" in dynamic_metadata: |
| 174 | + config = dynamic_metadata.copy() |
| 175 | + provider = config.pop("provider") |
| 176 | + provider_path = config.pop("provider-path", None) |
| 177 | + module = load_provider(provider, provider_path) |
| 178 | + # Run hooks from module |
| 179 | +``` |
| 180 | + |
16 | 181 | <!-- prettier-ignore-start --> |
17 | 182 | [actions-badge]: https://github.com/scikit-build/dynamic-metadata/workflows/CI/badge.svg |
18 | 183 | [actions-link]: https://github.com/scikit-build/dynamic-metadata/actions |
|
0 commit comments