Bundled plugins¶
This package ships eight plugins. ast, regex, template, from_file, and
substitute are generic — they read their target from a field setting.
static writes values straight from its settings; readme_fragment and
pin_installed are single-purpose and always write readme and dependencies
respectively. Because they live inside dynamic-metadata, you must add
dynamic-metadata to your [build-system].requires to use them.
Each registers a provider name of dynamic_metadata. plus the heading below, so
the regex plugin is provider = "dynamic_metadata.regex". The examples use
these names.
Entries run in order and each sees the project resolved so far, so several
entries can cooperate on one field: readme_fragment and substitute build a
readme the way hatch-fancy-pypi-readme assembles one — one entry per
fragment or substitution rather than a nested list.
regex¶
dynamic_metadata.regex extracts a value from a file with a regular expression.
By default it pulls a version out of a __version__/VERSION assignment.
[project]
dynamic = ["version"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.regex"
field = "version"
input = "src/my_package/__init__.py"
Settings (all values must be strings):
Setting |
Required |
Description |
|---|---|---|
|
yes |
The metadata field to set. |
|
yes |
The file to read. |
|
unless version |
The pattern to search for. Must capture a |
|
no ( |
A |
|
no |
A regex stripped from the result. |
The search runs in re.MULTILINE mode. When the target field is not a string
field, result is applied across the container shape the field requires (each
string in a list, each value in a table, and so on).
ast¶
dynamic_metadata.ast reads the literal value assigned to a module-level global
in a Python file. The file is parsed with ast, never imported, so it
works without the package (or its dependencies) being importable in the build
environment.
[project]
dynamic = ["version"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.ast"
field = "version"
input = "src/my_package/__init__.py"
name = "__version__"
Settings (all values must be strings):
Setting |
Required |
Description |
|---|---|---|
|
yes |
The metadata field to set. |
|
yes |
The Python file to parse. |
|
yes |
The global to read. |
Only assignments at module scope are considered (including annotated ones like
__version__: str = "1.2.3"); if the name is assigned more than once, the last
assignment wins, as it would when executing the file. The value must be a
literal accepted by ast.literal_eval() — a call like get_version() is an
error.
Unlike regex, which always extracts a string, the value keeps its Python
shape, so a list or table field can be filled directly — for example
field = "keywords" from KEYWORDS = ["science", "build"]. Tuples are
converted to lists. The shape must match what the field requires.
template¶
dynamic_metadata.template fills a str.format template from fields resolved
by earlier entries, demonstrating cross-field references.
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.template"
field = "readme"
result = "{project[name]} {project[version]}"
Settings:
Setting |
Required |
Description |
|---|---|---|
|
yes |
The metadata field to set. |
|
yes |
A |
Only fields produced by earlier entries (or static values already in
[project]) are available — a forward reference raises a KeyError.
from_file¶
dynamic_metadata.from_file fills a field with the contents of a file. The file
is interpreted by the shape of the target field:
A string field gets the file’s contents, stripped of surrounding whitespace — the classic
VERSIONfile.A list field gets one item per line, requirements.txt-style —
dependenciesfrom arequirements.txt.A table field names the key to fill after a dot in
field, one entry per key:optional-dependencies.testfills thetestextra with one requirement per line, and aurls/scripts/gui-scriptskey takes the stripped file contents like a string field.
[project]
dynamic = ["version", "dependencies", "optional-dependencies"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.from_file"
field = "version"
path = "VERSION"
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.from_file"
field = "dependencies"
path = "requirements.txt"
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.from_file"
field = "optional-dependencies.test"
path = "requirements-test.txt"
Settings (all values must be strings):
Setting |
Required |
Description |
|---|---|---|
|
yes |
The metadata field to set; a table field takes the key after a dot ( |
|
yes |
The file to read (UTF-8). |
Line parsing follows the requirements.txt conventions: blank lines and #
comments (at line start or preceded by whitespace) are dropped, and a trailing
backslash joins a line with the next. pip options (-r other.txt, -e .,
--index-url) are not requirements and raise an error — to combine several
files, use one entry per file: list fields append across entries, entries for
the same extra append, and each extra is its own entry.
Fields whose values aren’t flat text — readme (use
readme_fragment), entry-points, authors, and
maintainers — are rejected.
static¶
dynamic_metadata.static sets fields directly from its own settings — an
alternative to writing them in [project]. Each setting is a metadata field
mapped to its value, returned verbatim.
[project]
dynamic = ["version", "description"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.static"
version = "1.2.3"
description = "My package"
Settings: any settable metadata field maps to the value to give it. The fields
must be listed in project.dynamic like every dynamic field, and values use the
same shape they would in [project] — a string for version, a list for
keywords, a table for readme, and so on.
This is mainly useful as the first half of a pipeline: it gives a later entry
like substitute a dynamic value to transform, which a field set in
[project] cannot be (a scalar field may not be both static and dynamic).
[project]
dynamic = ["version"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.static"
version = "1.2.3-beta"
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.substitute"
field = "version"
pattern = "-beta$"
replacement = "b0"
It can also keep metadata out of [project], hiding it from tools that read
[project] directly.
readme_fragment¶
dynamic_metadata.readme_fragment builds a readme from an ordered series of
fragments, each its own entry. Every entry appends to the readme produced by the
entries before it, so a heading, a slice of a file, and a changelog excerpt can
be stitched together. An entry with text is a literal fragment; an entry with
path reads a file and may slice it.
[project]
dynamic = ["readme"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.readme_fragment"
content-type = "text/markdown"
text = "# My Project\n\n"
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.readme_fragment"
path = "README.md"
start-after = "<!-- start -->\n"
end-before = "\n<!-- end -->"
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.readme_fragment"
path = "CHANGELOG.md"
pattern = "(## .*?)(?=\n## )"
Settings (all values are strings):
Setting |
Required |
Description |
|---|---|---|
|
one of text/path |
A literal fragment, used verbatim. |
|
one of text/path |
A file to read (UTF-8) as the fragment, optionally sliced by the keys below. |
|
no ( |
The readme content type. Consulted when the first fragment creates the readme. |
|
no |
Drop everything up to and including this marker (file fragments). Excludes |
|
no |
Drop everything before this marker, keeping it (file fragments). Excludes |
|
no |
Keep everything before this marker (file fragments). Excludes |
|
no |
Keep everything through this marker (file fragments). Excludes |
|
no |
A regex searched with |
Slicing is applied in order: start, then end, then pattern. A missing marker
or a non-matching pattern raises a RuntimeError.
pin_installed¶
dynamic_metadata.pin_installed pins runtime dependencies to the version of a
package installed in the build environment. This is the classic
compiled-extension workflow: a wheel built against the pytorch (or historically
numpy) ABI must require a matching version at runtime, and that version is only
known when the wheel is built.
[project]
dynamic = ["dependencies"]
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.pin_installed"
packages = ["torch==x.x.*"]
Settings:
Setting |
Required |
Description |
|---|---|---|
|
yes |
A list of requirement templates, resolved against the build environment. |
Each template is a distribution name followed by a specifier set whose version components are, dot-separated:
x— the corresponding release component of the installed version (0if the release is shorter). Epoch, pre/post/dev markers, and local version segments (+cu126) are ignored.x+N— that component plusN, for an upper bound like<x+1.*— a literal PEP 440 wildcard; only valid as the last component with==or!=.a literal number, passed through.
With torch 2.7.1 installed, "torch==x.x.*" resolves to torch==2.7.*, and
"numpy>=x.x.x,<x+1" with numpy 1.26.4 resolves to numpy>=1.26.4,<2 (the
numpy ABI recommendation). The resolved requirements are appended to any static
dependencies (PEP 808).
The plugin implements both optional collection hooks:
get_requires_for_dynamic_metadatarequests the bare names, so the packages are present to be inspected. Constrain which version is used for the build in[build-system].requires(or by pre-installing with--no-build-isolation); the plugin pins the runtime requirement to whatever was resolved.dynamic_wheelreportsdependenciesas dynamic, so an SDist’sPKG-INFOmarksRequires-DistasDynamic(METADATA 2.2) — the pins legitimately differ per build environment, so installers must not trust the SDist’s value.
substitute¶
dynamic_metadata.substitute applies a single regex substitution to a field
already produced by an earlier entry, the way fancy-pypi-readme touches up an
assembled readme (for example, turning #123 into a link).
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.substitute"
field = "readme"
pattern = "#(\\d+)"
replacement = "[#\\1](https://github.com/org/repo/issues/\\1)"
Settings:
Setting |
Required |
Description |
|---|---|---|
|
yes |
The field to transform. Must be a scalar field (see below). |
|
yes |
The regex to replace, applied with |
|
yes |
The replacement; backreferences such as |
|
no ( |
Match case-insensitively. |
|
no ( |
Resolve |
With format = true, replacement is run through str.format(project=...)
before substitution, so it can pull in fields produced by earlier entries — the
same {project[...]} syntax as template. Backreferences keep
working alongside it (braces and backslashes don’t collide):
[[tool.dynamic-metadata]]
provider = "dynamic_metadata.substitute"
field = "readme"
pattern = "#(\\d+)"
replacement = "[#\\1](https://github.com/org/repo/v{project[version]}/issues/\\1)"
format = true
It is opt-in because formatting makes { and } special: with format = true
a literal brace in the replacement must be doubled ({{ / }}), as with any
str.format string. Leave it off (the default) to use the replacement verbatim.
field must be a single-value field — a string field (version, description,
requires-python, license) or readme — and must already hold a value from
an earlier entry. List and table fields are rejected: the backend appends a
provider’s contribution to those, so re-emitting a whole transformed value would
duplicate it. For readme the substitution is applied across the table, so
anchor patterns to the body text rather than the content type.
Warning
substitute only works on a dynamic field produced by an earlier entry. A
field set statically in [project] cannot be modified — a scalar field may not
be both static and dynamic (PEP 808), so substituting one is an error.