diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4578c0f137..6d2475a6c6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -19,7 +19,7 @@ jobs: uses: ./.github/actions/python-environment - name: Check Version(s) - run: poetry run version-check `poetry run -- python -c "from noxconfig import PROJECT_CONFIG; print(PROJECT_CONFIG.version_file)"` + run: poetry run -- nox -s version:check -- `poetry run -- python -c "from noxconfig import PROJECT_CONFIG; print(PROJECT_CONFIG.version_file)"` Documentation: name: Docs diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 79e701b844..5662f8595d 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -1 +1,5 @@ # Unreleased + +## ⚒️ Refactorings + +* [#412](https://github.com/exasol/python-toolbox/issues/392): Refactor pre commit hook package version.py into nox task \ No newline at end of file diff --git a/doc/developer_guide/modules/modules.rst b/doc/developer_guide/modules/modules.rst index e241572593..0dfb809190 100644 --- a/doc/developer_guide/modules/modules.rst +++ b/doc/developer_guide/modules/modules.rst @@ -7,5 +7,3 @@ Modules sphinx/sphinx nox nox_tasks - pre_commit_hooks - diff --git a/doc/developer_guide/modules/pre_commit_hooks.rst b/doc/developer_guide/modules/pre_commit_hooks.rst deleted file mode 100644 index 7807655d03..0000000000 --- a/doc/developer_guide/modules/pre_commit_hooks.rst +++ /dev/null @@ -1,10 +0,0 @@ -pre_commit_hooks -================= - -In the pre_commit_hook package contains git commit hooks and similar functionalities. - -.. figure:: ../../_static/nothing-to-see-here.png - :alt: nothing to see here - :target: https://imgflip.com/i/2a7gqa - - source: `imgflip.com `_ diff --git a/exasol/toolbox/cli.py b/exasol/toolbox/cli.py index 5dbc818a7c..377572ee44 100644 --- a/exasol/toolbox/cli.py +++ b/exasol/toolbox/cli.py @@ -1,6 +1,6 @@ from argparse import ArgumentTypeError -from exasol.toolbox.release import Version +from exasol.toolbox.util.version import Version def version(arg: str) -> Version: diff --git a/exasol/toolbox/pre_commit_hooks/package_version.py b/exasol/toolbox/nox/_package_version.py similarity index 50% rename from exasol/toolbox/pre_commit_hooks/package_version.py rename to exasol/toolbox/nox/_package_version.py index d2d99def1e..1435f249b1 100644 --- a/exasol/toolbox/pre_commit_hooks/package_version.py +++ b/exasol/toolbox/nox/_package_version.py @@ -1,21 +1,17 @@ -import subprocess +import argparse import sys from argparse import ( ArgumentParser, Namespace, ) -from collections import namedtuple -from collections.abc import Iterable from inspect import cleandoc from pathlib import Path -from shutil import which -from typing import ( - Any, - Dict, - Union, -) -Version = namedtuple("Version", ["major", "minor", "patch"]) +import nox +from nox import Session + +from exasol.toolbox.error import ToolboxError +from exasol.toolbox.util.version import Version _SUCCESS = 0 _FAILURE = 1 @@ -23,11 +19,11 @@ # fmt: off _VERSION_MODULE_TEMPLATE = cleandoc(''' # ATTENTION: - # This file is generated by exasol/toolbox/pre_commit_hooks/package_version.py when using: + # This file is generated by exasol/toolbox/nox/_package_version.py when using: # * either "poetry run -- nox -s project:fix" - # * or "poetry run -- version-check --fix" + # * or "poetry run -- nox version:check -- --fix" # Do not edit this file manually! - # If you need to change the version, do so in the project.toml, e.g. by using `poetry version X.Y.Z`. + # If you need to change the version, do so in the pyproject.toml, e.g. by using `poetry version X.Y.Z`. MAJOR = {major} MINOR = {minor} PATCH = {patch} @@ -37,47 +33,10 @@ # fmt: on -def version_from_string(s: str) -> Version: - """Converts a version string of the following format major.minor.patch to a version object""" - major, minor, patch = (int(number, base=0) for number in s.split(".")) - return Version(major, minor, patch) - - -class CommitHookError(Exception): - """Indicates that this commit hook encountered an error""" - - -def version_from_python_module(path: Path) -> Version: - """Retrieve version information from the `version` module""" - with open(path, encoding="utf-8") as file: - _locals: dict[str, Any] = {} - _globals: dict[str, Any] = {} - exec(file.read(), _locals, _globals) - - try: - version = _globals["VERSION"] - except KeyError as ex: - raise CommitHookError("Couldn't find version within module") from ex - - return version_from_string(version) - - -def version_from_poetry() -> Version: - poetry = which("poetry") - if not poetry: - raise CommitHookError("Couldn't find poetry executable") - - result = subprocess.run( - [poetry, "version", "--no-ansi"], capture_output=True, check=False - ) - version = result.stdout.decode().split()[1] - return version_from_string(version) - - def write_version_module(version: Version, path: str, exists_ok: bool = True) -> None: version_file = Path(path) if version_file.exists() and not exists_ok: - raise CommitHookError(f"Version file [{version_file}] already exists.") + raise ToolboxError(f"Version file [{version_file}] already exists.") version_file.unlink(missing_ok=True) with open(version_file, "w", encoding="utf-8") as f: f.write( @@ -88,7 +47,10 @@ def write_version_module(version: Version, path: str, exists_ok: bool = True) -> def _create_parser() -> ArgumentParser: - parser = ArgumentParser() + parser = ArgumentParser( + prog="nox -s version:check --", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) parser.add_argument("version_module", help="Path to version module") parser.add_argument("files", nargs="*") parser.add_argument( @@ -109,13 +71,13 @@ def _create_parser() -> ArgumentParser: def _main_debug(args: Namespace) -> int: - module_version = version_from_python_module(args.version_module) - poetry_version = version_from_poetry() + module_version = Version.from_python_module(args.version_module) + poetry_version = Version.from_poetry() if args.fix: write_version_module(poetry_version, args.version_module) - if not module_version == poetry_version: + if module_version != poetry_version: print( f"Version in pyproject.toml {poetry_version} and {args.version_module} {module_version} do not match!" ) @@ -138,12 +100,11 @@ def _main(args: Namespace) -> int: return _FAILURE -def main(argv: Union[Iterable[str], None] = None) -> int: +@nox.session(name="version:check", python=False) +def version_check(session: Session) -> None: + """""" parser = _create_parser() - args = parser.parse_args() + args = parser.parse_args(session.posargs) entry_point = _main if not args.debug else _main_debug - return entry_point(args) - - -if __name__ == "__main__": - sys.exit(main()) + if entry_point(args): + session.error() diff --git a/exasol/toolbox/nox/_release.py b/exasol/toolbox/nox/_release.py index 2d9db76008..fcd7ebe1bd 100644 --- a/exasol/toolbox/nox/_release.py +++ b/exasol/toolbox/nox/_release.py @@ -14,13 +14,15 @@ ) from exasol.toolbox.nox.plugin import NoxTasks from exasol.toolbox.release import ( - ReleaseTypes, - Version, extract_release_notes, new_changelog, new_changes, new_unreleased, ) +from exasol.toolbox.util.version import ( + ReleaseTypes, + Version, +) from noxconfig import PROJECT_CONFIG diff --git a/exasol/toolbox/nox/_shared.py b/exasol/toolbox/nox/_shared.py index d2e3e5c4d7..8f4284abdf 100644 --- a/exasol/toolbox/nox/_shared.py +++ b/exasol/toolbox/nox/_shared.py @@ -37,7 +37,7 @@ def python_files(project_root: Path) -> Iterable[Path]: def _version(session: Session, mode: Mode, version_file: Path) -> None: - command = ["version-check"] + command = ["nox", "-s", "version:check", "--"] command = command if mode == Mode.Check else command + ["--fix"] session.run(*command, f"{version_file}") diff --git a/exasol/toolbox/nox/tasks.py b/exasol/toolbox/nox/tasks.py index ce8f901b72..7076a4443b 100644 --- a/exasol/toolbox/nox/tasks.py +++ b/exasol/toolbox/nox/tasks.py @@ -85,5 +85,7 @@ def check(session: Session) -> None: audit ) +from exasol.toolbox.nox._package_version import version_check + # isort: on # fmt: on diff --git a/exasol/toolbox/release/__init__.py b/exasol/toolbox/release/__init__.py index a8e5b93e6a..a20e24d0df 100644 --- a/exasol/toolbox/release/__init__.py +++ b/exasol/toolbox/release/__init__.py @@ -1,112 +1,10 @@ from __future__ import annotations -import subprocess -from dataclasses import dataclass from datetime import datetime -from enum import Enum -from functools import ( - total_ordering, - wraps, -) from inspect import cleandoc from pathlib import Path -from shutil import which - -from exasol.toolbox.error import ToolboxError - - -def _index_or(container, index, default): - try: - return container[index] - except IndexError: - return default - - -class ReleaseTypes(Enum): - Major = "major" - Minor = "minor" - Patch = "patch" - - def __str__(self): - return self.name.lower() - - -def poetry_command(func): - @wraps(func) - def wrapper(*args, **kwargs): - cmd = which("poetry") - if not cmd: - raise ToolboxError("Couldn't find poetry executable") - try: - return func(*args, **kwargs) - except subprocess.CalledProcessError as ex: - raise ToolboxError(f"Failed to execute: {ex.cmd}") from ex - - return wrapper - - -@total_ordering -@dataclass(frozen=True) -class Version: - major: int - minor: int - patch: int - - def __str__(self): - return f"{self.major}.{self.minor}.{self.patch}" - - def __lt__(self, other: object): - if not isinstance(other, Version): - return NotImplemented - return ( - self.major < other.major - or (self.major <= other.major and self.minor < other.minor) - or ( - self.major <= other.major - and self.minor <= other.minor - and self.patch < other.patch - ) - ) - - def __eq__(self, other: object): - if not isinstance(other, Version): - return NotImplemented - return ( - self.major == other.major - and self.minor == other.minor - and self.patch == other.patch - ) - - @staticmethod - def from_string(version): - parts = [int(number, base=0) for number in version.split(".")] - if len(parts) > 3: - raise ValueError( - "Version has an invalid format, " - f"expected: '..', actual: '{version}'" - ) - version = [_index_or(parts, i, 0) for i in range(3)] - return Version(*version) - - @staticmethod - @poetry_command - def from_poetry(): - output = subprocess.run( - ["poetry", "version", "--no-ansi", "--short"], - capture_output=True, - text=True, - ) - return Version.from_string(output.stdout.strip()) - - @staticmethod - @poetry_command - def upgrade_version_from_poetry(t: ReleaseTypes): - output = subprocess.run( - ["poetry", "version", str(t), "--dry-run", "--no-ansi", "--short"], - capture_output=True, - text=True, - ) - return Version.from_string(output.stdout.strip()) + +from exasol.toolbox.util.version import Version def extract_release_notes(file: str | Path) -> str: diff --git a/exasol/toolbox/sphinx/multiversion/main.py b/exasol/toolbox/sphinx/multiversion/main.py index 694fd258a5..f183d97cc8 100644 --- a/exasol/toolbox/sphinx/multiversion/main.py +++ b/exasol/toolbox/sphinx/multiversion/main.py @@ -21,11 +21,11 @@ from sphinx import config as sphinx_config from sphinx import project as sphinx_project -from exasol.toolbox.release import Version as ExasolVersion from exasol.toolbox.sphinx.multiversion import ( git, sphinx, ) +from exasol.toolbox.util.version import Version as ExasolVersion logging.basicConfig( level="INFO", diff --git a/exasol/toolbox/sphinx/multiversion/sphinx.py b/exasol/toolbox/sphinx/multiversion/sphinx.py index 18fb154880..91db2b8813 100644 --- a/exasol/toolbox/sphinx/multiversion/sphinx.py +++ b/exasol/toolbox/sphinx/multiversion/sphinx.py @@ -9,7 +9,7 @@ from sphinx.locale import _ from sphinx.util import i18n as sphinx_i18n -from exasol.toolbox.release import Version as ExasolVersion +from exasol.toolbox.util.version import Version as ExasolVersion from exasol.toolbox.version import VERSION as PLUGIN_VERSION logger = logging.getLogger(__name__) diff --git a/exasol/toolbox/templates/github/workflows/checks.yml b/exasol/toolbox/templates/github/workflows/checks.yml index b507c12165..cf650ff62f 100644 --- a/exasol/toolbox/templates/github/workflows/checks.yml +++ b/exasol/toolbox/templates/github/workflows/checks.yml @@ -22,7 +22,7 @@ jobs: run: | echo "Please enable the version check by replacing this output with shell command bellow:" echo "" - echo "poetry run -- version-check <>" + echo "poetry run -- nox -s version:check -- <>" echo "" echo "Note: <> needs to point to the version file of the project (version.py)." exit 1 diff --git a/exasol/toolbox/pre_commit_hooks/__init__.py b/exasol/toolbox/util/__init__.py similarity index 100% rename from exasol/toolbox/pre_commit_hooks/__init__.py rename to exasol/toolbox/util/__init__.py diff --git a/exasol/toolbox/util/version.py b/exasol/toolbox/util/version.py new file mode 100644 index 0000000000..d65968384d --- /dev/null +++ b/exasol/toolbox/util/version.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import subprocess +from dataclasses import dataclass +from enum import Enum +from functools import ( + total_ordering, + wraps, +) +from pathlib import Path +from shutil import which +from typing import Any + +from exasol.toolbox.error import ToolboxError + + +def _index_or(container, index, default): + try: + return container[index] + except IndexError: + return default + + +class ReleaseTypes(Enum): + Major = "major" + Minor = "minor" + Patch = "patch" + + def __str__(self): + return self.name.lower() + + +def poetry_command(func): + @wraps(func) + def wrapper(*args, **kwargs): + cmd = which("poetry") + if not cmd: + raise ToolboxError("Couldn't find poetry executable") + try: + return func(*args, **kwargs) + except subprocess.CalledProcessError as ex: + raise ToolboxError(f"Failed to execute: {ex.cmd}") from ex + + return wrapper + + +@total_ordering +@dataclass(frozen=True) +class Version: + major: int + minor: int + patch: int + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + def __lt__(self, other: object): + if not isinstance(other, Version): + return NotImplemented + return ( + self.major < other.major + or (self.major <= other.major and self.minor < other.minor) + or ( + self.major <= other.major + and self.minor <= other.minor + and self.patch < other.patch + ) + ) + + def __eq__(self, other: object): + if not isinstance(other, Version): + return NotImplemented + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + ) + + @staticmethod + def from_string(version): + parts = [int(number, base=0) for number in version.split(".")] + if len(parts) > 3: + raise ValueError( + "Version has an invalid format, " + f"expected: '..', actual: '{version}'" + ) + version = [_index_or(parts, i, 0) for i in range(3)] + return Version(*version) + + @staticmethod + @poetry_command + def from_poetry(): + output = subprocess.run( + ["poetry", "version", "--no-ansi", "--short"], + capture_output=True, + text=True, + ) + return Version.from_string(output.stdout.strip()) + + @staticmethod + @poetry_command + def upgrade_version_from_poetry(t: ReleaseTypes): + output = subprocess.run( + ["poetry", "version", str(t), "--dry-run", "--no-ansi", "--short"], + capture_output=True, + text=True, + ) + return Version.from_string(output.stdout.strip()) + + @staticmethod + def from_python_module(path: Path) -> Version: + """Retrieve version information from the `version` module""" + with open(path, encoding="utf-8") as file: + _locals: dict[str, Any] = {} + _globals: dict[str, Any] = {} + exec(file.read(), _locals, _globals) + + try: + version = _globals["VERSION"] + except KeyError as ex: + raise ToolboxError("Couldn't find version within module") from ex + + return Version.from_string(version) diff --git a/exasol/toolbox/version.py b/exasol/toolbox/version.py index ccb7c4e012..bc4dfa29fa 100644 --- a/exasol/toolbox/version.py +++ b/exasol/toolbox/version.py @@ -1,9 +1,9 @@ # ATTENTION: -# This file is generated by exasol/toolbox/pre_commit_hooks/package_version.py when using: +# This file is generated by exasol/toolbox/nox/_package_version.py when using: # * either "poetry run -- nox -s project:fix" -# * or "poetry run -- version-check --fix" +# * or "poetry run -- nox version:check -- --fix" # Do not edit this file manually! -# If you need to change the version, do so in the project.toml, e.g. by using `poetry version X.Y.Z`. +# If you need to change the version, do so in the pyproject.toml, e.g. by using `poetry version X.Y.Z`. MAJOR = 1 MINOR = 1 PATCH = 0 diff --git a/project-template/{{cookiecutter.repo_name}}/exasol/{{cookiecutter.package_name}}/version.py b/project-template/{{cookiecutter.repo_name}}/exasol/{{cookiecutter.package_name}}/version.py index 84d43669b2..7691647fdf 100644 --- a/project-template/{{cookiecutter.repo_name}}/exasol/{{cookiecutter.package_name}}/version.py +++ b/project-template/{{cookiecutter.repo_name}}/exasol/{{cookiecutter.package_name}}/version.py @@ -1,9 +1,9 @@ # ATTENTION: -# This file is generated by exasol/toolbox/pre_commit_hooks/package_version.py when using: +# This file is generated by exasol/toolbox/nox/_package_version.py when using: # * either "poetry run -- nox -s project:fix" -# * or "poetry run -- version-check --fix" +# * or "poetry run -- nox -s version:check -- --fix" # Do not edit this file manually! -# If you need to change the version, do so in the project.toml, e.g. by using `poetry version X.Y.Z`. +# If you need to change the version, do so in the pyproject.toml, e.g. by using `poetry version X.Y.Z`. MAJOR = 0 MINOR = 1 PATCH = 0 diff --git a/pyproject.toml b/pyproject.toml index 5933590493..2426d13832 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,6 +110,5 @@ module = [ ignore_errors = true [tool.poetry.plugins."console_scripts"] -version-check = "exasol.toolbox.pre_commit_hooks.package_version:main" tbx = 'exasol.toolbox.tools.tbx:CLI' sphinx-multiversion = 'exasol.toolbox.sphinx.multiversion:main' \ No newline at end of file diff --git a/test/unit/cli_test.py b/test/unit/cli_test.py index 29ed8ab65d..e97c365443 100644 --- a/test/unit/cli_test.py +++ b/test/unit/cli_test.py @@ -3,7 +3,7 @@ import pytest from exasol.toolbox.cli import version -from exasol.toolbox.release import Version +from exasol.toolbox.util.version import Version @pytest.mark.parametrize( diff --git a/test/unit/release_test.py b/test/unit/release_test.py index d1009fd423..32bb74ca18 100644 --- a/test/unit/release_test.py +++ b/test/unit/release_test.py @@ -1,4 +1,3 @@ -import subprocess from datetime import datetime from inspect import cleandoc from subprocess import CalledProcessError @@ -9,75 +8,15 @@ import pytest -from exasol.toolbox.error import ToolboxError from exasol.toolbox.nox._release import ( ReleaseError, _trigger_release, ) from exasol.toolbox.release import ( - ReleaseTypes, - Version, extract_release_notes, new_changelog, - poetry_command, ) - - -@pytest.mark.parametrize( - "input,expected", - [ - ("1.2.3", Version(1, 2, 3)), - ("1.2", Version(1, 2, 0)), - ("1", Version(1, 0, 0)), - ], -) -def test_create_version_from_string(input, expected): - actual = Version.from_string(input) - assert expected == actual - - -@pytest.mark.parametrize( - "old_version,new_version,expected", - [ - (Version(1, 2, 3), Version(1, 2, 4), True), - (Version(1, 2, 3), Version(1, 3, 3), True), - (Version(1, 2, 3), Version(2, 2, 3), True), - (Version(1, 2, 3), Version(1, 1, 3), False), - (Version(1, 2, 3), Version(1, 2, 1), False), - (Version(1, 2, 3), Version(0, 3, 3), False), - ], -) -def test_is_later_version(old_version, new_version, expected): - actual = new_version > old_version - assert expected == actual - - -@pytest.fixture -def poetry_version(): - def set_poetry_version(version): - return subprocess.CompletedProcess( - args=["poetry", "version", "--no-ansi", "--short"], - returncode=0, - stdout=version, - stderr="", - ) - - yield set_poetry_version - - -@pytest.mark.parametrize( - "version,expected", - [ - ("1.2.3", Version(1, 2, 3)), - ("1.2", Version(1, 2, 0)), - ("1", Version(1, 0, 0)), - ], -) -def test_version_from_poetry(poetry_version, version, expected): - with patch("subprocess.run", return_value=poetry_version(version)): - actual = Version.from_poetry() - - assert expected == actual +from exasol.toolbox.util.version import Version @pytest.mark.parametrize( @@ -245,24 +184,3 @@ def simulate_fail(args, **kwargs): with pytest.raises(ReleaseError) as ex: _trigger_release() assert f"release {version} already exists" in str(ex) - - -@patch("exasol.toolbox.release.which", return_value=None) -def test_poetry_decorator_no_poetry_executable(mock): - @poetry_command - def test(): - pass - - with pytest.raises(ToolboxError): - test() - - -@patch("exasol.toolbox.release.which", return_value="test/path") -def test_poetry_decorator_subprocess(mock): - @poetry_command - def test(): - raise subprocess.CalledProcessError(returncode=1, cmd=["test"]) - pass - - with pytest.raises(ToolboxError): - test() diff --git a/test/unit/util/version_test.py b/test/unit/util/version_test.py new file mode 100644 index 0000000000..caa2918551 --- /dev/null +++ b/test/unit/util/version_test.py @@ -0,0 +1,111 @@ +import subprocess +from unittest.mock import ( + patch, +) + +import pytest + +from exasol.toolbox.error import ToolboxError +from exasol.toolbox.util.version import ( + Version, + poetry_command, +) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("1.2.3", Version(1, 2, 3)), + ("1.2", Version(1, 2, 0)), + ("1", Version(1, 0, 0)), + ], +) +def test_create_version_from_string(input, expected): + actual = Version.from_string(input) + assert expected == actual + + +@pytest.mark.parametrize( + "old_version,new_version,expected", + [ + (Version(1, 2, 3), Version(1, 2, 4), True), + (Version(1, 2, 3), Version(1, 3, 3), True), + (Version(1, 2, 3), Version(2, 2, 3), True), + (Version(1, 2, 3), Version(1, 1, 3), False), + (Version(1, 2, 3), Version(1, 2, 1), False), + (Version(1, 2, 3), Version(0, 3, 3), False), + ], +) +def test_is_later_version(old_version, new_version, expected): + actual = new_version > old_version + assert expected == actual + + +@pytest.fixture +def poetry_version(): + def set_poetry_version(version): + return subprocess.CompletedProcess( + args=["poetry", "version", "--no-ansi", "--short"], + returncode=0, + stdout=version, + stderr="", + ) + + yield set_poetry_version + + +@pytest.mark.parametrize( + "version,expected", + [ + ("1.2.3", Version(1, 2, 3)), + ("1.2", Version(1, 2, 0)), + ("1", Version(1, 0, 0)), + ], +) +def test_version_from_poetry(poetry_version, version, expected): + with patch("subprocess.run", return_value=poetry_version(version)): + actual = Version.from_poetry() + + assert expected == actual + + +@patch("exasol.toolbox.util.version.which", return_value=None) +def test_poetry_decorator_no_poetry_executable(mock): + @poetry_command + def test(): + pass + + with pytest.raises(ToolboxError): + test() + + +@patch("exasol.toolbox.util.version.which", return_value="test/path") +def test_poetry_decorator_subprocess(mock): + @poetry_command + def test(): + raise subprocess.CalledProcessError(returncode=1, cmd=["test"]) + pass + + with pytest.raises(ToolboxError): + test() + + +def test_version_from_python_module(tmp_path): + tmp_file = tmp_path / "file" + file = """ +MAJOR = 1 +MINOR = 2 +PATCH = 3 +VERSION = f"{MAJOR}.{MINOR}.{PATCH}" +__version__ = VERSION + """ + tmp_file.write_text(file) + assert Version.from_python_module(tmp_file) == Version.from_string("1.2.3") + + +def test_version_from_python_no_module_error(tmp_path): + file_path = tmp_path / "file" + file_path.write_text("") + with pytest.raises(ToolboxError) as ex: + Version.from_python_module(file_path) + assert str(ex.value) == "Couldn't find version within module"