Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/scripts/check-pinned-deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Check that all dependencies in pyproject.toml are pinned to exact versions.

Runtime dependencies (under [project].dependencies) are allowed to use bounded
ranges (>=X,<Y) since this is a library. All other dependencies must use exact
pins (==X.Y.Z).
"""
from __future__ import annotations

import re
import sys
from pathlib import Path

try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib # type: ignore[no-redefine]

# Sections where bounded ranges (>=, <) are acceptable (library runtime deps)
RUNTIME_ALLOWLIST_SECTIONS = frozenset({"project.dependencies"})

# Regex that matches a dependency pinned to an exact version with ==
EXACT_PIN_RE = re.compile(r"==\S+")

# Regex that detects unpinned or loosely-pinned version specifiers
LOOSE_SPECIFIERS_RE = re.compile(r"(>=|<=|~=|!=|>\s|<\s|\.\*)")


def _parse_deps(data: dict, path: str = "") -> list[tuple[str, str]]:
"""Walk the TOML structure and collect (section_path, dep_string) pairs."""
results: list[tuple[str, str]] = []

# [build-system].requires
if "build-system" in data:
for dep in data["build-system"].get("requires", []):
results.append(("build-system.requires", dep))

# [project].dependencies
if "project" in data:
for dep in data["project"].get("dependencies", []):
results.append(("project.dependencies", dep))

# [tool.hatch.envs.*].dependencies and extra-dependencies
hatch_envs = data.get("tool", {}).get("hatch", {}).get("envs", {})
for env_name, env_cfg in hatch_envs.items():
if isinstance(env_cfg, dict):
for dep in env_cfg.get("dependencies", []):
results.append((f"tool.hatch.envs.{env_name}.dependencies", dep))
for dep in env_cfg.get("extra-dependencies", []):
results.append((f"tool.hatch.envs.{env_name}.extra-dependencies", dep))

return results


def _check_dep(section: str, dep: str) -> str | None:
"""Return an error message if the dependency is not properly pinned."""
# Runtime deps are allowed to use bounded ranges
if section in RUNTIME_ALLOWLIST_SECTIONS:
# They must still have *some* version constraint
if not re.search(r"[><=~!]", dep):
return f" {section}: {dep!r} has no version constraint"
return None

# All other deps must use exact pins
if EXACT_PIN_RE.search(dep):
return None

return f" {section}: {dep!r} is not pinned to an exact version (use ==X.Y.Z)"


def main() -> int:
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
if not pyproject_path.exists():
print(f"ERROR: {pyproject_path} not found")
return 1

with open(pyproject_path, "rb") as f:
data = tomllib.load(f)

deps = _parse_deps(data)
errors: list[str] = []

for section, dep in deps:
err = _check_dep(section, dep)
if err:
errors.append(err)

if errors:
print("Unpinned dependencies found:")
for err in errors:
print(err)
return 1

print(f"All {len(deps)} dependencies are properly pinned.")
return 0


if __name__ == "__main__":
sys.exit(main())
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ jobs:
python-version: "3.13"

- name: Install Hatch
run: pip install "click<=8.2.1" "hatch==1.14.1"
run: pip install "hatch==1.16.5"

- name: Check pinned dependencies
run: python .github/scripts/check-pinned-deps.py

- name: Run QA checks
run: hatch run qa:qa

Expand All @@ -37,9 +40,9 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
pytest-version: ["7.2.0", "8.1.2", "8.4.*"]
pytest-version: ["7.2.0", "8.1.2", "8.4.1"]
exclude:
# Python 3.12 and 3.13 only support pytest 8.1.2 and 8.4.*
# Python 3.12 and 3.13 only support pytest 8.1.2 and 8.4.1
- python-version: "3.12"
pytest-version: "7.2.0"
- python-version: "3.13"
Expand All @@ -54,7 +57,12 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install Hatch
run: pip install "click<=8.2.1" "hatch==1.14.1"
run: |
if [ "${{ matrix.python-version }}" = "3.9" ]; then
pip install "hatch==1.15.1" "virtualenv==20.29.3"
else
pip install "hatch==1.16.5"
fi

- name: Run tests
env:
Expand All @@ -78,7 +86,7 @@ jobs:
python-version: "3.13"

- name: Install Hatch
run: pip install "click<=8.2.1" "hatch==1.14.1"
run: pip install "hatch==1.16.5"

- name: Run coverage tests
id: coverage
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build wheel setuptools
python -m pip install "build==1.4.2" "wheel==0.46.3" "setuptools==82.0.1"

- name: Build release distributions
run: |
Expand Down
40 changes: 20 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[build-system]
requires = ["hatchling", "versioningit"]
requires = ["hatchling==1.27.0", "versioningit==3.3.0"]
build-backend = "hatchling.build"

[project]
name = "ddtestpy"
dynamic = ["version"]
dependencies = [
"bytecode>=0.15.0",
"msgpack>=1.0.0",
"bytecode>=0.15.0,<1.0",
"msgpack>=1.0.0,<2.0",
]
license = "Apache-2.0"

Expand All @@ -26,15 +26,15 @@ default-tag = "0.0.0"

[tool.hatch.envs.hatch-test]
dependencies = [
"slipcover",
"pytest-socket",
"pytest-randomly~=3.15",
"pytest-rerunfailures~=14.0",
"pytest-xdist[psutil]~=3.5",
"slipcover==1.0.18",
"pytest-socket==0.7.0",
"pytest-randomly==3.15.0",
"pytest-rerunfailures==14.0",
"pytest-xdist[psutil]==3.5.0",
]
extra-dependencies = [
"pytest-memray",
"ddtrace",
"pytest-memray==1.8.0",
"ddtrace==4.6.5",
]
randomize = true
parallel = true
Expand All @@ -43,11 +43,11 @@ retry-delay = 1

[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.13", "3.12"]
pytest = ["8.1.2", "8.4.*"]
pytest = ["8.1.2", "8.4.1"]

[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.9", "3.10", "3.11"]
pytest = ["7.2.0", "8.1.2", "8.4.*"]
pytest = ["7.2.0", "8.1.2", "8.4.1"]

[tool.hatch.envs.hatch-test.overrides]
matrix.pytest.dependencies = [
Expand All @@ -63,9 +63,9 @@ cov-report = "true"

[tool.hatch.envs.int_test]
dependencies = [
"pytest",
"pytest-socket",
"flask"
"pytest==8.4.1",
"pytest-socket==0.7.0",
"flask==3.1.3",
]

[[tool.hatch.envs.int_test.matrix]]
Expand All @@ -82,11 +82,11 @@ test = "pytest {args:test_fixtures}"
[tool.hatch.envs.qa]
python = "3.13"
dependencies = [
"black",
"ruff",
"mypy",
"pytest-mypy-plugins",
"ddtrace",
"black==25.12.0",
"ruff==0.15.9",
"mypy==1.20.0",
"pytest-mypy-plugins==4.0.0",
"ddtrace==4.6.5",
]

[tool.hatch.envs.qa.scripts]
Expand Down
Loading