Skip to content

Metadata hooks should run even when project.dynamic is empty #2153

@bilelomrani1

Description

@bilelomrani1

Problem

Metadata hooks only run when project.dynamic is non-empty. This is a problem for plugins that need to modify existing static metadata rather than populate dynamic fields.

In hatchling/metadata/core.py:

if metadata.dynamic:
    for metadata_hook in metadata_hooks.values():
        metadata_hook.update(self.core_raw_metadata)

Even if [tool.hatch.metadata.hooks.my-plugin] is configured, the hook never runs unless project.dynamic has at least one entry. This came up in #2038 where users found this requirement counter-intuitive.

Use case

Consider a plugin that rewrites workspace dependency versions at build time:

[project]
name = "my-client"
dependencies = ["my-core"]  # workspace dependency, no version

[tool.uv.sources]
my-core = { workspace = true }

[tool.hatch.metadata.hooks.my-plugin]
...

The plugin reads dependencies from [project].dependencies, looks up my-core's version from the lockfile, and outputs my-core>=1.2.3 in the built wheel.

Keeping dependencies in [project].dependencies is essential. This is the standard location per PEP 621, and the entire Python tooling ecosystem expects dependencies there. Moving them elsewhere breaks interoperability with many tools, for instance:

Current workarounds

Dummy PEP 621 field:

[project]
dynamic = ["license-files"]  # not actually dynamic, just triggers hooks

Misleading, but passes schema validation.

Arbitrary string (used by hatch-dependency-coversion), from their official docs:

[project]
# the dynamic entry must be present and the array must not be empty, otherwise hatch
# will not invoke the plugin. however, something in dynamic cannot be in the rest of
# the metadata, and only top-level keys can appear here - so if you did
# dynamic = ['dependencies'], then it (a) would not be true since it's not all the
# dependencies, just the versions of some of them and (b) you couldn't have a
# dependencies entry in the project table. so put an arbitrary string here, and the
# name of the plugin is as good an arbitrary string as any.
dynamic = ['dependency-coversioning']

This works but fails schema validation in editors like Even Better TOML, and looks like a hack.

Declare dependencies as dynamic (used by uv-dynamic-versioning):

[project]
dynamic = ["dependencies"]

[tool.hatch.metadata.hooks.uv-dynamic-versioning]
dependencies = ["my-core=={{ version }}"]

This moves dependencies out of [project].dependencies entirely, which breaks all the tooling mentioned above.

Proposal

Run metadata hooks whenever [tool.hatch.metadata.hooks.*] is configured, regardless of project.dynamic. The existing validation that prevents hooks from adding undeclared fields would still apply, this just changes whether hooks run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions