From c24ab48704a7e43ed7e208046970fda1f100f5b7 Mon Sep 17 00:00:00 2001 From: Sven Geboers Date: Tue, 24 Mar 2026 22:41:36 +0100 Subject: [PATCH] chore(deps): move pytest to dev-dependencies --- pyproject.toml | 2 +- requirements-dev.txt | 1 + tests/config/test_pyproject_deps.py | 101 ++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 requirements-dev.txt create mode 100644 tests/config/test_pyproject_deps.py diff --git a/pyproject.toml b/pyproject.toml index 604e001..53ab241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "scipy>=1.11", "umap-learn>=0.5", "plotly>=5.0", - "pytest>=9.0.2", + # moved pytest to development requirements "requests>=2.32.4", "schedule>=1.2.2", "streamlit>=1.48.0", diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..16c4879 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pytest>=9.0.2 diff --git a/tests/config/test_pyproject_deps.py b/tests/config/test_pyproject_deps.py new file mode 100644 index 0000000..c3ef39a --- /dev/null +++ b/tests/config/test_pyproject_deps.py @@ -0,0 +1,101 @@ +from pathlib import Path +import re + + +def _extract_project_dependencies(text: str): + """Return the dependencies block text from a [project] section, or None.""" + lines = text.splitlines() + cur = None + deps_block = [] + in_dependencies = False + for line in lines: + s = line.strip() + if s.startswith("[") and s.endswith("]"): + cur = s + in_dependencies = False + continue + if cur == "[project]": + if ( + s.startswith("dependencies") + and "=[" in s + or s.startswith("dependencies") + and s.endswith("=[") + ): + # start of block on same line or next + # capture from the first '[' + idx = line.find("[") + if idx != -1 and line.rstrip().endswith("]"): + # single-line + deps_block.append(line[idx:]) + in_dependencies = False + else: + deps_block.append(line[idx:]) + in_dependencies = True + continue + if in_dependencies: + deps_block.append(line) + if "]" in line: + in_dependencies = False + if deps_block: + return "\n".join(deps_block) + return None + + +def _extract_poetry_dependencies(text: str): + """Return the lines under [tool.poetry.dependencies] as a single string, or None.""" + lines = text.splitlines() + cur = None + collected = [] + for line in lines: + s = line.strip() + if s.startswith("[") and s.endswith("]"): + cur = s + continue + if cur == "[tool.poetry.dependencies]": + if s == "" or s.startswith("#"): + continue + collected.append(s) + if collected: + return "\n".join(collected) + return None + + +def test_pytest_not_in_production_deps(): + p = Path("pyproject.toml") + assert p.exists(), "pyproject.toml must exist for this project" + text = p.read_text() + + # Check PEP 621 [project] dependencies = [ ... ] + proj_deps = _extract_project_dependencies(text) + if proj_deps: + assert "pytest" not in proj_deps.lower(), ( + "pytest must not be listed in [project] production dependencies" + ) + + # Check poetry style [tool.poetry.dependencies] + poetry_deps = _extract_poetry_dependencies(text) + if poetry_deps: + # lines like pytest = "^..." or pytest = "..." + for line in poetry_deps.splitlines(): + # split on = and check key + key = line.split("=", 1)[0].strip() + assert key.lower() != "pytest", ( + "pytest must not be listed in [tool.poetry.dependencies]" + ) + + +def test_requirements_dev_contains_pytest_if_present(): + # If pytest was removed from production deps, ensure it's present in requirements-dev.txt + dev = Path("requirements-dev.txt") + if dev.exists(): + text = dev.read_text().lower() + assert "pytest" in text, ( + "requirements-dev.txt exists but does not contain pytest" + ) + else: + # If requirements-dev.txt does not exist, that's acceptable as long as pytest isn't in prod deps + p = Path("pyproject.toml") + text = p.read_text() + assert "pytest" not in text.lower(), ( + "pytest found in pyproject.toml and requirements-dev.txt is missing" + )