From f2cd2c2faedbcb6eeea632dfa18835303113bcb1 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Thu, 20 Aug 2026 08:13:07 +0200 Subject: [PATCH 1/2] fix(cli): report actual emissions file path The monitor report resolved the output path from `output_file` alone, which is only the basename, so `os.path.abspath` resolved it against the CWD and ignored `output_dir`. Users were told the file was somewhere it was not. Ask the output handlers for their `save_file_path` instead, which also prints nothing when CSV output is disabled. Keeps the `typing_extensions.Annotated` import the CLI needs on older Pythons. Closes #1322 Co-Authored-By: Claude Opus 5 (1M context) --- codecarbon/cli/monitor.py | 13 ++++++------- tests/cli/test_monitor.py | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/codecarbon/cli/monitor.py b/codecarbon/cli/monitor.py index dbecf7855..3d6ea779d 100644 --- a/codecarbon/cli/monitor.py +++ b/codecarbon/cli/monitor.py @@ -120,13 +120,12 @@ def run_and_monitor( else: print(" Emissions: N/A") - # Show where the data was saved - if hasattr(tracker, "_conf") and "output_file" in tracker._conf: - output_path = tracker._conf["output_file"] - # Make it absolute if it's relative - if not os.path.isabs(output_path): - output_path = os.path.abspath(output_path) - print(f" Saved to: {output_path}") + # Show where the data was saved, asking the output handlers themselves + # so the path stays right whatever `output_dir` / `output_file` are. + for handler in getattr(tracker, "_output_handlers", []): + save_file_path = getattr(handler, "save_file_path", None) + if save_file_path: + print(f" Saved to: {os.path.abspath(save_file_path)}") print(" ⚠️ Note: Tracked the command process and its children") print("=" * 60) diff --git a/tests/cli/test_monitor.py b/tests/cli/test_monitor.py index f3fa4852c..ab35b9fd2 100644 --- a/tests/cli/test_monitor.py +++ b/tests/cli/test_monitor.py @@ -1,3 +1,4 @@ +import os from types import SimpleNamespace import pytest @@ -11,6 +12,7 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs self.stopped = 0 self._conf = {"output_file": "emissions.csv"} + self._output_handlers = [SimpleNamespace(save_file_path="emissions.csv")] def start(self): return None @@ -226,3 +228,41 @@ def kill(self): assert exc_info.value.exit_code == 130 assert process_info["terminated"] == 1 assert process_info["killed"] == 1 + + +def _run_and_capture(monkeypatch, capsys, handlers): + class FakePopen: + def __init__(self, command, text=True): + pass + + def wait(self): + return 0 + + class FakeTrackerWithHandlers(FakeTracker): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._output_handlers = handlers + + _patch_trackers(monkeypatch, online_cls=FakeTrackerWithHandlers) + monkeypatch.setattr(monitor_module.subprocess, "Popen", FakePopen) + + with pytest.raises(typer.Exit): + monitor_module.run_and_monitor(SimpleNamespace(args=["echo", "hi"])) + + # rich wraps long lines to the terminal width, so strip all whitespace + return "".join(capsys.readouterr().out.split()) + + +def test_run_and_monitor_reports_output_dir(monkeypatch, capsys, tmp_path): + save_file_path = os.path.join(str(tmp_path), "emissions.csv") + out = _run_and_capture( + monkeypatch, capsys, [SimpleNamespace(save_file_path=save_file_path)] + ) + + assert f"Savedto:{save_file_path}" in out + + +def test_run_and_monitor_reports_no_path_without_file_output(monkeypatch, capsys): + out = _run_and_capture(monkeypatch, capsys, [SimpleNamespace()]) + + assert "Savedto:" not in out From 586dc637b6ad2183a593d76815c70eda4aa7905d Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 23 Sep 2026 16:47:55 +0900 Subject: [PATCH 2/2] fix: report BoAmps output path in CLI summary, test relative path BoAmpsOutput now exposes save_file_path once a report is written, so the monitor summary lists it. Add the relative-path test the PR description mentions. Co-Authored-By: Claude Opus 5.5 (1M context) --- codecarbon/output_methods/boamps/output.py | 3 +++ tests/cli/test_monitor.py | 9 +++++++++ tests/test_boamps_output.py | 1 + 3 files changed, 13 insertions(+) diff --git a/codecarbon/output_methods/boamps/output.py b/codecarbon/output_methods/boamps/output.py index ff172e9fa..61eac48f3 100644 --- a/codecarbon/output_methods/boamps/output.py +++ b/codecarbon/output_methods/boamps/output.py @@ -64,6 +64,8 @@ def __init__( self._quality = quality self._infra_overrides = infra_overrides self._environment_overrides = environment_overrides + # Path of the last report written, None until out() succeeds. + self.save_file_path: Optional[str] = None @classmethod def from_file(cls, context_file_path: str, output_dir: str = ".") -> "BoAmpsOutput": @@ -182,6 +184,7 @@ def out(self, total: EmissionsData, delta: EmissionsData): file_path = os.path.join(self._output_dir, file_name) with open(file_path, "w") as f: json.dump(report_dict, f, indent=2) + self.save_file_path = file_path logger.info(f"BoAmps report saved to {os.path.abspath(file_path)}") except Exception as e: logger.error(f"Failed to write BoAmps report: {e}", exc_info=True) diff --git a/tests/cli/test_monitor.py b/tests/cli/test_monitor.py index ab35b9fd2..17ddf21aa 100644 --- a/tests/cli/test_monitor.py +++ b/tests/cli/test_monitor.py @@ -262,6 +262,15 @@ def test_run_and_monitor_reports_output_dir(monkeypatch, capsys, tmp_path): assert f"Savedto:{save_file_path}" in out +def test_run_and_monitor_reports_relative_path_from_cwd(monkeypatch, capsys, tmp_path): + monkeypatch.chdir(tmp_path) + out = _run_and_capture( + monkeypatch, capsys, [SimpleNamespace(save_file_path="emissions.csv")] + ) + + assert f"Savedto:{os.path.join(os.getcwd(), 'emissions.csv')}" in out + + def test_run_and_monitor_reports_no_path_without_file_output(monkeypatch, capsys): out = _run_and_capture(monkeypatch, capsys, [SimpleNamespace()]) diff --git a/tests/test_boamps_output.py b/tests/test_boamps_output.py index 06e46c13f..bf938f41e 100644 --- a/tests/test_boamps_output.py +++ b/tests/test_boamps_output.py @@ -539,6 +539,7 @@ def test_out_writes_json_file(self): self.tmpdir, f"boamps_report_{self.emissions.run_id}.json" ) self.assertTrue(os.path.isfile(expected_file)) + self.assertEqual(handler.save_file_path, expected_file) with open(expected_file) as f: report = json.load(f)