Using a dynamic version when building a Python package with Setuptools (setup.py)
Asked Answered
D

1

9

We have a big legacy project that contains some python code inside, the version of all components in the project is set during runtime by the build system which builds the project. We now need to package the python code with the same version used by the other components.
The package is currently archived using the following command python setup.py bdist_wheel with no versioning.

The question is how can we pass the dynamic version to the setup.py command during build time. Something like:

python setup.py --version x.x.x bdist_wheel

There are several options to override a version file like version.py or version.txt with the new version and then use it in the setup.py file, but assuming we can't add any more steps to the build process and can only modify the build command and the python files how can it be done?

Additional requirements:

  • In case no version is passed a default value should be used.
  • The version should be available in the python code (like having it as a __version__ parameter in the __init__.py)
  • If possible should support the usage of setup.cfg or pyproject.toml for metadata configuration
Drusilla answered 27/6, 2021 at 7:43 Comment(2)
Same question here. We have "version=0.0.0" in setup.cfg, and in ci-pipeline, we replace "0.0.0" with the build # or tag name. I'm looking for a method to build in dev environment with dynamic version #, without the need to modify the setup.cfg.Larissa
"assuming we can't add any more steps to the build process and can only modify the build command" - this restriction seems quite artificial to me. Dynamic metadata can be problematic for end users if they have to build locally from an sdist. Why not use higher-level tooling to modify pyproject.toml before the sdist build? Then the user has that result pre-computed, and wheel building with Setuptools can stick to the intended primary purpose - compiling extensions and organizing package folders.Tamekia
D
0

If you choose to only use setup.py and exclude pyproject.toml and setup.cfg, you can retrieve the version of your package using Python code in any way that you prefer. For instance, you can obtain the version by reading an environment variable (e.g. EXAMPLE_VERSION) within the setup.py script:

import os

import setuptools
from packaging.version import Version

# Try to read version string from env variable
# Default to 0.0.0.dev0 to allow for installs through `pip install -e .`
version_string = os.environ.get("EXAMPLE_VERSION", "0.0.0.dev0")
version = Version(version_string)

setuptools.setup(
    name="package-name",
    version=str(version),
    description="description",
    long_description="long_description",
    install_requires=[],
)

Code is from this gist.

Run

EXAMPLE_VERSION="0.1.0" python setup.py bdist_wheel

to build the dist package_name-0.1.0-py3-none-any.whl

However, if you want the version to be available in the variable __version__ in the __init__.py file of your module, you need to dynamically update the value of this variable when running the setup.py script. Here is an example:

import os
import re
from pathlib import Path

import setuptools
from packaging.version import Version

init_file = Path(__file__).parent.absolute() / "src/my_module/__init__.py"

# Try to read version string from env variable
# Default to 0.0.0.dev0 to allow for installs through `pip install -e .`
version_string = os.environ.get("EXAMPLE_VERSION", "0.0.0.dev0")
version = Version(version_string)

print(f"### NEW_VERSION: {version} ###")

try:
    # read __init__.py file
    with open(init_file, "r") as f:
        file_contents = f.read()

    # replace __version__ = "0.0.0" string
    new_version_string = f'__version__ = "{version}"'
    new_contents = re.sub(r'^__version__ = ".+"$', new_version_string, file_contents)

    # write content to __init__.py file again
    with open(init_file, "w") as f:
        f.write(new_contents)

    setuptools.setup(
        name="package-name",
        package_dir={"": "src"},
        packages=["my_module"],
        version=str(version),
        description="description",
        long_description="long_description",
        install_requires=[],
    )
finally:
    with open(init_file, "w") as f:
        f.write(file_contents)

Running

EXAMPLE_VERSION="0.2.0" python setup.py bdist_wheel

will build the bdist_wheel with the version 0.2.0. Before that, the __version__ variable in the __init__.py is updated. After executing the setuptools.setup command, the content of the __init__.py is restored. Please note that I'm assuming a src layout here.

I haven't played around with the pyproject.toml or setup.cfg config files, but restricting yourself to only using the setup.py allows you to write code that does what you want.

Decorate answered 14/2, 2023 at 14:35 Comment(2)
We have considered this approach, but manually updating the version in all relevant files is a less preferred option, as it varies between packages, it can appear in many places. and kind of ignores the built in mechanism that allows to pull the version inside the module files. In addition the version should be passed as a parameter to the setup.py, and not as an environment variable.Drusilla
If the code needs to know its own version number, it shouldn't be using __version__ attributes or whatever anyway. It should instead use importlib.metadata.version() to retrieve this information from the metadata that is left behind when the package is installed (in site-packages/package_name.dist-info). Therefore the first example should be all that's necessary. Speaking of which, it is possible to combine this with pyproject.toml - simply add dynamic = [ "version" ] to the [project] table.Tamekia

© 2022 - 2024 — McMap. All rights reserved.