diff --git a/.github/scripts/check-pinned-deps.py b/.github/scripts/check-pinned-deps.py new file mode 100644 index 0000000..6d71f99 --- /dev/null +++ b/.github/scripts/check-pinned-deps.py @@ -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,=, <) 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()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47911a2..d73322d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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" @@ -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: @@ -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 diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index f5fb770..7176a2a 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -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: | diff --git a/pyproject.toml b/pyproject.toml index 37d7788..d985bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -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 @@ -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 = [ @@ -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]] @@ -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]