How to specify version in only one place when using pyproject.toml?
Asked Answered
S

6

72

My package version is defined in two places:

  • __version__ = 1.2.3 in mypackage/__init__.py
  • version = "1.2.3" in pyproject.toml (I am using Poetry)

I have to update both whenever I bump the version which is annoying and not DRY. Is there a way to make Python read the version from the TOML, or to make the TOML read the version from Python?

Semanteme answered 14/4, 2021 at 3:18 Comment(1)
You sniffed something fishy and you were right. Why would you need another place to track your project's metadata if you already use pyproject.toml?Chthonian
B
66

After you have installed your project - either in editable mode by poetry install or from the wheel - you can access several metadata via importlib.metadata (importlib_metadata for python < 3.8).

So keep the version only in the pyproject.toml and use this in your python code:

import importlib.metadata

__version__ = importlib.metadata.version("mypackage")
Bisexual answered 14/4, 2021 at 18:19 Comment(2)
Thanks! Out of curiosity, where would I find this information in the docs? python-poetry.org/docs/pyproject/#version is quite terse.Semanteme
This works quite well until it runs in a GitLab pipeline. Any clues on how to do this without the ability to install the package?Transplant
S
16

This code worked for me:

import importlib.metadata

__version__ = importlib_metadata.version(__package__ or __name__)

However, this only works if the package is already installed using pip or poetry.

On newer version (dot instead of underscore):

__version__ = importlib.metadata.version(__package__ or __name__)
Stockmon answered 15/2, 2022 at 12:56 Comment(3)
would this work for editable installs? from what i am seeing when editable install and change version in pyproject.toml wouldnt be reflected?Bisutun
@Bisutun it is reflected, but only after re-running pip install -Ue [package name], as far as I've been able to tell.Sabadilla
@Sabadilla that is what i noticed as well, but doesnt requiring to run upgrade install bit counter what editable install is meant to be (where thought one of goals would be you editable install once, otherwise if have to run install upgrade each time make change like this what is difference - again know this limited to changes in pyproject.toml). i could be misunderstanding expectations of editable install thoughBisutun
O
7

Maybe overly complicated, but in order to not confuse an installed version of a package with an instance lingering around locally I use this code:


from contextlib import suppress
import importlib.metadata
from pathlib import Path


def extract_version() -> str:
    """Returns either the version of installed package or the one
    found in nearby pyproject.toml"""
    with suppress(FileNotFoundError, StopIteration):
        with open((root_dir := Path(__file__).parent.parent)
                  / "pyproject.toml", encoding="utf-8") as pyproject_toml:
            version = (
                next(line for line in pyproject_toml if line.startswith("version"))
                .split("=")[1]
                .strip("'\"\n ")
            )
            return f"{version}-dev (at {root_dir})"
    return importlib.metadata.version(__package__
                                      or __name__.split(".", maxsplit=1)[0])

__version__ = extract_version()

Giving me either 1.2.23 for installed packages or something like 1.3.42-dev (at /project/location)

Overleap answered 9/5, 2023 at 5:46 Comment(5)
This needs something imported right?Ganef
from contexlib import suppress and importlibOverleap
tomli would be a much better tool for doing this - see this answerBalcer
that's right and you should do so if you don't want to avoid extra dependencies just for displaying a development-versionOverleap
Clever solution but also somewhat dangerous. The __version__ variable may be consumed by build tools, too. Inserting a -dev (at path/to/projectroot) suffix may break the build in the best case (because it detects the PEP 440 violation, like pybuild does) or it may silently leak the suffix into production in the worst case. A safer approach may be to have extract_version() return a clean version number without a suffix, and instead have your --version handler check for the presence of a pyproject.toml and do the work.Pharyngology
Q
3

All of the current answers address the issue of getting the version after install. However, as Jonathan Belden points out in a comment on @finswimmer's answer, these methods break in CI pipelines and the like when the package isn't installed. In keeping with @z33k's comment on the question, one solution would be to read the value from pyproject.toml.

With poetry as your package manager, another solution would be to use the poetry-bumpversion plugin to manage version bumps using poetry's version command. For example, say you have a package called widget, with __version__ defined in widget/__init__.py, with the same value as pyproject.toml has for version. With the poetry-bumpversion plugin, you would add

[tool.poetry_bumpversion.file."widget/__init__.py"]

to pyproject.toml, then

% poetry version patch
Bumping version from 0.7.9 to 0.7.10
poetry_bumpversion: processed file widget/__init__.py

% git diff -U0
diff --git a/widget/__init__.py b/widget/__init__.py
index 18c7cbf..9ff9982 100644
--- a/widget/__init__.py
+++ b/widget/__init__.py
@@ -1 +1 @@
-__version__ = "0.7.9"
+__version__ = "0.7.10"
diff --git a/pyproject.toml b/pyproject.toml
index 1b86c6e..5de1ce1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3 +3 @@ name = "widget"
-version = "0.7.9"
+version = "0.7.10"
@@ -55,0 +56,2 @@ tox = "^4.6.4"
+[tool.poetry_bumpversion.file."widget/__init__.py"]
+

Queensland answered 14/7, 2023 at 23:5 Comment(0)
E
0

I provided the answer to the same question in the similar discussion. Here is a copy:


In the project.toml you can specify version in different ways:

  • Poetry expects version in the [tool.poetry]:
[tool.poetry]
name = "my-package"
version = "0.1.0"
[project]
name = "my-package"
version = "2020.0.0"

The next point is that you may want to see the version of installed packages (e.g. via pip or poetry) or your application. The next:

import importlib.metadata

version = importlib.metadata.version("my-package")
print(version)

This code works fine with installed packages. To make working it with your project, you should install it using pip install -e . or something similar.

If you want to check version of your project without installing, then you can use the following snippet:

from pathlib import Path
import toml

version = "unknown"
# adopt path to your pyproject.toml
pyproject_toml_file = Path(__file__).parent / "pyproject.toml"
if pyproject_toml_file.exists() and pyproject_toml_file.is_file():
    data = toml.load(pyproject_toml_file)
    # check project.version
    if "project" in data and "version" in data["project"]:
        version = data["project"]["version"]
    # check tool.poetry.version
    elif "tool" in data and "poetry" in data["tool"] and "version" in data["tool"]["poetry"]:
        version = data["tool"]["poetry"]["version"]
print(version)

To make it working you should install toml package in advance.

Epiphany answered 29/2, 2024 at 15:28 Comment(0)
J
0

I might be late to the party, but this is how I solved it.

pyproject.toml is using the version as it is defined in the package version file (some_package/version.py) using poetry-dynamic-versioning which allows "dynamic versioning based on tags in your version control system" but offers also the possibility to utilize Jinja templating to fill in the version.

And that version.py file again is, in my case, generated by changelog2version which updates the version file based on a markdown changelog file following SemVer.

Ensure to once call poetry self add "poetry-dynamic-versioning[plugin]", as documented in poetry-dynamic-versioning.

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

__version_info__ = ("0", "1", "0")
__version__ = ".".join(__version_info__)
[tool.poetry]
name = "my-package-name"
version = "0.0.0+will-be-updated-automatically"
description = "Cool stuff"
authors = ["brainelectronics"]
packages = [
  {include = "some_package/**/*.py"}
]

# https://github.com/mtkennerly/poetry-dynamic-versioning/tree/v1.3.0
[tool.poetry-dynamic-versioning]
enable = true
format-jinja-imports = [
  { module = "subprocess", item = "check_output" },
]
format-jinja = """{{ check_output(["python", "-c", "from pathlib import Path; exec(Path('some_package/version.py').read_text()); print(__version__)"]).decode().strip() }}"""

[tool.poetry.dependencies]
python = "^3.10.4"

[tool.poetry.group.dev.dependencies]
poetry-dynamic-versioning = "~1.3.0"

[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
build-backend = "poetry_dynamic_versioning.backend"
Jacie answered 22/5, 2024 at 13:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.