pythonpython-3.xsetuptoolspyproject.toml

Injecting build date automatically when building WHL with setuptools?


I have a simple Python project that is often updated, so I need to track its version number and display it in the runtime. I store version and build date in __init__.py in project's root:

__version__ = "1.0.6"
__date__ = "2025-10-08 18:33"

This works well, however, when I'm just about to build the wheel file, I need to update these 2 values manually. For __version__ I don't mind, but can __date__ be somehow set automatically?

I build my project simply with python -m build --wheel. Below are the relevant sections of my pyproject.toml. I don't have setup.py file at all.

[project]
name = "MyProject"
dynamic = ["version"]

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
"*" = ["*.html", "*.ini", "*.json"]

[tool.setuptools.dynamic]
version = { attr = "myproject.__version__" }

Or maybe there's another way, like checking timestamps of dist-info meta files inside the WHL package?


Solution

  • Thanks @sakshi-sharma for your informative tips! Here's my fully working solution.

    Eventually, I've decided that it's better to create a new file containing build timestamp (and add it to .gitignore) rather than update an existing one. Also, I prefer having it as a data file rather than .py (in case I want to ignore it being missing - like, when running my project from the IDE).

    So, in the end, I am creating a .dotenv-like file src/myproject/.build_info containing key like TIMESTAMP=2025-10-11 21:37:57 each time I execute build --wheel.

    Changes to pyproject.toml:

    dependencies = [
        ...more stuff...
        "python-dotenv>=1.1.0",
    ]
    
    [build-system]
    requires = ["setuptools"]  # no "wheel" needed
    build-backend = "setuptools_build_hook"
    backend-path = ["."]  # important!
    
    [tool.setuptools.package-data]
    "*" = [
        ...more stuff...,
        ".build_info",
    ]
    

    New file setuptools_build_hook.py in project's root:

    """
    Setuptools build hook wrapper that writes file `src/myproject/.build_info`
    containing build timestamp when building WHL files with `build --wheel`.
    """
    
    from datetime import datetime
    from os import PathLike
    from pathlib import Path
    
    from setuptools import build_meta
    
    
    def build_wheel(
            wheel_directory: str | PathLike[str],
            config_settings: dict[str, str | list[str] | None] | None = None,
            metadata_directory: str | PathLike[str] | None = None,
    ) -> str:
        """Creates file `src/myproject/.build_info` with key TIMESTAMP, then proceeds normally."""
        Path("src/myproject/.build_info").write_text(f"TIMESTAMP={datetime.now():%Y-%m-%d %H:%M:%S}\n", encoding="utf-8")
        print("* Written .build_info.")
        return build_meta.build_wheel(wheel_directory, config_settings, metadata_directory)
    
    
    # Proxy (wrappers) for setuptools.build_meta
    get_requires_for_build_wheel = build_meta.get_requires_for_build_wheel
    get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist
    prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel
    build_sdist = build_meta.build_sdist
    get_requires_for_build_editable = build_meta.get_requires_for_build_editable
    prepare_metadata_for_build_editable = build_meta.prepare_metadata_for_build_editable
    build_editable = build_meta.build_editable
    

    And now, how to read this value in runtime:

    import myproject as this_package
    
    
    from io import StringIO
    build_timestamp: str | None = None
    # noinspection PyBroadException
    try:
        build_timestamp = dotenv_values(stream=StringIO(resources.files(this_package).joinpath(".build_info")
                                                        .read_text(encoding="utf-8")))["TIMESTAMP"]
    except Exception:
        pass