From 87b6a7a3cd5c9408f739aa4470ab1c91194f818c Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 12 Aug 2026 17:03:31 +0200 Subject: [PATCH 1/4] ci: test every Python version the package promises requires-python is >=3.10 and the classifiers list 3.10-3.14, but test-package.yml only exercised 3.12-3.14. Add 3.10 and 3.11 to the matrix with fail-fast: false so one version's failure does not hide the others. Also add a bare-install smoke job to package-validation-reusable.yml: the existing wheel job syncs the dev group before installing the wheel, so it cannot catch a runtime dependency that is missing from [project.dependencies]. The new job installs only the wheel, on the floor interpreter, and runs an OfflineEmissionsTracker plus the CLI. Co-Authored-By: Claude Opus 5 (1M context) --- .../workflows/package-validation-reusable.yml | 38 +++++++++++++++++++ .github/workflows/test-package.yml | 5 ++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package-validation-reusable.yml b/.github/workflows/package-validation-reusable.yml index 175cc20cc..e615eed96 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,39 @@ 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) + print('emissions:', tracker.stop()) + " 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 From 6e1472827afc15a990a2a3cb60b5739277853617 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 12 Aug 2026 17:31:24 +0200 Subject: [PATCH 2/4] fix(tests): restore gpu_amd package attribute after import-failure tests The amdsmi import-failure tests re-import codecarbon.core.gpu_amd with a patched __import__ and restore sys.modules afterwards, but importlib also rebinds gpu_amd on the codecarbon.core package, and that binding was left pointing at the throwaway module whose amdsmi is None. On Python < 3.12 mock.patch resolves a dotted target through getattr on the parent package, so every later patch of codecarbon.core.gpu_amd.amdsmi hit the throwaway module while the code under test still used the real one, failing with AttributeError: 'NoneType' object has no attribute 'amdsmi_init'. Python 3.12+ resolves via pkgutil.resolve_name and hid the leak. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_gpu_amd.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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.""" From 0bd33962ea7cfecd16a616c298a5c891270dccf9 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 12 Aug 2026 19:59:49 +0200 Subject: [PATCH 3/4] ci(smoke): assert the bare-install tracker actually returns a result tracker.stop() returns None on every internal give-up path, so printing it left the smoke job green on a silently broken wheel. Assert the shape rather than the magnitude, since 0.0 is legitimate on a runner without RAPL. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/package-validation-reusable.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package-validation-reusable.yml b/.github/workflows/package-validation-reusable.yml index e615eed96..14f05af90 100644 --- a/.github/workflows/package-validation-reusable.yml +++ b/.github/workflows/package-validation-reusable.yml @@ -105,5 +105,13 @@ jobs: tracker = OfflineEmissionsTracker(country_iso_code='FRA', measure_power_secs=1) tracker.start() time.sleep(2) - print('emissions:', tracker.stop()) + 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 " From 027ad658b33bb2fffc2eafeb69531b7605c041ac Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Thu, 13 Aug 2026 08:31:47 +0200 Subject: [PATCH 4/4] ci: run package validation when its own workflows change The new bare-install-smoke job lives in a workflow the `package` paths filter does not watch, so it never ran on this PR and would first execute on master. Watch the two workflow files too. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/package.yml | 4 ++++ 1 file changed, 4 insertions(+) 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: