uv-dynamic-versioning

repository·main·Indexed 41 days ago

https://github.com/ninoseki/uv-dynamic-versioning

A dynamic versioning tool for uv and hatch projects powered by dunamai. It automates project versioning and dependency management based on Version Control System (VCS) state. It provides a version source plugin to set project versions and a metadata hook plugin to dynamically define dependencies and optional-dependencies using Jinja2 templates.

Tokens
6.8K
Snippets
28
Records
34
Agent score
53%

What's inside uv-dynamic-versioning

  1. Overview of uv-dynamic-versioning plugins

    main

    The project provides two distinct plugins to handle dynamic project metadata:

    1. Version source plugin: Used to determine and set the project version based on your Version Control System (VCS).
    2. Metadata hook plugin: Used to dynamically set dependencies and optional-dependencies based on the VCS version. This is particularly useful in monorepo architectures where dependencies might change depending on the version or context.
  2. Pin build dependencies for supply chain security

    main

    To protect against supply chain attacks, you can use uv's build-constraint-dependencies to pin the dependencies used during the build process (e.g., hatchling and uv-dynamic-versioning).

    1. Generate the pinned dependency list using the following command:
    $ printf '%s\n' hatchling uv-dynamic-versioning | uv pip compile - --no-annotate --no-header --quiet | jq -R -s 'split("\n") | map(select(length > 0))'
    1. Copy the resulting JSON array output.
    2. Paste it into your pyproject.toml under the [tool.uv] section.
    $ printf '%s\n' hatchling uv-dynamic-versioning | uv pip compile - --no-annotate --no-header --quiet | jq -R -s 'split("\n") | map(select(length > 0))'
    [tool.uv]
    build-constraint-dependencies = [
      # Paste output here
    ]
  3. Set the `__version__` attribute using `importlib.metadata`

    main

    You can expose your package version via a __version__ attribute in your __init__.py by reading it from the installed package metadata.

    Note: importlib.metadata can be relatively slow. Avoid this method if performance is critical. Additionally, this approach may fail if the package is installed in development mode. To handle cases where the package is not yet installed (e.g., during local development), use a try/except block to catch importlib.metadata.PackageNotFoundError and provide a fallback version.

    import importlib.metadata
    
    try:
        __version__ = importlib.metadata.version(__name__)
    except importlib.metadata.PackageNotFoundError:
        __version__ = "0.0.0"
  4. Handle sandboxed builds (Nix) by patching fallback-version

    main

    In sandboxed environments like Nix, the .git directory is unavailable during builds. Since uv sync and uv build trigger the build backend, they will fail with:

    RuntimeError: Error getting the version from source uv-dynamic-versioning: This does not appear to be a Git project

    Workaround: Resolve the version outside the sandbox and patch the fallback-version in pyproject.toml before entering the sandbox. This requires a fallback-version to already exist in your configuration.

    1. Ensure your config has a fallback:
    [tool.uv-dynamic-versioning]
    fallback-version = "0.0.0"
    1. Run the following commands outside the sandbox:

    Linux (GNU sed):

    VERSION=$(uvx uv-dynamic-versioning)
    sed -i "s|^fallback-version = \".*\"|fallback-version = \"$VERSION\"|" pyproject.toml

    macOS (BSD sed):

    VERSION=$(uvx uv-dynamic-versioning)
    sed -i '' "s|^fallback-version = \".*\"|fallback-version = \"$VERSION\"|" pyproject.toml
    # Linux (GNU sed):
    VERSION=$(uvx uv-dynamic-versioning)
    sed -i "s|^fallback-version = \".*\"|fallback-version = \"$VERSION\"|" pyproject.toml
    
    # macOS (BSD sed):
    VERSION=$(uvx uv-dynamic-versioning)
    sed -i '' "s|^fallback-version = \".*\"|fallback-version = \"$VERSION\"|" pyproject.toml
  5. Set the `__version__` attribute using a Hatch Version Build Hook

    main

    To write the version to a specific file during the build process, you can use Hatch's official version build hook. This allows you to generate a version file (e.g., _version.py) that your code can then import.

    Important: The generated version file should not be committed to Version Control (VCS). You should add the path of the generated file to your .gitignore.

    [tool.hatch.build.hooks.version]
    path = "path/to/_version.py"
    template = '''
    version = "{version}"
    '''
  6. Set up uv-dynamic-versioning with Hatchling

    main

    To use uv-dynamic-versioning to set your project version based on your Version Control System (VCS), you must configure your pyproject.toml to use hatchling as the build backend and declare the version as dynamic.

    1. Update [build-system] to require both hatchling and uv-dynamic-versioning.
    2. In the [project] section, remove the static version key and add version to the dynamic list.

    Example configuration:

    [build-system]
    requires = ["hatchling", "uv-dynamic-versioning"]
    build-backend = "hatchling.build"
    
    [project]
    name = "your-package"
    dynamic = ["version"]
    [build-system]
    requires = ["hatchling", "uv-dynamic-versioning"]
    build-backend = "hatchling.build"
    
    [project]
    name = "..."
    dynamic = ["version"]
  7. Use the Metadata Hook to set dynamic dependencies

    main

    The uv-dynamic-versioning metadata hook allows you to dynamically define dependencies and optional-dependencies using Jinja2 templates based on your VCS version.

    To use the hook, you must:

    1. Add [tool.hatch.metadata.hooks.uv-dynamic-versioning] to your pyproject.toml.
    2. Define your dependencies as Jinja2 templates within that section.
    3. Remove the static dependencies list from the [project] section.
    4. Add dependencies to the project.dynamic list in pyproject.toml to signal that they are managed dynamically.
    [project]
    name = "..."
    dynamic = ["dependencies"]
    
    [tool.hatch.metadata.hooks.uv-dynamic-versioning]
    dependencies = ["foo=={{ version }}"]
  8. Install uv-dynamic-versioning

    main

    To use uv-dynamic-versioning, you must update or add the build-system section in your project's configuration file (e.g., pyproject.toml) to include uv-dynamic-versioning in the requires list. This project is designed to work with hatchling as the build backend.

    Note: This plugin currently does not work with the uv build backend (see astral-sh/uv#14561).

    [build-system]
    requires = ["hatchling", "uv-dynamic-versioning"]
    build-backend = "hatchling.build"
  9. Configure dynamic dependencies and optional-dependencies

    main

    The metadata hook supports two main configuration keys. Both require the corresponding field to be listed in project.dynamic within your pyproject.toml.

    • dependencies: A list of Jinja2 templates defining your project's core dependencies.
    • optional-dependencies: A list of Jinja2 templates defining your project's optional dependencies.

    Both keys have access to the same set of Jinja2 variables and functions provided by the underlying versioning engine.

  10. Format the version output

    main

    You can control the final version string using format (simple substitutions) or format-jinja (complex logic).

    Simple Formatting (format)

    Use placeholders like {base}, {stage}, {revision}, {distance}, {commit}, {dirty}, {tagged_metadata}, {branch}, and {timestamp}.

    format = "v{base}+{distance}.{commit}"

    Advanced Formatting (format-jinja)

    Use Jinja2 templates for complex logic. When format-jinja is set, format is ignored. Available variables include version (a dunamai.Version object), base, stage, revision, distance, commit, dirty, env, branch, and timestamp.

    Example: Conditional formatting based on distance

    format-jinja = "{% if distance == 0 %}{{ base }}{% else %}{{ base }}+{{ distance }}.{{ commit }}{% endif %}"
  11. Configure uv cache keys for dynamic metadata

    main

    Because uv-dynamic-versioning makes your project's metadata dynamic, you should configure uv's cache keys to ensure the cache is invalidated correctly when your version source changes.

    Add cache-keys to your [tool.uv] configuration to track changes in pyproject.toml and Git commits/tags.

    [tool.uv]
    cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true }}]
  12. Read version from a file instead of VCS

    main

    Instead of using Git or other VCS, you can use the from-file section to read the version from a specific file.

    • source (string): Path to the file relative to pyproject.toml.
    • pattern (string): A regular expression to extract the version. The first capture group must contain the version.
    [tool.uv-dynamic-versioning.from-file]
    source = "VERSION.txt"
    pattern = '(?P<version>\d+\.\d+\.\d+)'