diff --git a/.github/workflows/package-validation-reusable.yml b/.github/workflows/package-validation-reusable.yml index 175cc20cc..14f05af90 100644 --- a/.github/workflows/package-validation-reusable.yml +++ b/.github/workflows/package-validation-reusable.yml @@ -8,6 +8,8 @@ permissions: env: MAIN_PYTHON_VERSION: 3.12 + # Lowest interpreter allowed by requires-python in pyproject.toml. + MIN_PYTHON_VERSION: "3.10" jobs: build-package: @@ -69,3 +71,47 @@ jobs: run: | .venv/bin/codecarbon --help .venv/bin/python -c "from codecarbon import EmissionsTracker; print('Package import successful')" + + # Proves `pip install codecarbon` works with only the declared runtime + # dependencies (no dev group, no extras), on the oldest supported + # interpreter. The job above installs the dev group first, so it cannot + # catch a runtime dependency that is declared nowhere but the lockfile. + bare-install-smoke: + runs-on: ubuntu-24.04 + needs: [build-package] + steps: + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + version: "latest" + - name: Download built package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pypi_dist + path: dist + - name: Install wheel into a bare virtualenv + run: | + uv venv --python ${{ env.MIN_PYTHON_VERSION }} .venv + uv pip install --python .venv/bin/python dist/*.whl + - name: Smoke test + env: + CODECARBON_ALLOW_MULTIPLE_RUNS: "True" + run: | + .venv/bin/python -c "import codecarbon; print(codecarbon.__version__)" + .venv/bin/codecarbon --help + .venv/bin/python -c " + import time + from codecarbon import OfflineEmissionsTracker + tracker = OfflineEmissionsTracker(country_iso_code='FRA', measure_power_secs=1) + tracker.start() + time.sleep(2) + emissions = tracker.stop() + print('emissions:', emissions) + # stop() returns None on every internal give-up path (tracker never + # started, another instance holding the lock, engine init failed), so + # a bare call would print None and still exit 0. Assert the shape, not + # the magnitude: a runner with no RAPL falls back to constant CPU + # power and 0.0 over 2s is a legitimate result. + assert isinstance(emissions, float), f'tracker.stop() returned {emissions!r}' + assert emissions >= 0.0, emissions + " diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 8167dc201..354cdd562 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -6,11 +6,15 @@ on: paths: - "codecarbon/**" - "pyproject.toml" + - ".github/workflows/package.yml" + - ".github/workflows/package-validation-reusable.yml" branches: [master] push: paths: - "codecarbon/**" - "pyproject.toml" + - ".github/workflows/package.yml" + - ".github/workflows/package-validation-reusable.yml" branches: [master] jobs: validate-package: diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 0d3717e86..9d793ac71 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -20,8 +20,11 @@ jobs: CODECARBON_API_KEY: ${{ secrets.CODECARBON_API_TOKEN }} CODECARBON_EXPERIMENT_ID: ${{ secrets.CODECARBON_EXPERIMENT_ID_TEST_PACKAGE }} strategy: + fail-fast: false matrix: - python-version: ["3.12", "3.13", "3.14"] + # Must cover every version allowed by requires-python / classifiers + # in pyproject.toml. + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv diff --git a/tests/test_gpu_amd.py b/tests/test_gpu_amd.py index e066ee657..2d5fcdd38 100644 --- a/tests/test_gpu_amd.py +++ b/tests/test_gpu_amd.py @@ -429,6 +429,15 @@ def _patched_import(name, *args, **kwargs): sys.modules.pop("amdsmi", None) sys.modules.pop("codecarbon.core.gpu_amd", None) sys.modules.update(saved) + # importlib also rebinds the submodule attribute on the parent + # package, and restoring sys.modules alone does not undo that. + # On Python < 3.12 mock.patch resolves "codecarbon.core.gpu_amd.x" + # through that attribute, so a stale binding would send later + # patches to the throwaway module. Put the real one back. + if "codecarbon.core.gpu_amd" in saved: + import codecarbon.core + + codecarbon.core.gpu_amd = saved["codecarbon.core.gpu_amd"] def test_oserror_sets_amdsmi_unavailable(self): """OSError during amdsmi import (e.g. missing libamd_smi.so) must disable AMD GPU support."""