From cbf6e59112dbdb6e76fce76e0f0c4bcc981041b3 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Thu, 20 Aug 2026 08:13:07 +0200 Subject: [PATCH] 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 41b3ca353..386d948f9 100644 --- a/codecarbon/cli/monitor.py +++ b/codecarbon/cli/monitor.py @@ -116,13 +116,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 0a9bda365..80fc9e1c6 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 @@ -178,3 +180,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