From 4214475357cba67baab483aeb43efa5b1d8491fa Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 12:46:08 +0200 Subject: [PATCH 01/20] Refactor out inner function for exporting & update nosec comments --- exasol/toolbox/util/dependencies/audit.py | 47 +++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 553c835f73..b3ffc111db 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -26,7 +26,6 @@ r"^Found \d+ known vulnerabilit\w{1,3} in \d+ package\w?$" ) - PipAuditEntry = dict[str, str | list[str] | tuple[str, ...]] @@ -159,6 +158,34 @@ def subsection_for_changelog_summary(self) -> str: """) +def export_dependencies_to_file(output_file: Path, working_directory: Path) -> None: + """ + Export all dependencies to a requirements.txt format + + The default for `poetry export` is to only include the main dependencies and their + transitive dependencies, by adding `--all-groups` and `all-extras` we get + all dependencies defined in groups, like dev dependencies, and all optional + dependencies. + """ + command = [ + "poetry", + "export", + "--format=requirements.txt", + "--all-groups", + "--all-extras", + ] + output = subprocess.run( + command, + capture_output=True, + text=True, + cwd=working_directory, + ) # nosec: B603 - allow fixed poetry usage + if output.returncode != 0: + raise PipAuditException.from_subprocess(output, command, cwd=working_directory) + + output_file.write_text(output.stdout) + + def audit_poetry_files(working_directory: Path) -> str: """ Audit the `pyproject.toml` and `poetry.lock` files @@ -172,20 +199,10 @@ def audit_poetry_files(working_directory: Path) -> str: and then inspecting the dependencies. """ - requirements_txt = "requirements.txt" - command = ["poetry", "export", "--format=requirements.txt"] - output = subprocess.run( - command, - capture_output=True, - text=True, - cwd=working_directory, - ) # nosec - if output.returncode != 0: - raise PipAuditException.from_subprocess(output, command, cwd=working_directory) - with tempfile.TemporaryDirectory() as path: tmpdir = Path(path) - (tmpdir / requirements_txt).write_text(output.stdout) + requirements_path = tmpdir / "requirements.txt" + export_dependencies_to_file(requirements_path, working_directory) # CLI option `--disable-pip` skips dependency resolution in pip. The # option can be used with hashed requirements files to avoid @@ -195,13 +212,13 @@ def audit_poetry_files(working_directory: Path) -> str: # In real use scenarios of the PTB we usually have hashed # requirements. Unfortunately this is not the case for the example # project created in the integration tests. - command = ["pip-audit", "-r", requirements_txt, "-f", "json"] + command = ["pip-audit", "-r", requirements_path.name, "-f", "json"] output = subprocess.run( command, capture_output=True, text=True, cwd=tmpdir, - ) # nosec + ) # nosec: B603 - allow fixed pip-audit usage if output.returncode != 0: # pip-audit does not distinguish between 1) finding vulnerabilities From 59f310d1f5ee615d646d8b0e54586dcf625b8558 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 13:02:57 +0200 Subject: [PATCH 02/20] Move poetry_2_1_pyproject_toml to conftest.py for sharing --- test/unit/util/dependencies/audit_test.py | 37 ++++++++++++++ test/unit/util/dependencies/conftest.py | 30 +++++++++++ .../dependencies/poetry_dependencies_test.py | 51 +++++-------------- 3 files changed, 79 insertions(+), 39 deletions(-) create mode 100644 test/unit/util/dependencies/conftest.py diff --git a/test/unit/util/dependencies/audit_test.py b/test/unit/util/dependencies/audit_test.py index ed9ce84031..92fff5a44f 100644 --- a/test/unit/util/dependencies/audit_test.py +++ b/test/unit/util/dependencies/audit_test.py @@ -1,4 +1,5 @@ import json +import re from inspect import cleandoc from pathlib import Path from subprocess import CompletedProcess @@ -13,6 +14,7 @@ Vulnerability, VulnerabilitySource, audit_poetry_files, + export_dependencies_to_file, get_vulnerabilities, get_vulnerabilities_from_latest_tag, ) @@ -124,6 +126,41 @@ def test_subsection_for_changelog_summary(self, sample_vulnerability): ) +class TestExportDependenciesToFile: + PACKAGES = [ + "astroid", + "black", + "click", + "colorama", + "dill", + "isort", + "mccabe", + "mypy-extensions", + "packaging", + "pathspec", + "platformdirs", + "pylint", + "tomli", + "tomlkit", + "typing-extensions", + ] + + def test_with_poetry_2_1_0(self, tmp_path, poetry_2_1_pyproject_text): + (tmp_path / "pyproject.toml").write_text(poetry_2_1_pyproject_text) + requirements_txt = tmp_path / "requirements.txt" + + export_dependencies_to_file( + output_file=requirements_txt, working_directory=tmp_path + ) + content = requirements_txt.read_text() + + packages = re.findall( + r"^([a-zA-Z0-9\-_]+)(?===|>=|<=|>|<|@)", content, re.MULTILINE + ) + + assert packages == self.PACKAGES + + class TestAuditPoetryFiles: @staticmethod @mock.patch("subprocess.run") diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py new file mode 100644 index 0000000000..fdf289664b --- /dev/null +++ b/test/unit/util/dependencies/conftest.py @@ -0,0 +1,30 @@ +import pytest + + +@pytest.fixture(scope="module") +def poetry_2_1_pyproject_text() -> str: + return """ + [project] + name = "project" + version = "0.1.0" + description = "" + authors = [] + readme = "README.md" + requires-python = ">=3.10" + dependencies = [ + "pylint (==3.3.7)" + ] + + [tool.poetry] + packages = [{include = "project", from = "src"}] + + [tool.poetry.group.dev.dependencies] + isort = "6.0.1" + + [tool.poetry.group.analysis.dependencies] + black = "25.1.0" + + [build-system] + requires = ["poetry-core>=2.0.0,<3.0.0"] + build-backend = "poetry.core.masonry.api" + """ diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index e1983bbadc..4235883cc4 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -44,7 +44,7 @@ def project_path(cwd, project_name): @pytest.fixture(scope="module") -def create_poetry_project(cwd, project_name, project_path): +def create_new_poetry_project(cwd, project_name, project_path): subprocess.run(["poetry", "new", project_name], cwd=cwd) subprocess.run( ["poetry", "add", f"{PYLINT.name}=={PYLINT.version}"], cwd=project_path @@ -60,61 +60,34 @@ def create_poetry_project(cwd, project_name, project_path): @pytest.fixture(scope="module") -def created_pyproject_toml(project_path, create_poetry_project): +def new_pyproject_toml(project_path, create_new_poetry_project): return PoetryToml.load_from_toml(working_directory=project_path) @pytest.fixture(scope="module") -def poetry_2_1_pyproject_toml(cwd, create_poetry_project): +def poetry_2_1_pyproject_toml(cwd, poetry_2_1_pyproject_text): older_project_path = cwd / "older_project" - pyproject_toml_text = """ - [project] - name = "project" - version = "0.1.0" - description = "" - authors = [] - readme = "README.md" - requires-python = ">=3.10" - dependencies = [ - "pylint (==3.3.7)" - ] - - [tool.poetry] - packages = [{include = "project", from = "src"}] - - - [tool.poetry.group.dev.dependencies] - isort = "6.0.1" - - - [tool.poetry.group.analysis.dependencies] - black = "25.1.0" - - [build-system] - requires = ["poetry-core>=2.0.0,<3.0.0"] - build-backend = "poetry.core.masonry.api" - """ older_project_path.mkdir(parents=True, exist_ok=True) pyproject_toml_path = older_project_path / "pyproject.toml" - pyproject_toml_path.write_text(cleandoc(pyproject_toml_text)) + pyproject_toml_path.write_text(cleandoc(poetry_2_1_pyproject_text)) return PoetryToml.load_from_toml(working_directory=older_project_path) @pytest.mark.slow class TestPoetryToml: @staticmethod - def test_get_section_dict_exists(created_pyproject_toml): - result = created_pyproject_toml.get_section_dict("project") + def test_get_section_dict_exists(new_pyproject_toml): + result = new_pyproject_toml.get_section_dict("project") assert result is not None @staticmethod - def test_get_section_dict_does_not_exist(created_pyproject_toml): - result = created_pyproject_toml.get_section_dict("test") + def test_get_section_dict_does_not_exist(new_pyproject_toml): + result = new_pyproject_toml.get_section_dict("test") assert result is None @staticmethod - def test_groups(created_pyproject_toml): - assert created_pyproject_toml.groups == (MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP) + def test_groups(new_pyproject_toml): + assert new_pyproject_toml.groups == (MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP) @staticmethod def test_groups_with_poetry_2_1_0(poetry_2_1_pyproject_toml): @@ -160,7 +133,7 @@ def test_extract_from_line(line, expected): @pytest.mark.slow @staticmethod - def test_direct_dependencies(create_poetry_project, project_path): + def test_direct_dependencies(create_new_poetry_project, project_path): poetry_dep = PoetryDependencies( groups=(MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP), working_directory=project_path, @@ -169,7 +142,7 @@ def test_direct_dependencies(create_poetry_project, project_path): @pytest.mark.slow @staticmethod - def test_all_dependencies(create_poetry_project, project_path): + def test_all_dependencies(create_new_poetry_project, project_path): poetry_dep = PoetryDependencies( groups=(MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP), working_directory=project_path, From bb1688bb14c3f6b26e2a8d2a6c2d0dcd8e6ec4d7 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 13:32:02 +0200 Subject: [PATCH 03/20] Expand test to another variant --- test/unit/util/dependencies/audit_test.py | 29 +++++++++++------- test/unit/util/dependencies/conftest.py | 37 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/test/unit/util/dependencies/audit_test.py b/test/unit/util/dependencies/audit_test.py index 92fff5a44f..8d50ac641a 100644 --- a/test/unit/util/dependencies/audit_test.py +++ b/test/unit/util/dependencies/audit_test.py @@ -129,36 +129,43 @@ def test_subsection_for_changelog_summary(self, sample_vulnerability): class TestExportDependenciesToFile: PACKAGES = [ "astroid", - "black", + "black", # group - analysis "click", "colorama", "dill", - "isort", + "isort", # group - dev "mccabe", "mypy-extensions", "packaging", "pathspec", "platformdirs", - "pylint", + "pylint", # main + "ruff", # optional-dependencies "tomli", "tomlkit", "typing-extensions", ] - def test_with_poetry_2_1_0(self, tmp_path, poetry_2_1_pyproject_text): - (tmp_path / "pyproject.toml").write_text(poetry_2_1_pyproject_text) + @staticmethod + def extract_package_names(content) -> list[str]: + return re.findall( + r"^([a-zA-Z0-9\-_]+)(?===|>=|<=|>|<|@)", content, re.MULTILINE + ) + + @pytest.mark.parametrize( + "pyproject_content", ["poetry_2_1_pyproject_text", "poetry_2_3_pyproject_text"] + ) + def test_poetry_export_versions(self, tmp_path, pyproject_content, request): + content_str = request.getfixturevalue(pyproject_content) + (tmp_path / "pyproject.toml").write_text(content_str) requirements_txt = tmp_path / "requirements.txt" export_dependencies_to_file( output_file=requirements_txt, working_directory=tmp_path ) - content = requirements_txt.read_text() - packages = re.findall( - r"^([a-zA-Z0-9\-_]+)(?===|>=|<=|>|<|@)", content, re.MULTILINE - ) - - assert packages == self.PACKAGES + content = requirements_txt.read_text() + assert self.extract_package_names(content) == self.PACKAGES class TestAuditPoetryFiles: diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index fdf289664b..6ae05f141a 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -24,6 +24,43 @@ def poetry_2_1_pyproject_text() -> str: [tool.poetry.group.analysis.dependencies] black = "25.1.0" + [project.optional-dependencies] + ruff = [ "ruff (==0.14.14)" ] + + [build-system] + requires = ["poetry-core>=2.0.0,<3.0.0"] + build-backend = "poetry.core.masonry.api" + """ + + +@pytest.fixture(scope="module") +def poetry_2_3_pyproject_text() -> str: + return """ + [project] + name = "project" + version = "0.1.0" + description = "" + authors = [] + readme = "README.md" + requires-python = ">=3.10" + dependencies = [ + "pylint (==3.3.7)" + ] + + [tool.poetry] + packages = [{include = "project", from = "src"}] + + [dependency-groups] + dev = [ + "isort==6.0.1", + ] + analysis = [ + "black==25.1.0" + ] + + [project.optional-dependencies] + ruff = [ "ruff (==0.14.14)" ] + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" From e5d280083e48d0968ccd9858ad9f8317d8380149 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 14:04:33 +0200 Subject: [PATCH 04/20] Add sample versions to conftest for later simplified usage --- test/unit/util/dependencies/conftest.py | 42 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 6ae05f141a..130a7abbbd 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -1,9 +1,23 @@ +from enum import Enum + import pytest +class SampleVersions(str, Enum): + black = "25.1.0" + isort = "6.0.1" + pylint = "3.3.7" + ruff = "0.14.14" + + @pytest.fixture(scope="module") -def poetry_2_1_pyproject_text() -> str: - return """ +def sample_versions(): + return SampleVersions + + +@pytest.fixture(scope="module") +def poetry_2_1_pyproject_text(sample_versions) -> str: + return f""" [project] name = "project" version = "0.1.0" @@ -12,20 +26,20 @@ def poetry_2_1_pyproject_text() -> str: readme = "README.md" requires-python = ">=3.10" dependencies = [ - "pylint (==3.3.7)" + "pylint (=={sample_versions.pylint})" ] [tool.poetry] - packages = [{include = "project", from = "src"}] + packages = [{{include = "project", from = "src"}}] [tool.poetry.group.dev.dependencies] - isort = "6.0.1" + isort = "{sample_versions.isort}" [tool.poetry.group.analysis.dependencies] - black = "25.1.0" + black = "{sample_versions.black}" [project.optional-dependencies] - ruff = [ "ruff (==0.14.14)" ] + ruff = [ "ruff (=={sample_versions.ruff})" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -34,8 +48,8 @@ def poetry_2_1_pyproject_text() -> str: @pytest.fixture(scope="module") -def poetry_2_3_pyproject_text() -> str: - return """ +def poetry_2_3_pyproject_text(sample_versions) -> str: + return f""" [project] name = "project" version = "0.1.0" @@ -44,22 +58,22 @@ def poetry_2_3_pyproject_text() -> str: readme = "README.md" requires-python = ">=3.10" dependencies = [ - "pylint (==3.3.7)" + "pylint (=={sample_versions.pylint})" ] [tool.poetry] - packages = [{include = "project", from = "src"}] + packages = [{{include = "project", from = "src"}}] [dependency-groups] dev = [ - "isort==6.0.1", + "isort=={sample_versions.isort}", ] analysis = [ - "black==25.1.0" + "black=={sample_versions.black}" ] [project.optional-dependencies] - ruff = [ "ruff (==0.14.14)" ] + ruff = [ "ruff (=={sample_versions.ruff})" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] From 2181b075d5e1e8a1ea5a812dc2aab5759df7eafe Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 14:15:14 +0200 Subject: [PATCH 05/20] Simplify setup --- .../dependencies/poetry_dependencies_test.py | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index 4235883cc4..8d49b6f26d 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -14,18 +14,11 @@ from noxconfig import PROJECT_CONFIG MAIN_GROUP = PoetryGroup(name="main", toml_section="project.dependencies") -DEV_GROUP = PoetryGroup(name="dev", toml_section="dependency-groups.dev") -ANALYSIS_GROUP = PoetryGroup(name="analysis", toml_section="dependency-groups.analysis") - -PYLINT = Package(name="pylint", version="3.3.7") -ISORT = Package(name="isort", version="6.0.1") -BLACK = Package(name="black", version="25.1.0") - -DIRECT_DEPENDENCIES = { - MAIN_GROUP.name: {PYLINT.name: PYLINT}, - DEV_GROUP.name: {ISORT.name: ISORT}, - ANALYSIS_GROUP.name: {BLACK.name: BLACK}, -} +GROUPS = ( + MAIN_GROUP, + PoetryGroup(name="dev", toml_section="dependency-groups.dev"), + PoetryGroup(name="analysis", toml_section="dependency-groups.analysis"), +) @pytest.fixture(scope="module") @@ -44,17 +37,17 @@ def project_path(cwd, project_name): @pytest.fixture(scope="module") -def create_new_poetry_project(cwd, project_name, project_path): +def create_new_poetry_project(cwd, project_name, project_path, sample_versions): subprocess.run(["poetry", "new", project_name], cwd=cwd) subprocess.run( - ["poetry", "add", f"{PYLINT.name}=={PYLINT.version}"], cwd=project_path + ["poetry", "add", f"pylint=={sample_versions.pylint}"], cwd=project_path ) subprocess.run( - ["poetry", "add", "--group", "dev", f"{ISORT.name}=={ISORT.version}"], + ["poetry", "add", "--group", "dev", f"isort=={sample_versions.isort}"], cwd=project_path, ) subprocess.run( - ["poetry", "add", "--group", "analysis", f"{BLACK.name}=={BLACK.version}"], + ["poetry", "add", "--group", "analysis", f"black=={sample_versions.black}"], cwd=project_path, ) @@ -87,7 +80,7 @@ def test_get_section_dict_does_not_exist(new_pyproject_toml): @staticmethod def test_groups(new_pyproject_toml): - assert new_pyproject_toml.groups == (MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP) + assert new_pyproject_toml.groups == GROUPS @staticmethod def test_groups_with_poetry_2_1_0(poetry_2_1_pyproject_toml): @@ -133,25 +126,35 @@ def test_extract_from_line(line, expected): @pytest.mark.slow @staticmethod - def test_direct_dependencies(create_new_poetry_project, project_path): + def test_direct_dependencies( + create_new_poetry_project, project_path, sample_versions + ): poetry_dep = PoetryDependencies( - groups=(MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP), + groups=GROUPS, working_directory=project_path, ) - assert poetry_dep.direct_dependencies == DIRECT_DEPENDENCIES + assert poetry_dep.direct_dependencies == { + "main": {"pylint": Package(name="pylint", version=sample_versions.pylint)}, + "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, + "analysis": {"black": Package(name="black", version=sample_versions.black)}, + } @pytest.mark.slow @staticmethod - def test_all_dependencies(create_new_poetry_project, project_path): + def test_all_dependencies(create_new_poetry_project, project_path, sample_versions): poetry_dep = PoetryDependencies( - groups=(MAIN_GROUP, DEV_GROUP, ANALYSIS_GROUP), + groups=GROUPS, working_directory=project_path, ) result = poetry_dep.all_dependencies transitive = result.pop("transitive") assert len(transitive) > 0 - assert result == DIRECT_DEPENDENCIES + assert result == { + "main": {"pylint": Package(name="pylint", version=sample_versions.pylint)}, + "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, + "analysis": {"black": Package(name="black", version=sample_versions.black)}, + } @pytest.mark.slow From 7c955487aa62331202c5644b1ec72a4597738de7 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 14:23:55 +0200 Subject: [PATCH 06/20] Move fixtures around for simplified usage and prevent local poetry changes --- test/conftest.py | 8 +++++ test/integration/conftest.py | 8 ----- test/unit/util/dependencies/conftest.py | 31 ++++++++++++++++++ .../dependencies/poetry_dependencies_test.py | 32 ------------------- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 8c7b692217..f4df3fea56 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,4 +1,5 @@ import json +import subprocess from inspect import cleandoc import pytest @@ -14,6 +15,13 @@ ) +@pytest.fixture(scope="session") +def poetry_path() -> str: + result = subprocess.run(["which", "poetry"], capture_output=True, text=True) + poetry_path = result.stdout.strip() + return poetry_path + + class SampleVulnerability: package_name = "jinja2" version = "3.1.5" diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 4fb7039e14..c19423388e 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -1,4 +1,3 @@ -import subprocess from pathlib import Path import pytest @@ -6,13 +5,6 @@ from exasol.toolbox.config import BaseConfig -@pytest.fixture(scope="session") -def poetry_path() -> str: - result = subprocess.run(["which", "poetry"], capture_output=True, text=True) - poetry_path = result.stdout.strip() - return poetry_path - - @pytest.fixture(scope="session") def ptb_minimum_python_version() -> str: """ diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 130a7abbbd..22e4d10045 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -1,3 +1,4 @@ +import subprocess from enum import Enum import pytest @@ -79,3 +80,33 @@ def poetry_2_3_pyproject_text(sample_versions) -> str: requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" """ + + +@pytest.fixture(scope="module") +def cwd(tmp_path_factory): + return tmp_path_factory.mktemp("test") + + +@pytest.fixture(scope="module") +def project_name(): + return "project" + + +@pytest.fixture(scope="module") +def project_path(cwd, project_name): + return cwd / project_name + + +@pytest.fixture(scope="module") +def create_new_poetry_project( + poetry_path, cwd, project_name, project_path, sample_versions +): + subprocess.run([poetry_path, "new", project_name], cwd=cwd, check=True) + + commands = [ + [poetry_path, "add", f"pylint=={sample_versions.pylint}"], + [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort}"], + [poetry_path, "add", "--group", "analysis", f"black=={sample_versions.black}"], + ] + for cmd in commands: + subprocess.run(cmd, cwd=project_path, env={}, check=True) diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index 8d49b6f26d..b2265b1cd1 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -1,4 +1,3 @@ -import subprocess from inspect import cleandoc import pytest @@ -21,37 +20,6 @@ ) -@pytest.fixture(scope="module") -def cwd(tmp_path_factory): - return tmp_path_factory.mktemp("test") - - -@pytest.fixture(scope="module") -def project_name(): - return "project" - - -@pytest.fixture(scope="module") -def project_path(cwd, project_name): - return cwd / project_name - - -@pytest.fixture(scope="module") -def create_new_poetry_project(cwd, project_name, project_path, sample_versions): - subprocess.run(["poetry", "new", project_name], cwd=cwd) - subprocess.run( - ["poetry", "add", f"pylint=={sample_versions.pylint}"], cwd=project_path - ) - subprocess.run( - ["poetry", "add", "--group", "dev", f"isort=={sample_versions.isort}"], - cwd=project_path, - ) - subprocess.run( - ["poetry", "add", "--group", "analysis", f"black=={sample_versions.black}"], - cwd=project_path, - ) - - @pytest.fixture(scope="module") def new_pyproject_toml(project_path, create_new_poetry_project): return PoetryToml.load_from_toml(working_directory=project_path) From cad38f4997be7e088a70df05b5e02d45a930ccc9 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 14:30:56 +0200 Subject: [PATCH 07/20] Use new fixture and add optional dependency ruff --- test/unit/util/dependencies/audit_test.py | 13 +++++++++++-- test/unit/util/dependencies/conftest.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/unit/util/dependencies/audit_test.py b/test/unit/util/dependencies/audit_test.py index 8d50ac641a..994dc0b2d0 100644 --- a/test/unit/util/dependencies/audit_test.py +++ b/test/unit/util/dependencies/audit_test.py @@ -96,7 +96,6 @@ def test_reference_links(sample_vulnerability, reference: str, expected: list[st ), ) def test_vulnerability_id(self, sample_vulnerability, aliases: list[str], expected): - result = Vulnerability( package=sample_vulnerability.vulnerability.package, id="DUMMY_IDENTIFIER", @@ -126,6 +125,11 @@ def test_subsection_for_changelog_summary(self, sample_vulnerability): ) +@pytest.fixture(scope="module") +def new_pyproject_toml(create_new_poetry_project, project_path): + return (project_path / "pyproject.toml").read_text() + + class TestExportDependenciesToFile: PACKAGES = [ "astroid", @@ -153,7 +157,12 @@ def extract_package_names(content) -> list[str]: ) @pytest.mark.parametrize( - "pyproject_content", ["poetry_2_1_pyproject_text", "poetry_2_3_pyproject_text"] + "pyproject_content", + [ + "poetry_2_1_pyproject_text", + "poetry_2_3_pyproject_text", + "new_pyproject_toml", + ], ) def test_poetry_export_versions(self, tmp_path, pyproject_content, request): content_str = request.getfixturevalue(pyproject_content) diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 22e4d10045..6741a20765 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -107,6 +107,7 @@ def create_new_poetry_project( [poetry_path, "add", f"pylint=={sample_versions.pylint}"], [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort}"], [poetry_path, "add", "--group", "analysis", f"black=={sample_versions.black}"], + [poetry_path, "add", f"ruff@{sample_versions.ruff}", "--optional", "ruff"], ] for cmd in commands: subprocess.run(cmd, cwd=project_path, env={}, check=True) From d4a1a4b6f43c28ee83b432490e2ea972676d9da1 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 14:58:08 +0200 Subject: [PATCH 08/20] Update poetry dependencies and test --- .../toolbox/util/dependencies/poetry_dependencies.py | 4 ++++ .../unit/util/dependencies/poetry_dependencies_test.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/exasol/toolbox/util/dependencies/poetry_dependencies.py b/exasol/toolbox/util/dependencies/poetry_dependencies.py index 26a0265565..e08b74a853 100644 --- a/exasol/toolbox/util/dependencies/poetry_dependencies.py +++ b/exasol/toolbox/util/dependencies/poetry_dependencies.py @@ -59,14 +59,17 @@ def get_section_dict(self, section: str) -> dict | None: def groups(self) -> tuple[PoetryGroup, ...]: groups = [] + # Main Dependencies main_key = "project.dependencies" if self.get_section_dict(main_key): groups.append(PoetryGroup(name="main", toml_section=main_key)) + # Legacy Poetry Main Dependencies main_dynamic_key = "tool.poetry.dependencies" if self.get_section_dict(main_dynamic_key): groups.append(PoetryGroup(name="main", toml_section=main_dynamic_key)) + # Legacy Poetry Group Dependencies group_key = "tool.poetry.group" if group_dict := self.get_section_dict(group_key): for group, content in group_dict.items(): @@ -78,6 +81,7 @@ def groups(self) -> tuple[PoetryGroup, ...]: ) ) + # Poetry Group Dependencies new_group_key = "dependency-groups" if group_dict := self.get_section_dict(new_group_key): for group, content in group_dict.items(): diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index b2265b1cd1..ecc7144f51 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -102,7 +102,10 @@ def test_direct_dependencies( working_directory=project_path, ) assert poetry_dep.direct_dependencies == { - "main": {"pylint": Package(name="pylint", version=sample_versions.pylint)}, + "main": { + "pylint": Package(name="pylint", version=sample_versions.pylint), + "ruff": Package(name="ruff", version=sample_versions.ruff), + }, "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, "analysis": {"black": Package(name="black", version=sample_versions.black)}, } @@ -119,7 +122,10 @@ def test_all_dependencies(create_new_poetry_project, project_path, sample_versio transitive = result.pop("transitive") assert len(transitive) > 0 assert result == { - "main": {"pylint": Package(name="pylint", version=sample_versions.pylint)}, + "main": { + "pylint": Package(name="pylint", version=sample_versions.pylint), + "ruff": Package(name="ruff", version=sample_versions.ruff), + }, "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, "analysis": {"black": Package(name="black", version=sample_versions.black)}, } From b958f5f0ce088ba65b27eede02063ef30f6b621f Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 24 Apr 2026 15:11:57 +0200 Subject: [PATCH 09/20] Add export plugin to conftest --- test/unit/util/dependencies/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 6741a20765..055b793809 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -104,6 +104,7 @@ def create_new_poetry_project( subprocess.run([poetry_path, "new", project_name], cwd=cwd, check=True) commands = [ + [poetry_path, "self", "add", "poetry-plugin-export"], [poetry_path, "add", f"pylint=={sample_versions.pylint}"], [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort}"], [poetry_path, "add", "--group", "analysis", f"black=={sample_versions.black}"], From b3e40668589baa55476960a828b83740cf861934 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Mon, 27 Apr 2026 09:00:30 +0200 Subject: [PATCH 10/20] With poetry export fixture --- test/conftest.py | 15 +++++++++++++++ test/unit/util/dependencies/audit_test.py | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/test/conftest.py b/test/conftest.py index f4df3fea56..8ef0af2784 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,4 +1,5 @@ import json +import os import subprocess from inspect import cleandoc @@ -22,6 +23,20 @@ def poetry_path() -> str: return poetry_path +@pytest.fixture +def install_poetry_export(poetry_path, monkeypatch): + monkeypatch.setenv("PATH", poetry_path, prepend=os.pathsep) + + def _install(cwd): + subprocess.run( + ["poetry", "self", "add", "poetry-plugin-export"], + cwd=cwd, + check=True, + ) + + return _install + + class SampleVulnerability: package_name = "jinja2" version = "3.1.5" diff --git a/test/unit/util/dependencies/audit_test.py b/test/unit/util/dependencies/audit_test.py index 994dc0b2d0..9dccc73694 100644 --- a/test/unit/util/dependencies/audit_test.py +++ b/test/unit/util/dependencies/audit_test.py @@ -164,11 +164,15 @@ def extract_package_names(content) -> list[str]: "new_pyproject_toml", ], ) - def test_poetry_export_versions(self, tmp_path, pyproject_content, request): + def test_poetry_export_versions( + self, install_poetry_export, tmp_path, pyproject_content, request + ): content_str = request.getfixturevalue(pyproject_content) (tmp_path / "pyproject.toml").write_text(content_str) requirements_txt = tmp_path / "requirements.txt" + install_poetry_export(cwd=tmp_path) + export_dependencies_to_file( output_file=requirements_txt, working_directory=tmp_path ) From f29d25e5981f5d28a9958817b77f1a63c6de8244 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 10:13:23 +0200 Subject: [PATCH 11/20] Try with changes to export fixture --- test/conftest.py | 12 +-- test/unit/util/dependencies/conftest.py | 82 +++++++++++++++---- .../dependencies/poetry_dependencies_test.py | 30 ++++--- 3 files changed, 93 insertions(+), 31 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 8ef0af2784..009eb17e96 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,6 +2,7 @@ import os import subprocess from inspect import cleandoc +from pathlib import Path import pytest @@ -18,18 +19,19 @@ @pytest.fixture(scope="session") def poetry_path() -> str: - result = subprocess.run(["which", "poetry"], capture_output=True, text=True) - poetry_path = result.stdout.strip() - return poetry_path + result = subprocess.run( + ["which", "poetry"], capture_output=True, text=True, check=True + ) + return result.stdout.strip() @pytest.fixture def install_poetry_export(poetry_path, monkeypatch): - monkeypatch.setenv("PATH", poetry_path, prepend=os.pathsep) + monkeypatch.setenv("PATH", str(Path(poetry_path).parent), prepend=os.pathsep) def _install(cwd): subprocess.run( - ["poetry", "self", "add", "poetry-plugin-export"], + [poetry_path, "self", "add", "poetry-plugin-export"], cwd=cwd, check=True, ) diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 055b793809..6fbafd1cf4 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -1,3 +1,4 @@ +import os import subprocess from enum import Enum @@ -11,6 +12,19 @@ class SampleVersions(str, Enum): ruff = "0.14.14" +def _path_without_active_virtualenv() -> str: + path = os.environ.get("PATH", "") + if not (virtualenv := os.environ.get("VIRTUAL_ENV")): + return path + + virtualenv_bin = os.path.abspath(os.path.join(virtualenv, "bin")) + return os.pathsep.join( + part + for part in path.split(os.pathsep) + if os.path.abspath(part) != virtualenv_bin + ) + + @pytest.fixture(scope="module") def sample_versions(): return SampleVersions @@ -27,20 +41,20 @@ def poetry_2_1_pyproject_text(sample_versions) -> str: readme = "README.md" requires-python = ">=3.10" dependencies = [ - "pylint (=={sample_versions.pylint})" + "pylint (=={sample_versions.pylint.value})" ] [tool.poetry] packages = [{{include = "project", from = "src"}}] [tool.poetry.group.dev.dependencies] - isort = "{sample_versions.isort}" + isort = "{sample_versions.isort.value}" [tool.poetry.group.analysis.dependencies] - black = "{sample_versions.black}" + black = "{sample_versions.black.value}" [project.optional-dependencies] - ruff = [ "ruff (=={sample_versions.ruff})" ] + ruff = [ "ruff (=={sample_versions.ruff.value})" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -59,7 +73,7 @@ def poetry_2_3_pyproject_text(sample_versions) -> str: readme = "README.md" requires-python = ">=3.10" dependencies = [ - "pylint (=={sample_versions.pylint})" + "pylint (=={sample_versions.pylint.value})" ] [tool.poetry] @@ -67,14 +81,14 @@ def poetry_2_3_pyproject_text(sample_versions) -> str: [dependency-groups] dev = [ - "isort=={sample_versions.isort}", + "isort=={sample_versions.isort.value}", ] analysis = [ - "black=={sample_versions.black}" + "black=={sample_versions.black.value}" ] [project.optional-dependencies] - ruff = [ "ruff (=={sample_versions.ruff})" ] + ruff = [ "ruff (=={sample_versions.ruff.value})" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -97,18 +111,54 @@ def project_path(cwd, project_name): return cwd / project_name +@pytest.fixture(scope="module") +def poetry_env(cwd): + return { + "HOME": str(cwd), + "PATH": _path_without_active_virtualenv(), + "POETRY_CACHE_DIR": str(cwd / ".cache" / "pypoetry"), + "POETRY_CONFIG_DIR": str(cwd / ".config" / "pypoetry"), + "POETRY_DATA_DIR": str(cwd / ".local" / "share" / "pypoetry"), + "POETRY_VIRTUALENVS_IN_PROJECT": "true", + } + + +@pytest.fixture +def isolated_poetry_env(monkeypatch, poetry_env): + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + monkeypatch.delenv("POETRY_ACTIVE", raising=False) + for name, value in poetry_env.items(): + monkeypatch.setenv(name, value) + + @pytest.fixture(scope="module") def create_new_poetry_project( - poetry_path, cwd, project_name, project_path, sample_versions + poetry_path, cwd, project_name, project_path, sample_versions, poetry_env ): - subprocess.run([poetry_path, "new", project_name], cwd=cwd, check=True) + subprocess.run( + [poetry_path, "new", "--python=>=3.10", project_name], + cwd=cwd, + env=poetry_env, + check=True, + ) commands = [ - [poetry_path, "self", "add", "poetry-plugin-export"], - [poetry_path, "add", f"pylint=={sample_versions.pylint}"], - [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort}"], - [poetry_path, "add", "--group", "analysis", f"black=={sample_versions.black}"], - [poetry_path, "add", f"ruff@{sample_versions.ruff}", "--optional", "ruff"], + [poetry_path, "add", f"pylint=={sample_versions.pylint.value}"], + [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort.value}"], + [ + poetry_path, + "add", + "--group", + "analysis", + f"black=={sample_versions.black.value}", + ], + [ + poetry_path, + "add", + f"ruff@{sample_versions.ruff.value}", + "--optional", + "ruff", + ], ] for cmd in commands: - subprocess.run(cmd, cwd=project_path, env={}, check=True) + subprocess.run(cmd, cwd=project_path, env=poetry_env, check=True) diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index ecc7144f51..8647b753fb 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -95,7 +95,7 @@ def test_extract_from_line(line, expected): @pytest.mark.slow @staticmethod def test_direct_dependencies( - create_new_poetry_project, project_path, sample_versions + create_new_poetry_project, project_path, sample_versions, isolated_poetry_env ): poetry_dep = PoetryDependencies( groups=GROUPS, @@ -103,16 +103,22 @@ def test_direct_dependencies( ) assert poetry_dep.direct_dependencies == { "main": { - "pylint": Package(name="pylint", version=sample_versions.pylint), - "ruff": Package(name="ruff", version=sample_versions.ruff), + "pylint": Package(name="pylint", version=sample_versions.pylint.value), + "ruff": Package(name="ruff", version=sample_versions.ruff.value), + }, + "dev": { + "isort": Package(name="isort", version=sample_versions.isort.value) + }, + "analysis": { + "black": Package(name="black", version=sample_versions.black.value) }, - "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, - "analysis": {"black": Package(name="black", version=sample_versions.black)}, } @pytest.mark.slow @staticmethod - def test_all_dependencies(create_new_poetry_project, project_path, sample_versions): + def test_all_dependencies( + create_new_poetry_project, project_path, sample_versions, isolated_poetry_env + ): poetry_dep = PoetryDependencies( groups=GROUPS, working_directory=project_path, @@ -123,11 +129,15 @@ def test_all_dependencies(create_new_poetry_project, project_path, sample_versio assert len(transitive) > 0 assert result == { "main": { - "pylint": Package(name="pylint", version=sample_versions.pylint), - "ruff": Package(name="ruff", version=sample_versions.ruff), + "pylint": Package(name="pylint", version=sample_versions.pylint.value), + "ruff": Package(name="ruff", version=sample_versions.ruff.value), + }, + "dev": { + "isort": Package(name="isort", version=sample_versions.isort.value) + }, + "analysis": { + "black": Package(name="black", version=sample_versions.black.value) }, - "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, - "analysis": {"black": Package(name="black", version=sample_versions.black)}, } From 19f5b9542bada49badd27d90a3c269c6bdba3aee Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 10:32:00 +0200 Subject: [PATCH 12/20] Remove hashes as unneeded for functionality --- exasol/toolbox/util/dependencies/audit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index b3ffc111db..5ad889052f 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -173,6 +173,7 @@ def export_dependencies_to_file(output_file: Path, working_directory: Path) -> N "--format=requirements.txt", "--all-groups", "--all-extras", + "--without-hashes", ] output = subprocess.run( command, From 4fed2f3c79b3449a5d0a981c69830a151d8338d4 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 10:45:47 +0200 Subject: [PATCH 13/20] Bump pip --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8c20438dfc..18b78aea2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2219,14 +2219,14 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "pip" -version = "26.0.1" +version = "26.1" description = "The PyPA recommended tool for installing Python packages." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}, - {file = "pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}, + {file = "pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1"}, + {file = "pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3"}, ] [[package]] From 06fd123d590fb53f5039f7d8e72d6fd8e6539a92 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 11:26:15 +0200 Subject: [PATCH 14/20] Fix to resolve failing integration test. There was a mismatch in the installed PTB of the temporary project (6.4.0) and the available code. --- test/integration/project-template/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/integration/project-template/conftest.py b/test/integration/project-template/conftest.py index b115521175..b5b452f4bd 100644 --- a/test/integration/project-template/conftest.py +++ b/test/integration/project-template/conftest.py @@ -53,7 +53,12 @@ def poetry_install(run_command, poetry_path): # dependency to the PTB itself in the pyproject.toml file by replacing the latest # released PTB version with the current checked-out branch in # PROJECT_CONFIG.root_path: - run_command([poetry_path, "add", "--group", "dev", PROJECT_CONFIG.root_path]) + run_command( + [poetry_path, "add", "--group", "dev", "--editable", PROJECT_CONFIG.root_path] + ) + # This is needed due to pysonar hard-pinning requests. Without this addition, + # the selected requests has an active vulnerability. + run_command([poetry_path, "add", "--group", "dev", "requests>=2.33.0"]) run_command([poetry_path, "install"]) From 21480f5d44c73ad18744ed3c66232d5748e82674 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 11:50:31 +0200 Subject: [PATCH 15/20] Switch so .value not needed everywhere --- test/unit/util/dependencies/conftest.py | 27 ++++++++++--------- .../dependencies/poetry_dependencies_test.py | 24 ++++++----------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 6fbafd1cf4..20c67333af 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -11,6 +11,9 @@ class SampleVersions(str, Enum): pylint = "3.3.7" ruff = "0.14.14" + def __str__(self) -> str: + return self.value + def _path_without_active_virtualenv() -> str: path = os.environ.get("PATH", "") @@ -41,20 +44,20 @@ def poetry_2_1_pyproject_text(sample_versions) -> str: readme = "README.md" requires-python = ">=3.10" dependencies = [ - "pylint (=={sample_versions.pylint.value})" + "pylint (=={sample_versions.pylint})" ] [tool.poetry] packages = [{{include = "project", from = "src"}}] [tool.poetry.group.dev.dependencies] - isort = "{sample_versions.isort.value}" + isort = "{sample_versions.isort}" [tool.poetry.group.analysis.dependencies] - black = "{sample_versions.black.value}" + black = "{sample_versions.black}" [project.optional-dependencies] - ruff = [ "ruff (=={sample_versions.ruff.value})" ] + ruff = [ "ruff (=={sample_versions.ruff})" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -73,7 +76,7 @@ def poetry_2_3_pyproject_text(sample_versions) -> str: readme = "README.md" requires-python = ">=3.10" dependencies = [ - "pylint (=={sample_versions.pylint.value})" + "pylint (=={sample_versions.pylint})" ] [tool.poetry] @@ -81,14 +84,14 @@ def poetry_2_3_pyproject_text(sample_versions) -> str: [dependency-groups] dev = [ - "isort=={sample_versions.isort.value}", + "isort=={sample_versions.isort}", ] analysis = [ - "black=={sample_versions.black.value}" + "black=={sample_versions.black}" ] [project.optional-dependencies] - ruff = [ "ruff (=={sample_versions.ruff.value})" ] + ruff = [ "ruff (=={sample_versions.ruff})" ] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] @@ -143,19 +146,19 @@ def create_new_poetry_project( ) commands = [ - [poetry_path, "add", f"pylint=={sample_versions.pylint.value}"], - [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort.value}"], + [poetry_path, "add", f"pylint=={sample_versions.pylint}"], + [poetry_path, "add", "--group", "dev", f"isort=={sample_versions.isort}"], [ poetry_path, "add", "--group", "analysis", - f"black=={sample_versions.black.value}", + f"black=={sample_versions.black}", ], [ poetry_path, "add", - f"ruff@{sample_versions.ruff.value}", + f"ruff@{sample_versions.ruff}", "--optional", "ruff", ], diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index 8647b753fb..ed07aee733 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -103,15 +103,11 @@ def test_direct_dependencies( ) assert poetry_dep.direct_dependencies == { "main": { - "pylint": Package(name="pylint", version=sample_versions.pylint.value), - "ruff": Package(name="ruff", version=sample_versions.ruff.value), - }, - "dev": { - "isort": Package(name="isort", version=sample_versions.isort.value) - }, - "analysis": { - "black": Package(name="black", version=sample_versions.black.value) + "pylint": Package(name="pylint", version=sample_versions.pylint), + "ruff": Package(name="ruff", version=sample_versions.ruff), }, + "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, + "analysis": {"black": Package(name="black", version=sample_versions.black)}, } @pytest.mark.slow @@ -129,15 +125,11 @@ def test_all_dependencies( assert len(transitive) > 0 assert result == { "main": { - "pylint": Package(name="pylint", version=sample_versions.pylint.value), - "ruff": Package(name="ruff", version=sample_versions.ruff.value), - }, - "dev": { - "isort": Package(name="isort", version=sample_versions.isort.value) - }, - "analysis": { - "black": Package(name="black", version=sample_versions.black.value) + "pylint": Package(name="pylint", version=sample_versions.pylint), + "ruff": Package(name="ruff", version=sample_versions.ruff), }, + "dev": {"isort": Package(name="isort", version=sample_versions.isort)}, + "analysis": {"black": Package(name="black", version=sample_versions.black)}, } From db9ee62dacb10bf1998337c099d417e83df69438 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 11:55:04 +0200 Subject: [PATCH 16/20] Modify regex so can handle more lines. Currently, if there is a warning, this is emitted first (depends on Python version), which can cause issues --- exasol/toolbox/util/dependencies/audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 5ad889052f..6a22c1b9bb 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -23,7 +23,7 @@ ) PIP_AUDIT_VULNERABILITY_PATTERN = ( - r"^Found \d+ known vulnerabilit\w{1,3} in \d+ package\w?$" + r"(?m)^Found \d+ known vulnerabilit(?:y|ies) in \d+ package(?:s)?$" ) PipAuditEntry = dict[str, str | list[str] | tuple[str, ...]] From 02b2b4425db7bc386187b0908d5b8f54b2f9b199 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 12:06:57 +0200 Subject: [PATCH 17/20] Reduce duplicated structures to 1 --- test/unit/util/dependencies/conftest.py | 25 ++++++------------- .../dependencies/poetry_dependencies_test.py | 4 +-- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/test/unit/util/dependencies/conftest.py b/test/unit/util/dependencies/conftest.py index 20c67333af..217e682c73 100644 --- a/test/unit/util/dependencies/conftest.py +++ b/test/unit/util/dependencies/conftest.py @@ -116,22 +116,12 @@ def project_path(cwd, project_name): @pytest.fixture(scope="module") def poetry_env(cwd): - return { - "HOME": str(cwd), - "PATH": _path_without_active_virtualenv(), - "POETRY_CACHE_DIR": str(cwd / ".cache" / "pypoetry"), - "POETRY_CONFIG_DIR": str(cwd / ".config" / "pypoetry"), - "POETRY_DATA_DIR": str(cwd / ".local" / "share" / "pypoetry"), - "POETRY_VIRTUALENVS_IN_PROJECT": "true", - } - - -@pytest.fixture -def isolated_poetry_env(monkeypatch, poetry_env): - monkeypatch.delenv("VIRTUAL_ENV", raising=False) - monkeypatch.delenv("POETRY_ACTIVE", raising=False) - for name, value in poetry_env.items(): - monkeypatch.setenv(name, value) + env_patch = pytest.MonkeyPatch() + env_patch.delenv("VIRTUAL_ENV", raising=False) + env_patch.setenv("HOME", str(cwd)) + env_patch.setenv("PATH", _path_without_active_virtualenv()) + yield + env_patch.undo() @pytest.fixture(scope="module") @@ -141,7 +131,6 @@ def create_new_poetry_project( subprocess.run( [poetry_path, "new", "--python=>=3.10", project_name], cwd=cwd, - env=poetry_env, check=True, ) @@ -164,4 +153,4 @@ def create_new_poetry_project( ], ] for cmd in commands: - subprocess.run(cmd, cwd=project_path, env=poetry_env, check=True) + subprocess.run(cmd, cwd=project_path, check=True) diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index ed07aee733..463131eda3 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -95,7 +95,7 @@ def test_extract_from_line(line, expected): @pytest.mark.slow @staticmethod def test_direct_dependencies( - create_new_poetry_project, project_path, sample_versions, isolated_poetry_env + create_new_poetry_project, project_path, sample_versions, poetry_env ): poetry_dep = PoetryDependencies( groups=GROUPS, @@ -113,7 +113,7 @@ def test_direct_dependencies( @pytest.mark.slow @staticmethod def test_all_dependencies( - create_new_poetry_project, project_path, sample_versions, isolated_poetry_env + create_new_poetry_project, project_path, sample_versions, poetry_env ): poetry_dep = PoetryDependencies( groups=GROUPS, From 2fc3455365a9beed68be78e4b50a0217461e1b11 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 28 Apr 2026 12:17:38 +0200 Subject: [PATCH 18/20] Add changelog entry --- doc/changes/unreleased.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 760278667c..edb2b0cca1 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -14,6 +14,10 @@ This is required for the nox session `docs:multiversion` to successfully complet and it is a Python standard for users to check in the terminal which version they are using. +## Feature + +* #803: Included other dependencies for local `pip-audit` check + ## Refactoring * #800: Removed tbx security pretty-print, tbx lint pretty-print, and creation of .lint.txt, as superseded by Sonar and .lint.json usage From 964748dc2e59fd7515fb17885d224a0230346dc1 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 29 Apr 2026 08:57:23 +0200 Subject: [PATCH 19/20] Apply review suggestion: use CLI output --- exasol/toolbox/util/dependencies/audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 6a22c1b9bb..176d4e5142 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -174,6 +174,8 @@ def export_dependencies_to_file(output_file: Path, working_directory: Path) -> N "--all-groups", "--all-extras", "--without-hashes", + "-o", + str(output_file), ] output = subprocess.run( command, @@ -184,8 +186,6 @@ def export_dependencies_to_file(output_file: Path, working_directory: Path) -> N if output.returncode != 0: raise PipAuditException.from_subprocess(output, command, cwd=working_directory) - output_file.write_text(output.stdout) - def audit_poetry_files(working_directory: Path) -> str: """ From 5f846c0c1effeb5c44f0a9e23b29ff04787acba2 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 29 Apr 2026 09:00:43 +0200 Subject: [PATCH 20/20] Apply review suggestion: create a general exception that we use for specific cases --- exasol/toolbox/util/dependencies/audit.py | 14 +++++++++++--- test/unit/util/dependencies/audit_test.py | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/exasol/toolbox/util/dependencies/audit.py b/exasol/toolbox/util/dependencies/audit.py index 176d4e5142..7a18cc3e7c 100644 --- a/exasol/toolbox/util/dependencies/audit.py +++ b/exasol/toolbox/util/dependencies/audit.py @@ -30,7 +30,7 @@ @dataclass -class PipAuditException(Exception): +class SubprocessException(Exception): command: list[str] cwd: Path env: dict[str, str] @@ -45,10 +45,18 @@ def from_subprocess( command: list[str], cwd: Path, env: dict[str, str] | None = None, - ) -> PipAuditException: + ) -> SubprocessException: return cls(command, cwd, env or {}, proc.returncode, proc.stdout, proc.stderr) +class PipAuditException(SubprocessException): + pass + + +class PoetryException(SubprocessException): + pass + + class VulnerabilitySource(str, Enum): CVE = "CVE" CWE = "CWE" @@ -184,7 +192,7 @@ def export_dependencies_to_file(output_file: Path, working_directory: Path) -> N cwd=working_directory, ) # nosec: B603 - allow fixed poetry usage if output.returncode != 0: - raise PipAuditException.from_subprocess(output, command, cwd=working_directory) + raise PoetryException.from_subprocess(output, command, cwd=working_directory) def audit_poetry_files(working_directory: Path) -> str: diff --git a/test/unit/util/dependencies/audit_test.py b/test/unit/util/dependencies/audit_test.py index 9dccc73694..cc414b0d23 100644 --- a/test/unit/util/dependencies/audit_test.py +++ b/test/unit/util/dependencies/audit_test.py @@ -10,6 +10,7 @@ from exasol.toolbox.util.dependencies.audit import ( PipAuditException, + PoetryException, Vulnerabilities, Vulnerability, VulnerabilitySource, @@ -191,7 +192,7 @@ def test_poetry_export_fails(mock_run): result.stderr = "pyproject.toml changed significantly since poetry.lock was last generated. Run `poetry lock` to fix the lock file.\n" mock_run.return_value = result - with pytest.raises(PipAuditException) as e: + with pytest.raises(PoetryException) as e: audit_poetry_files(working_directory=Path()) assert e.value.stderr == result.stderr