Haterind
Haterind

Reputation: 1465

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

My package version is defined in two places:

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?

Upvotes: 80

Views: 34371

Answers (6)

brainelectronics
brainelectronics

Reputation: 95

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"

Upvotes: 1

Maxim Suslov
Maxim Suslov

Reputation: 5485

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.

Upvotes: 0

hlongmore
hlongmore

Reputation: 1856

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"]
+

Upvotes: 5

frans
frans

Reputation: 9808

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)

Upvotes: 7

Alwin
Alwin

Reputation: 1077

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__)

Upvotes: 18

finswimmer
finswimmer

Reputation: 15252

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")

Upvotes: 73

Related Questions