diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 93f627e5b..ef0501cc1 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -390,15 +390,63 @@ def monitor( str, typer.Option(help="Log level (critical, error, warning, info, debug)"), ] = "error", + project_name: Annotated[ + str, + typer.Option(help="Project name for the current experiment run."), + ] = None, + output_dir: Annotated[ + str, + typer.Option(help="Directory to write emissions.csv to."), + ] = None, + pue: Annotated[ + float, + typer.Option(help="Power Usage Effectiveness of the datacenter."), + ] = None, + wue: Annotated[ + float, + typer.Option(help="Water Usage Effectiveness of the datacenter."), + ] = None, + gpu_ids: Annotated[ + str, + typer.Option(help="Comma-separated list of GPU ids to track, e.g. '0,1'."), + ] = None, + force_cpu_power: Annotated[ + int, + typer.Option(help="Force CPU power draw in Watts instead of estimating it."), + ] = None, + force_ram_power: Annotated[ + int, + typer.Option(help="Force RAM power draw in Watts instead of estimating it."), + ] = None, + allow_multiple_runs: Annotated[ + bool, + typer.Option( + help="Allow multiple codecarbon trackers to run at the same time on this machine." + ), + ] = None, ): """Monitor your machine's carbon emissions.""" - # Shared tracker args so monitor and run_and_monitor behave the same + # Shared tracker args so monitor and run_and_monitor behave the same. + # Options left at their default (None) are omitted so EmissionsTracker + # can still fall back to its own config-file / environment-variable + # defaults instead of having them silently overridden by None here. tracker_args = { "measure_power_secs": measure_power_secs, "api_call_interval": api_call_interval, "log_level": log_level, } + optional_args = { + "project_name": project_name, + "output_dir": output_dir, + "pue": pue, + "wue": wue, + "gpu_ids": [g.strip() for g in gpu_ids.split(",")] if gpu_ids else None, + "force_cpu_power": force_cpu_power, + "force_ram_power": force_ram_power, + "allow_multiple_runs": allow_multiple_runs, + } + tracker_args.update({k: v for k, v in optional_args.items() if v is not None}) # Set up the tracker arguments based on mode (offline vs online) and validate required args for each mode if offline: if not country_iso_code: diff --git a/tests/cli/test_cli_main.py b/tests/cli/test_cli_main.py index 8bb4d66f4..5fe68bfe3 100644 --- a/tests/cli/test_cli_main.py +++ b/tests/cli/test_cli_main.py @@ -487,3 +487,124 @@ def test_monitor_online_requires_experiment_id_for_wrapped_command(monkeypatch): with pytest.raises(typer.Exit) as exc_info: cli_main.monitor(ctx=ctx, offline=False, api=True) assert exc_info.value.exit_code == 1 + + +def test_monitor_help_lists_new_tracker_options(): + """`codecarbon monitor --help` should expose the previously-missing + EmissionsTracker options (see #1273).""" + runner = CliRunner() + result = runner.invoke( + cli_main.codecarbon, ["monitor", "--help"], env={"COLUMNS": "200"} + ) + assert result.exit_code == 0 + for flag in ( + "--project-name", + "--output-dir", + "--pue", + "--wue", + "--gpu-ids", + "--force-cpu-power", + "--force-ram-power", + "--allow-multiple-runs", + ): + assert flag in result.output + + +def test_monitor_offline_forwards_new_options_to_tracker(monkeypatch): + """Explicitly-provided options should reach OfflineEmissionsTracker's kwargs, + with gpu-ids parsed from a comma-separated string into a list.""" + calls = {"kwargs": None} + + class FakeOfflineTracker: + def __init__(self, **kwargs): + calls["kwargs"] = kwargs + self._another_instance_already_running = True + + def start(self): + pass + + def stop(self): + return None + + monkeypatch.setattr( + "codecarbon.emissions_tracker.OfflineEmissionsTracker", FakeOfflineTracker + ) + monkeypatch.setattr(cli_main.signal, "signal", lambda *args, **kwargs: None) + + runner = CliRunner() + result = runner.invoke( + cli_main.codecarbon, + [ + "monitor", + "--offline", + "--country-iso-code", + "FRA", + "--project-name", + "my-project", + "--output-dir", + "/tmp/emissions", + "--pue", + "1.2", + "--wue", + "0.8", + "--gpu-ids", + "0,1", + "--force-cpu-power", + "45", + "--force-ram-power", + "5", + "--allow-multiple-runs", + ], + ) + assert result.exit_code == 0 + kwargs = calls["kwargs"] + assert kwargs["project_name"] == "my-project" + assert kwargs["output_dir"] == "/tmp/emissions" + assert kwargs["pue"] == 1.2 + assert kwargs["wue"] == 0.8 + assert kwargs["gpu_ids"] == ["0", "1"] + assert kwargs["force_cpu_power"] == 45 + assert kwargs["force_ram_power"] == 5 + assert kwargs["allow_multiple_runs"] is True + + +def test_monitor_offline_omits_unset_optional_tracker_args(monkeypatch): + """Options left at their CLI default (None) must be omitted from tracker_args + entirely, so EmissionsTracker still falls back to its own config/env + defaults instead of having them overridden with None.""" + calls = {"kwargs": None} + + class FakeOfflineTracker: + def __init__(self, **kwargs): + calls["kwargs"] = kwargs + self._another_instance_already_running = True + + def start(self): + pass + + def stop(self): + return None + + monkeypatch.setattr( + "codecarbon.emissions_tracker.OfflineEmissionsTracker", FakeOfflineTracker + ) + monkeypatch.setattr(cli_main.signal, "signal", lambda *args, **kwargs: None) + + runner = CliRunner() + result = runner.invoke( + cli_main.codecarbon, + ["monitor", "--offline", "--country-iso-code", "FRA"], + ) + assert result.exit_code == 0 + kwargs = calls["kwargs"] + for unset_option in ( + "project_name", + "output_dir", + "pue", + "wue", + "gpu_ids", + "force_cpu_power", + "force_ram_power", + "allow_multiple_runs", + ): + assert unset_option not in kwargs