Source code for matrixctl.package_version

# matrixctl
# Copyright (c) 2021-2023  Michael Sasser <Michael@MichaelSasser.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Get the packages version.

The package's version is determined by first checking if a
``pyproject.toml``exists. If this is given, the version variable is
searched line by line in the file using a regular expression. When a
match occurs, the version is returned. If the ``pyproject.toml`` does
not exist, e.g. because the package was installed, it uses the version
stored in the package's metadata. In any case, if the version could not
be determined, it will return ``None``.

"""

from __future__ import annotations

import re
import typing as t

from contextlib import suppress
from pathlib import Path


__author__: str = "Michael Sasser"
__email__: str = "Michael@MichaelSasser.org"


# semver
VERSION_PATTERN: str = (
    r"^\s*version\s*=\s*[\"']\s*"
    r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
    r"(?:-(?P<prerelease>"
    r"(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
    r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
    r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?\s*[\"']\s*$"
)
# Fast approx. for simpel versions


def __from_pyproject(file: Path) -> str | None:
    """Get the version of a Python package from the pyproject.toml.

    Parameters
    ----------
    file : pythlib.Path
        The filepath to the pyproject.toml file.

    Returns
    -------
    version : str or None
        The package version, if the ``pyproject.toml`` and the variable
        ``version`` inside it exists. Otherwise, it will return ``None``.
        If the package is installed, the return value will be ``None``.

    """
    with suppress(FileNotFoundError), file.open() as fp:
        vers: t.Pattern[str] = re.compile(VERSION_PATTERN)
        for line in fp:
            version: t.Match[str] | None = vers.search(line)
            if version is not None:
                # semver
                return (
                    f"{version.group(1)}."
                    f"{version.group(2) or '0'}."
                    f"{version.group(3) or '0'}"
                    f"{f'-{version.group(4)}' if version.group(4) else ''}"
                    f"{f'+{version.group(5)}' if version.group(5) else ''}"
                )
                # Fast approx.
    return None


def __from_metadata(name: str) -> str | None:
    """Get the version of a Python package from the Metadata.

    Parameters
    ----------
    name : str
        The packages ``__name__``.

    Returns
    -------
    version : str or None
        The package version, if the package is installed and the version
        of it is stored in the packages metadata.

    """
    import importlib.metadata as importlib_metadata  # Python >= 3.8

    with suppress(importlib_metadata.PackageNotFoundError):
        return importlib_metadata.version(name).strip()
    return None


[docs] def get_version(name: str, file: str | Path) -> str | None: """Get the version of a Python package. Examples -------- .. code-block:: python # file: __init__.py from .package_version import get_version __version__: str | None = get_version(__name__, __file__) # or __version__: str = get_version(__name__, __file__) or "Unknown" # Optional: if __version__ is None: raise ValueError("Could not find the version of the package.") .. code-block:: python # file: conf.py (sphinx) import sys sys.path.insert(0, os.path.abspath("../")) sys.path.insert(0, os.path.abspath("../..")) from matrixctl.package_version import get_version __version__: str = ( get_version("matrixctl", Path(__file__).parent) or "Unknown" ) Parameters ---------- name : str The packages ``__name__``. file : str The ``__name__`` of ``__init__.py`` Returns ------- version : str or None The package version, if the package is installed and the version of it is stored in the packages metadata. """ file_: Path = Path(file).parent.parent / "pyproject.toml" return __from_pyproject(file_) or __from_metadata(name)