From 67901761ea7d420071082faf3e15e389efc84b78 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 6 Feb 2026 11:45:15 +0100 Subject: [PATCH 1/6] Modify cookiecutter command to reduce errors experienced by users --- doc/changes/unreleased.md | 1 + doc/user_guide/getting_started.rst | 6 +++++- test/integration/project-template/conftest.py | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index ea8fce542..c716235b5 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -5,6 +5,7 @@ ## Documentation * #585: Added instructions how to ignore sonar issues to the User Guide +* #630: Updated cookiecutter command to reduce errors experienced by users ## Refactoring diff --git a/doc/user_guide/getting_started.rst b/doc/user_guide/getting_started.rst index e253ebd91..528556ee8 100644 --- a/doc/user_guide/getting_started.rst +++ b/doc/user_guide/getting_started.rst @@ -31,7 +31,8 @@ Use the following command to create a new project: .. code-block:: shell cookiecutter https://github.com/exasol/python-toolbox.git \ - --checkout --directory project-template + --checkout --directory project-template \ + --overwrite-if-exists .. note:: @@ -39,6 +40,9 @@ Use the following command to create a new project: to get reliable and reproducible results, we recommend using the tag of PTB's latest released version instead. + Without option :code:`--overwrite-if-exists`, the cookiecutter command will fail if + you already have files in that directory, like from ``git init``. + **2. Follow the interactive project setup prompt** **3. Bootstrap the development environment** diff --git a/test/integration/project-template/conftest.py b/test/integration/project-template/conftest.py index 943b33ab8..26fd281f1 100644 --- a/test/integration/project-template/conftest.py +++ b/test/integration/project-template/conftest.py @@ -16,9 +16,14 @@ def new_project(cwd): project_name = "project" repo_name = "repo" package_name = "package" + project_path = cwd / repo_name + + subprocess.run(["mkdir", "-p", project_path]) + subprocess.run(["git", "init"], cwd=project_path) subprocess.run( - ["cookiecutter", PROJECT_CONFIG.root_path / "project-template", "-o", cwd, "--no-input", + ["cookiecutter", PROJECT_CONFIG.root_path / "project-template", + "-o", cwd, "--no-input", "--overwrite-if-exists", f"project_name={project_name}", f"repo_name={repo_name}", f"package_name={package_name}", ], capture_output=True, check=True) From 97cf3c6227340282c09b3cebd1ba1ddbb615581d Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 6 Feb 2026 12:06:13 +0100 Subject: [PATCH 2/6] Catch exception & raise custom one to use at higher level --- doc/changes/unreleased.md | 4 ++++ exasol/toolbox/util/dependencies/shared_models.py | 14 +++++++++++++- exasol/toolbox/util/release/changelog.py | 13 ++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index c716235b5..90e078afb 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -2,6 +2,10 @@ ## Summary +## Bug + +* #692: Fixed bug where creating first release failed due to no previous tags + ## Documentation * #585: Added instructions how to ignore sonar issues to the User Guide diff --git a/exasol/toolbox/util/dependencies/shared_models.py b/exasol/toolbox/util/dependencies/shared_models.py index 8caf1d7f6..ddbb711e8 100644 --- a/exasol/toolbox/util/dependencies/shared_models.py +++ b/exasol/toolbox/util/dependencies/shared_models.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path +from subprocess import CalledProcessError from typing import ( Annotated, Final, @@ -25,6 +26,13 @@ VERSION_TYPE = Annotated[str, AfterValidator(lambda v: Version(v))] +class LatestTagNotFoundError(Exception): + """Raised when the requested latest tag cannot be found in the repository.""" + + def __init__(self, *args): + super().__init__("The latest git tag was not found in the repository.", *args) + + def normalize_package_name(package_name: str) -> NormalizedPackageStr: return NormalizedPackageStr(package_name.lower().replace("_", "-")) @@ -64,7 +72,11 @@ def files(self) -> tuple[str, ...]: @contextmanager def poetry_files_from_latest_tag(root_path: Path) -> Generator[Path]: """Context manager to set up a temporary directory with poetry files from the latest tag""" - latest_tag = Git.get_latest_tag() + try: + latest_tag = Git.get_latest_tag() + except CalledProcessError: + raise LatestTagNotFoundError() + path = root_path.relative_to(Git.toplevel()) with tempfile.TemporaryDirectory() as tmpdir_str: tmp_dir = Path(tmpdir_str) diff --git a/exasol/toolbox/util/release/changelog.py b/exasol/toolbox/util/release/changelog.py index 5393e0de5..54c319275 100644 --- a/exasol/toolbox/util/release/changelog.py +++ b/exasol/toolbox/util/release/changelog.py @@ -8,6 +8,7 @@ get_dependencies, get_dependencies_from_latest_tag, ) +from exasol.toolbox.util.dependencies.shared_models import LatestTagNotFoundError from exasol.toolbox.util.dependencies.track_changes import DependencyChanges from exasol.toolbox.util.version import Version @@ -77,9 +78,15 @@ def _describe_dependency_changes(self) -> str: Describe the dependency changes between the latest tag and the current version for use in the versioned changes file. """ - previous_dependencies_in_groups = get_dependencies_from_latest_tag( - root_path=self.root_path - ) + try: + previous_dependencies_in_groups = get_dependencies_from_latest_tag( + root_path=self.root_path + ) + except LatestTagNotFoundError: + # In new projects, there is not a pre-existing tag, and all dependencies + # are considered new. + previous_dependencies_in_groups = {} + current_dependencies_in_groups = get_dependencies( working_directory=self.root_path ) From 275cdf314c9de0c5dbceaaa0723206e5d3a5ea64 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 6 Feb 2026 12:28:39 +0100 Subject: [PATCH 3/6] Add test to verify works without --no-pr --no-branch --no-add --- test/integration/project-template/nox_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/project-template/nox_test.py b/test/integration/project-template/nox_test.py index 19210a14e..5a0d55e66 100644 --- a/test/integration/project-template/nox_test.py +++ b/test/integration/project-template/nox_test.py @@ -58,3 +58,9 @@ def test_package_check(self, poetry_path, run_command): output = run_command(package_check) assert output.returncode == 0 + + def test_release_prepare(self, poetry_path, run_command): + release_prepare = self._command(poetry_path, task="release:prepare", add_ons=["--type", "minor", "--no-pr", "--no-branch", "--no-add"]) + output = run_command(release_prepare) + + assert output.returncode == 0 From 665d70c4314cb155d2b696668b06bc65f2570afe Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 6 Feb 2026 12:31:49 +0100 Subject: [PATCH 4/6] Fix type for consistency --- exasol/toolbox/util/release/changelog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exasol/toolbox/util/release/changelog.py b/exasol/toolbox/util/release/changelog.py index 54c319275..59d858cf9 100644 --- a/exasol/toolbox/util/release/changelog.py +++ b/exasol/toolbox/util/release/changelog.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import OrderedDict from datetime import datetime from inspect import cleandoc from pathlib import Path @@ -85,7 +86,7 @@ def _describe_dependency_changes(self) -> str: except LatestTagNotFoundError: # In new projects, there is not a pre-existing tag, and all dependencies # are considered new. - previous_dependencies_in_groups = {} + previous_dependencies_in_groups = OrderedDict() current_dependencies_in_groups = get_dependencies( working_directory=self.root_path From 802e27720dbd069948f0027462c90b50bfd97e08 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 6 Feb 2026 12:43:11 +0100 Subject: [PATCH 5/6] Add ignore for security concern --- exasol/toolbox/util/dependencies/shared_models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exasol/toolbox/util/dependencies/shared_models.py b/exasol/toolbox/util/dependencies/shared_models.py index ddbb711e8..95b73e5f2 100644 --- a/exasol/toolbox/util/dependencies/shared_models.py +++ b/exasol/toolbox/util/dependencies/shared_models.py @@ -5,7 +5,9 @@ from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path -from subprocess import CalledProcessError +from subprocess import ( + CalledProcessError, # nosec: B404 - risk of using subprocess is acceptable +) from typing import ( Annotated, Final, From 75dd35ae637485e2b0e4f3d19528b02bfd5a8f71 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 6 Feb 2026 13:16:06 +0100 Subject: [PATCH 6/6] Add test to ensure custom error raised in such a scenario --- .../util/dependencies/shared_models_test.py | 35 +++++++++++++++---- test/unit/util/release/changelog_test.py | 27 ++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/test/unit/util/dependencies/shared_models_test.py b/test/unit/util/dependencies/shared_models_test.py index c59dbc8e4..3d085ffad 100644 --- a/test/unit/util/dependencies/shared_models_test.py +++ b/test/unit/util/dependencies/shared_models_test.py @@ -1,3 +1,6 @@ +from subprocess import CalledProcessError +from unittest import mock + import pytest from packaging.version import Version from pydantic import BaseModel @@ -5,6 +8,7 @@ from exasol.toolbox.util.dependencies.shared_models import ( VERSION_TYPE, + LatestTagNotFoundError, Package, PoetryFiles, poetry_files_from_latest_tag, @@ -56,11 +60,28 @@ def test_coordinates(): assert dep.coordinates == "numpy:0.1.0" -def test_poetry_files_from_latest_tag(): - latest_tag = Git.get_latest_tag() - with poetry_files_from_latest_tag(root_path=PROJECT_CONFIG.root_path) as tmp_dir: - for file in PoetryFiles().files: - assert (tmp_dir / file).is_file() +class TestPoetryFilesFromLatestTag: + @staticmethod + def test_works_as_expected(): + latest_tag = Git.get_latest_tag() + with poetry_files_from_latest_tag( + root_path=PROJECT_CONFIG.root_path + ) as tmp_dir: + for file in PoetryFiles().files: + assert (tmp_dir / file).is_file() - contents = (tmp_dir / PoetryFiles.pyproject_toml).read_text() - assert f'version = "{latest_tag}"' in contents + contents = (tmp_dir / PoetryFiles.pyproject_toml).read_text() + assert f'version = "{latest_tag}"' in contents + + @staticmethod + def test_raises_exception_when_latest_tag_not_found(): + with pytest.raises(LatestTagNotFoundError): + with mock.patch.object( + Git, + "get_latest_tag", + side_effect=CalledProcessError( + cmd="Mocked subprocess error", returncode=1 + ), + ): + with poetry_files_from_latest_tag(root_path=PROJECT_CONFIG.root_path): + pass diff --git a/test/unit/util/release/changelog_test.py b/test/unit/util/release/changelog_test.py index 8a207d695..51527ec2b 100644 --- a/test/unit/util/release/changelog_test.py +++ b/test/unit/util/release/changelog_test.py @@ -4,6 +4,7 @@ import pytest +from exasol.toolbox.util.dependencies.shared_models import LatestTagNotFoundError from exasol.toolbox.util.release.changelog import ( UNRELEASED_INITIAL_CONTENT, Changelogs, @@ -84,6 +85,18 @@ def mock_dependencies(dependencies, previous_dependencies): yield +@pytest.fixture(scope="function") +def mock_new_dependencies(dependencies): + mock_latest_tag_not_found_error = mock.Mock(side_effect=LatestTagNotFoundError) + + with mock.patch.multiple( + "exasol.toolbox.util.release.changelog", + get_dependencies_from_latest_tag=mock_latest_tag_not_found_error, + get_dependencies=lambda working_directory: dependencies, + ): + yield + + @pytest.fixture(scope="function") def mock_no_dependencies(): with mock.patch.multiple( @@ -145,6 +158,20 @@ def test_describe_dependency_changes(changelogs, mock_dependencies): "* Added dependency `package2:0.2.0`\n" ) + @staticmethod + def test_describe_dependency_changes_without_latest_version( + changelogs, mock_new_dependencies + ): + result = changelogs._describe_dependency_changes() + assert result == ( + "\n" + "### `main`\n" + "* Added dependency `package1:0.1.0`\n" + "\n" + "### `dev`\n" + "* Added dependency `package2:0.2.0`\n" + ) + @staticmethod @pytest.mark.parametrize( "groups,expected",