Compare commits

..
7 Commits
7 changed files with 79 additions and 15 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
PYTHONPATH: src PYTHONPATH: src
run: | run: |
. .venv/bin/activate . .venv/bin/activate
python3 -m devx.tools.check_test_speed --max-seconds 60 --max-single-seconds 2.0 python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
- name: Documentation coverage check - name: Documentation coverage check
env: env:
PYTHONPATH: src PYTHONPATH: src
+1 -1
View File
@@ -1 +1 @@
DEVX-15 DEVX-19
+19
View File
@@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.8.5] - 2026-06-23
### Bug Fixes
- Retry pip install with --ignore-installed only on failure
## [0.8.4] - 2026-06-23
### Bug Fixes
- Add --ignore-installed to pip in CI to bypass debian packages
## [0.8.3] - 2026-06-23
### Bug Fixes
- Lower check_test_speed threshold to 4 seconds
- Pass --break-system-packages to pip in CI environments
## [0.8.2] - 2026-06-23 ## [0.8.2] - 2026-06-23
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -4,4 +4,4 @@
# Aligned with CI (ci.yml uses same thresholds). # Aligned with CI (ci.yml uses same thresholds).
set -e set -e
export PYTHONPATH=src export PYTHONPATH=src
python3 -m devx.tools.check_test_speed --max-seconds 10 --max-single-seconds 0.5 python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects.""" """devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.8.2" __version__ = "0.8.5"
+18 -3
View File
@@ -26,9 +26,24 @@ def _run(cmd: list[str]) -> None:
def _install_python_deps(bin_dir: str, extras: str = "dev") -> None: def _install_python_deps(bin_dir: str, extras: str = "dev") -> None:
"""Install the project with the specified extras in editable mode.""" """Install the project with the specified extras in editable mode.
In CI (system Python with PIP_BREAK_SYSTEM_PACKAGES=1), a first attempt
uses --break-system-packages. If that fails (e.g. debian-installed
packages without RECORD files), retry with --ignore-installed to skip
uninstalling system packages entirely.
"""
pip = str(Path(bin_dir) / "pip") pip = str(Path(bin_dir) / "pip")
_run([pip, "install", "-e", f".[{extras}]"]) cmd = [pip, "install", "-e", f".[{extras}]"]
if os.environ.get("PIP_BREAK_SYSTEM_PACKAGES") == "1":
cmd.append("--break-system-packages")
result = subprocess.run(cmd, check=False) # nosec B603
if result.returncode != 0 and os.environ.get("PIP_BREAK_SYSTEM_PACKAGES") == "1":
click.echo(" Retrying with --ignore-installed to bypass system packages...")
cmd.append("--ignore-installed")
_run(cmd)
elif result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, cmd)
def _install_pre_commit_hooks(bin_dir: str) -> None: def _install_pre_commit_hooks(bin_dir: str) -> None:
@@ -40,7 +55,7 @@ def _install_pre_commit_hooks(bin_dir: str) -> None:
def _install_ansible_collections(bin_dir: str) -> None: def _install_ansible_collections(bin_dir: str) -> None:
"""Install required Ansible Galaxy collections if requirements exist.""" """Install required Ansible Galaxy collections if requirements exist."""
galaxy = str(Path(bin_dir) / "ansible-galaxy") galaxy = shutil.which("ansible-galaxy") or str(Path(bin_dir) / "ansible-galaxy")
requirements = Path("ansible/requirements.yml") requirements = Path("ansible/requirements.yml")
if not requirements.exists(): if not requirements.exists():
click.echo(" ansible/requirements.yml not found — skipping collections.") click.echo(" ansible/requirements.yml not found — skipping collections.")
+38 -8
View File
@@ -1,5 +1,6 @@
"""Unit tests for devx.tools.setup.""" """Unit tests for devx.tools.setup."""
import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -32,20 +33,48 @@ class TestRun:
class TestInstallPythonDeps: class TestInstallPythonDeps:
@patch("devx.tools.setup._run") @patch("devx.tools.setup.subprocess.run")
def test_install_dev(self, mock_run: MagicMock) -> None: def test_install_dev(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
_install_python_deps(".venv/bin", "dev") _install_python_deps(".venv/bin", "dev")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"]) mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"], check=False)
@patch("devx.tools.setup._run") @patch("devx.tools.setup.subprocess.run")
def test_install_ci(self, mock_run: MagicMock) -> None: def test_install_ci(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
_install_python_deps(".venv/bin", "ci") _install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"]) mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"], check=False)
@patch("devx.tools.setup.subprocess.run")
def test_install_custom_extras(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
_install_python_deps(".venv/bin", "ci,lint")
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"], check=False)
@patch("devx.tools.setup.subprocess.run")
def test_install_with_break_system_packages(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
with patch.dict(os.environ, {"PIP_BREAK_SYSTEM_PACKAGES": "1"}):
_install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with(
[".venv/bin/pip", "install", "-e", ".[ci]", "--break-system-packages"], check=False
)
@patch("devx.tools.setup._run") @patch("devx.tools.setup._run")
def test_install_custom_extras(self, mock_run: MagicMock) -> None: @patch("devx.tools.setup.subprocess.run")
_install_python_deps(".venv/bin", "ci,lint") def test_install_retry_with_ignore_installed(self, mock_subprocess: MagicMock, mock_run: MagicMock) -> None:
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"]) mock_subprocess.return_value = MagicMock(returncode=1)
with patch.dict(os.environ, {"PIP_BREAK_SYSTEM_PACKAGES": "1"}):
_install_python_deps(".venv/bin", "ci")
mock_run.assert_called_once_with(
[".venv/bin/pip", "install", "-e", ".[ci]", "--break-system-packages", "--ignore-installed"]
)
@patch("devx.tools.setup.subprocess.run")
def test_install_failure_without_break_system(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=1)
with pytest.raises(subprocess.CalledProcessError):
_install_python_deps(".venv/bin", "ci")
class TestInstallPreCommitHooks: class TestInstallPreCommitHooks:
@@ -60,8 +89,9 @@ class TestInstallPreCommitHooks:
class TestInstallAnsibleCollections: class TestInstallAnsibleCollections:
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/ansible-galaxy")
@patch("devx.tools.setup._run") @patch("devx.tools.setup._run")
def test_installs_from_requirements(self, mock_run: MagicMock, tmp_path: Path) -> None: def test_installs_from_requirements(self, mock_run: MagicMock, mock_which: MagicMock, tmp_path: Path) -> None:
req = tmp_path / "ansible" / "requirements.yml" req = tmp_path / "ansible" / "requirements.yml"
req.parent.mkdir(parents=True) req.parent.mkdir(parents=True)
req.write_text("collections: []") req.write_text("collections: []")