From fff1c7971e34621dd06ea2d61093f2d174fb9c46 Mon Sep 17 00:00:00 2001 From: inimaz Date: Fri, 12 Dec 2025 16:46:40 +0100 Subject: [PATCH 1/6] feat: codecarbon run -- any_command --- codecarbon/cli/main.py | 112 ++++++++++++++++++++++++++++++++++ examples/command_line_tool.py | 10 ++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 9b3daf699..f517448dc 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -1,5 +1,6 @@ import os import signal +import subprocess import sys import time from pathlib import Path @@ -339,6 +340,117 @@ def config(): ) +@codecarbon.command( + "run", + short_help="Run a command and track its emissions.", + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, +) +def run( + ctx: typer.Context, +): + """ + Run a command and track its carbon emissions. + + This command wraps any executable and measures the machine's total power + consumption during its execution. When the command completes, a summary + report is displayed and emissions data is saved to a CSV file. + + Note: This tracks machine-level emissions (entire system), not just the + specific command. For process-specific tracking, the command must be + instrumented with the CodeCarbon Python library. + + Examples: + + # Run any shell command + codecarbon run -- ./benchmark.sh + + # Commands with arguments (use single quotes for special chars) + codecarbon run -- python -c 'print("Hello World!")' + + # Pipe the command output + codecarbon run -- npm run test > output.txt + + The emissions data is appended to emissions.csv (default) in the current + directory. The file path is shown in the final report. + """ + # Suppress all CodeCarbon logs during execution + from codecarbon.external.logger import set_logger_level + + set_logger_level("critical") + + # Get the command from remaining args + command = ctx.args + + if not command: + print( + "ERROR: No command provided. Use: codecarbon run -- ", + file=sys.stderr, + ) + raise typer.Exit(1) + + # Initialize tracker with minimal logging + tracker = EmissionsTracker(log_level="error", save_to_logger=False) + + print("🌱 CodeCarbon: Starting emissions tracking...") + print(f" Command: {' '.join(command)}") + print() + + tracker.start() + + process = None + try: + # Run the command, streaming output to console + process = subprocess.Popen( + command, + stdout=sys.stdout, + stderr=sys.stderr, + text=True, + ) + + # Wait for completion + exit_code = process.wait() + + except FileNotFoundError: + print(f"āŒ Error: Command not found: {command[0]}", file=sys.stderr) + exit_code = 127 + except KeyboardInterrupt: + print("\nāš ļø Interrupted by user", file=sys.stderr) + if process is not None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + exit_code = 130 + except Exception as e: + print(f"āŒ Error running command: {e}", file=sys.stderr) + exit_code = 1 + finally: + emissions = tracker.stop() + print() + print("=" * 60) + print("🌱 CodeCarbon Emissions Report") + print("=" * 60) + print(f" Command: {' '.join(command)}") + if emissions is not None: + print(f" Emissions: {emissions * 1000:.4f} g CO2eq") + 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}") + + print(" āš ļø Note: Measured entire machine (includes all system processes)") + print("=" * 60) + + raise typer.Exit(exit_code) + + @codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.") def monitor( measure_power_secs: Annotated[ diff --git a/examples/command_line_tool.py b/examples/command_line_tool.py index da6f6584e..5308f3dfe 100644 --- a/examples/command_line_tool.py +++ b/examples/command_line_tool.py @@ -1,8 +1,16 @@ """ This example demonstrates how to use CodeCarbon with command line tools. -Here we measure the emissions of an speech-to-text with WhisperX. +āš ļø IMPORTANT LIMITATION: +CodeCarbon tracks emissions at the MACHINE level when monitoring external commands +via subprocess. It measures total system power during the command execution, which +includes the command itself AND all other system processes. +For accurate process-level tracking, the tracking code must be embedded in the +application being measured (not possible with external binaries like WhisperX). + +This example measures emissions during WhisperX execution, but cannot isolate +WhisperX's exact contribution from other system activity. """ import subprocess From 8d2c22c8fd96621d231bc646b6a69d3549ba50f7 Mon Sep 17 00:00:00 2001 From: benoit-cty <4-benoit-cty@users.noreply.git.leximpact.dev> Date: Sun, 1 Feb 2026 22:18:58 +0100 Subject: [PATCH 2/6] Process mode --- codecarbon/cli/main.py | 29 +++++++++++++------- docs/edit/usage.rst | 62 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index f517448dc..72ba4ee87 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -347,36 +347,43 @@ def config(): ) def run( ctx: typer.Context, + log_level: Annotated[ + str, typer.Option(help="Log level (critical, error, warning, info, debug)") + ] = "error", ): """ Run a command and track its carbon emissions. - This command wraps any executable and measures the machine's total power + This command wraps any executable and measures the process's total power consumption during its execution. When the command completes, a summary report is displayed and emissions data is saved to a CSV file. - Note: This tracks machine-level emissions (entire system), not just the - specific command. For process-specific tracking, the command must be - instrumented with the CodeCarbon Python library. + Note: This tracks process-level emissions (only the specific command), not the + entire machine. For machine-level tracking, use the `monitor` command. Examples: - # Run any shell command + Do not use quotes around the command. Use -- to separate CodeCarbon args. + + # Run any shell command: codecarbon run -- ./benchmark.sh - # Commands with arguments (use single quotes for special chars) + # Commands with arguments (use single quotes for special chars): codecarbon run -- python -c 'print("Hello World!")' - # Pipe the command output + # Pipe the command output: codecarbon run -- npm run test > output.txt + # Display the CodeCarbon detailed logs: + codecarbon run --log-level debug -- python --version + The emissions data is appended to emissions.csv (default) in the current directory. The file path is shown in the final report. """ # Suppress all CodeCarbon logs during execution from codecarbon.external.logger import set_logger_level - set_logger_level("critical") + set_logger_level(log_level) # Get the command from remaining args command = ctx.args @@ -388,8 +395,10 @@ def run( ) raise typer.Exit(1) - # Initialize tracker with minimal logging - tracker = EmissionsTracker(log_level="error", save_to_logger=False) + # Initialize tracker with specified logging level + tracker = EmissionsTracker( + log_level=log_level, save_to_logger=False, tracking_mode="process" + ) print("🌱 CodeCarbon: Starting emissions tracking...") print(f" Command: {' '.join(command)}") diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index 169fa3d7d..378d17147 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -58,7 +58,67 @@ The command line could also works without internet by providing the country code codecarbon monitor --offline --country-iso-code FRA -Implementing CodeCarbon in your code allows you to track the emissions of a specific block of code. + +Running Any Command with CodeCarbon +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to track emissions while running any command or program (not just Python scripts), you can use the ``codecarbon run`` command. +This allows non-Python users to measure machine emissions during the execution of any command: + +.. code-block:: console + + codecarbon run -- + +Do not surround ```` with quotes. The double hyphen ``--`` indicates the end of CodeCarbon options and the beginning of the command to run. + +**Examples:** + +.. code-block:: console + + # Run a shell script + codecarbon run -- ./benchmark.sh + + # Run a command with arguments (use quotes for special characters) + codecarbon run -- bash -c 'echo "Processing..."; sleep 30; echo "Done!"' + + # Run Python scripts + codecarbon run -- python train_model.py + + # Run Node.js applications + codecarbon run -- node app.js + + # Run tests with output redirection + codecarbon run -- npm run test > output.txt + + # Display the CodeCarbon detailed logs + codecarbon run --log-level debug -- python --version + +**Output:** + +When the command completes, CodeCarbon displays a summary report and saves the emissions data to a CSV file: + +.. code-block:: console + + 🌱 CodeCarbon: Starting emissions tracking... + Command: bash -c echo "Processing..."; sleep 30; echo "Done!" + + Processing... + Done! + + ============================================================ + 🌱 CodeCarbon Emissions Report + ============================================================ + Command: bash -c echo "Processing..."; sleep 30; echo "Done!" + Emissions: 0.0317 g CO2eq + Saved to: /home/user/emissions.csv + āš ļø Note: Measured entire machine (includes all system processes) + ============================================================ + +.. note:: + The ``codecarbon run`` command tracks process-level emissions (only the specific command), not the + entire machine. For machine-level tracking, use the ``codecarbon monitor`` command. + +For more fine-grained tracking, implementing CodeCarbon in your code allows you to track the emissions of a specific block of code. Explicit Object ~~~~~~~~~~~~~~~ From 0e7c9620697746ece397c5296dfd8ca2f080a4bc Mon Sep 17 00:00:00 2001 From: benoit-cty <4-benoit-cty@users.noreply.git.leximpact.dev> Date: Sun, 1 Feb 2026 22:49:19 +0100 Subject: [PATCH 3/6] Monitor sub-process --- codecarbon/cli/main.py | 2 +- codecarbon/external/hardware.py | 65 +++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 72ba4ee87..69c33fd54 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -454,7 +454,7 @@ def run( output_path = os.path.abspath(output_path) print(f" Saved to: {output_path}") - print(" āš ļø Note: Measured entire machine (includes all system processes)") + print(" āš ļø Note: Tracked the command process and its children") print("=" * 60) raise typer.Exit(exit_code) diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index 30ce125ee..aaa548617 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -4,6 +4,7 @@ import math import re +import time from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Dict, Iterable, List, Optional, Tuple @@ -182,6 +183,9 @@ def __init__( self._pid = psutil.Process().pid self._cpu_count = count_cpus() self._process = psutil.Process(self._pid) + # For process tracking: store last measurement time and CPU times + self._last_measurement_time: Optional[float] = None + self._last_cpu_times: Dict[int, float] = {} # pid -> total cpu time if self._mode == "intel_power_gadget": self._intel_interface = IntelPowerGadget(self._output_dir) @@ -245,11 +249,62 @@ def _get_power_from_cpu_load(self): f"CPU load {self._tdp} W and {cpu_load:.1f}% {load_factor=} => estimation of {power} W for whole machine." ) elif self._tracking_mode == "process": + # Use CPU times for accurate process tracking + current_time = time.time() + current_cpu_times: Dict[int, float] = {} + + # Get CPU time for main process and all children + try: + processes = [self._process] + self._process.children(recursive=True) + except (psutil.NoSuchProcess, psutil.AccessDenied): + processes = [self._process] + + for proc in processes: + try: + cpu_times = proc.cpu_times() + # Total CPU time = user + system time + total_cpu_time = cpu_times.user + cpu_times.system + current_cpu_times[proc.pid] = total_cpu_time + except (psutil.NoSuchProcess, psutil.AccessDenied): + logger.debug( + f"Process {proc.pid} disappeared or access denied when getting CPU times." + ) + + # Calculate CPU usage based on delta + if self._last_measurement_time is not None: + time_delta = current_time - self._last_measurement_time + if time_delta > 0: + total_cpu_delta = 0.0 + for pid, cpu_time in current_cpu_times.items(): + last_cpu_time = self._last_cpu_times.get(pid, cpu_time) + cpu_delta = cpu_time - last_cpu_time + if cpu_delta > 0: + total_cpu_delta += cpu_delta + logger.debug( + f"Process {pid} CPU time delta: {cpu_delta:.3f}s" + ) + + # CPU load as percentage (can be > 100% with multiple cores) + # total_cpu_delta is the CPU time used, time_delta is wall clock time + cpu_load = (total_cpu_delta / time_delta) * 100 + logger.debug( + f"Total CPU delta: {total_cpu_delta:.3f}s over {time_delta:.3f}s = {cpu_load:.1f}% (across {self._cpu_count} cores)" + ) + else: + cpu_load = 0.0 + else: + cpu_load = 0.0 + logger.debug("First measurement, no CPU delta available yet") - cpu_load = self._process.cpu_percent(interval=0.5) / self._cpu_count - power = self._tdp * cpu_load / 100 + # Store for next measurement + self._last_measurement_time = current_time + self._last_cpu_times = current_cpu_times + + # Normalize to percentage of total CPU capacity + cpu_load_normalized = cpu_load / self._cpu_count + power = self._tdp * cpu_load_normalized / 100 logger.debug( - f"CPU load {self._tdp} W and {cpu_load * 100:.1f}% => estimation of {power} W for process {self._pid}." + f"CPU load {self._tdp} W and {cpu_load:.1f}% ({cpu_load_normalized:.1f}% normalized) => estimation of {power:.2f} W for process {self._pid} and {len(current_cpu_times) - 1} children." ) else: raise Exception(f"Unknown tracking_mode {self._tracking_mode}") @@ -318,9 +373,13 @@ def measure_power_and_energy(self, last_duration: float) -> Tuple[Power, Energy] def start(self): if self._mode in ["intel_power_gadget", "intel_rapl", "apple_powermetrics"]: self._intel_interface.start() + # Reset process tracking state for fresh measurements + self._last_measurement_time = None + self._last_cpu_times = {} if self._mode == MODE_CPU_LOAD: # The first time this is called it will return a meaningless 0.0 value which you are supposed to ignore. _ = self._get_power_from_cpu_load() + _ = self._get_power_from_cpu_load() def monitor_power(self): cpu_power = self._get_power_from_cpus() From f9ccbfa5b38a52f3898fc5b03aeeb0efac2bd0b7 Mon Sep 17 00:00:00 2001 From: inimaz Date: Fri, 13 Feb 2026 18:27:53 +0100 Subject: [PATCH 4/6] fix: rename to codecarbon monitor --- codecarbon/cli/main.py | 160 +++++++------------------------------- codecarbon/cli/monitor.py | 130 +++++++++++++++++++++++++++++++ docs/edit/usage.rst | 18 ++--- 3 files changed, 168 insertions(+), 140 deletions(-) create mode 100644 codecarbon/cli/monitor.py diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 34719041f..b69e24ec2 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -1,6 +1,6 @@ +from codecarbon.cli.monitor import run_and_monitor import os import signal -import subprocess import sys import time from pathlib import Path @@ -341,132 +341,17 @@ def config(): @codecarbon.command( - "run", - short_help="Run a command and track its emissions.", + "monitor", + short_help="Monitor your machine's carbon emissions.", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, ) -def run( - ctx: typer.Context, - log_level: Annotated[ - str, typer.Option(help="Log level (critical, error, warning, info, debug)") - ] = "error", -): - """ - Run a command and track its carbon emissions. - - This command wraps any executable and measures the process's total power - consumption during its execution. When the command completes, a summary - report is displayed and emissions data is saved to a CSV file. - - Note: This tracks process-level emissions (only the specific command), not the - entire machine. For machine-level tracking, use the `monitor` command. - - Examples: - - Do not use quotes around the command. Use -- to separate CodeCarbon args. - - # Run any shell command: - codecarbon run -- ./benchmark.sh - - # Commands with arguments (use single quotes for special chars): - codecarbon run -- python -c 'print("Hello World!")' - - # Pipe the command output: - codecarbon run -- npm run test > output.txt - - # Display the CodeCarbon detailed logs: - codecarbon run --log-level debug -- python --version - - The emissions data is appended to emissions.csv (default) in the current - directory. The file path is shown in the final report. - """ - # Suppress all CodeCarbon logs during execution - from codecarbon.external.logger import set_logger_level - - set_logger_level(log_level) - - # Get the command from remaining args - command = ctx.args - - if not command: - print( - "ERROR: No command provided. Use: codecarbon run -- ", - file=sys.stderr, - ) - raise typer.Exit(1) - - # Initialize tracker with specified logging level - tracker = EmissionsTracker( - log_level=log_level, save_to_logger=False, tracking_mode="process" - ) - - print("🌱 CodeCarbon: Starting emissions tracking...") - print(f" Command: {' '.join(command)}") - print() - - tracker.start() - - process = None - try: - # Run the command, streaming output to console - process = subprocess.Popen( - command, - stdout=sys.stdout, - stderr=sys.stderr, - text=True, - ) - - # Wait for completion - exit_code = process.wait() - - except FileNotFoundError: - print(f"āŒ Error: Command not found: {command[0]}", file=sys.stderr) - exit_code = 127 - except KeyboardInterrupt: - print("\nāš ļø Interrupted by user", file=sys.stderr) - if process is not None: - process.terminate() - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - process.kill() - exit_code = 130 - except Exception as e: - print(f"āŒ Error running command: {e}", file=sys.stderr) - exit_code = 1 - finally: - emissions = tracker.stop() - print() - print("=" * 60) - print("🌱 CodeCarbon Emissions Report") - print("=" * 60) - print(f" Command: {' '.join(command)}") - if emissions is not None: - print(f" Emissions: {emissions * 1000:.4f} g CO2eq") - 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}") - - print(" āš ļø Note: Tracked the command process and its children") - print("=" * 60) - - raise typer.Exit(exit_code) - - -@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.") def monitor( + ctx: typer.Context, measure_power_secs: Annotated[ - int, typer.Argument(help="Interval between two measures.") + int, typer.Option(help="Interval between two measures.") ] = 10, api_call_interval: Annotated[ - int, typer.Argument(help="Number of measures between API calls.") + int, typer.Option(help="Number of measures between API calls.") ] = 30, api: Annotated[ bool, typer.Option(help="Choose to call Code Carbon API or not") @@ -480,6 +365,13 @@ def monitor( ] = None, ): """Monitor your machine's carbon emissions.""" + + # Shared tracker args so monitor and run_and_monitor behave the same + tracker_args = { + "measure_power_secs": measure_power_secs, + "api_call_interval": api_call_interval, + } + # 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: print( @@ -487,11 +379,11 @@ def monitor( ) raise typer.Exit(1) - tracker = OfflineEmissionsTracker( - measure_power_secs=measure_power_secs, - country_iso_code=country_iso_code, - region=region, - ) + tracker_args = { + **tracker_args, + "country_iso_code": country_iso_code, + "region": region, + } else: experiment_id = get_existing_local_exp_id() if api and experiment_id is None: @@ -501,11 +393,17 @@ def monitor( ) raise typer.Exit(1) - tracker = EmissionsTracker( - measure_power_secs=measure_power_secs, - api_call_interval=api_call_interval, - save_to_api=api, - ) + tracker_args = {**tracker_args, "save_to_api": api} + + # If extra args are provided (e.g. `codecarbon monitor -- my_script.py`), delegate to `run_and_monitor` + if getattr(ctx, "args", None): + return run_and_monitor(ctx, **tracker_args) + + # Instantiate the tracker + if offline: + tracker = OfflineEmissionsTracker(**tracker_args) + else: + tracker = EmissionsTracker(**tracker_args) def signal_handler(signum, frame): print("\nReceived signal to stop. Saving emissions data...") diff --git a/codecarbon/cli/monitor.py b/codecarbon/cli/monitor.py new file mode 100644 index 000000000..e7e46c14c --- /dev/null +++ b/codecarbon/cli/monitor.py @@ -0,0 +1,130 @@ +"""CodeCarbon CLI - Monitor Command""" + +import os +import subprocess +import sys + +import typer +from rich import print +from typing_extensions import Annotated + +from codecarbon.emissions_tracker import EmissionsTracker + + +def run_and_monitor( + ctx: typer.Context, + log_level: Annotated[ + str, typer.Option(help="Log level (critical, error, warning, info, debug)") + ] = "error", + **tracker_args, +): + """ + Run a command and track its carbon emissions. + + This command wraps any executable and measures the process's total power + consumption during its execution. When the command completes, a summary + report is displayed and emissions data is saved to a CSV file. + + Note: This tracks process-level emissions (only the specific command), not the + entire machine. For machine-level tracking, use the `monitor` command. + + Examples: + + Do not use quotes around the command. Use -- to separate CodeCarbon args. + + # Run any shell command: + codecarbon monitor -- ./benchmark.sh + + # Commands with arguments (use single quotes for special chars): + codecarbon monitor -- python -c 'print("Hello World!")' + + # Pipe the command output: + codecarbon monitor -- npm run test > output.txt + + # Display the CodeCarbon detailed logs: + codecarbon monitor --log-level debug -- python --version + + The emissions data is appended to emissions.csv (default) in the current + directory. The file path is shown in the final report. + """ + # Suppress all CodeCarbon logs during execution + from codecarbon.external.logger import set_logger_level + + set_logger_level(log_level) + + # Get the command from remaining args + command = ctx.args + + if not command: + print( + "ERROR: No command provided. Use: codecarbon monitor -- ", + file=sys.stderr, + ) + raise typer.Exit(1) + + # Initialize tracker with specified logging level and shared args + tracker = EmissionsTracker( + log_level=log_level, + save_to_logger=False, + tracking_mode="process", + **tracker_args, + ) + + print("🌱 CodeCarbon: Starting emissions tracking...") + print(f" Command: {' '.join(command)}") + print() + + tracker.start() + + process = None + try: + # Run the command, streaming output to console + process = subprocess.Popen( + command, + stdout=sys.stdout, + stderr=sys.stderr, + text=True, + ) + + # Wait for completion + exit_code = process.wait() + + except FileNotFoundError: + print(f"āŒ Error: Command not found: {command[0]}", file=sys.stderr) + exit_code = 127 + except KeyboardInterrupt: + print("\nāš ļø Interrupted by user", file=sys.stderr) + if process is not None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + exit_code = 130 + except Exception as e: + print(f"āŒ Error running command: {e}", file=sys.stderr) + exit_code = 1 + finally: + emissions = tracker.stop() + print() + print("=" * 60) + print("🌱 CodeCarbon Emissions Report") + print("=" * 60) + print(f" Command: {' '.join(command)}") + if emissions is not None: + print(f" Emissions: {emissions * 1000:.4f} g CO2eq") + 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}") + + print(" āš ļø Note: Tracked the command process and its children") + print("=" * 60) + + raise typer.Exit(exit_code) diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index a5ecf856d..63774359a 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -70,12 +70,12 @@ The command line could also works without internet by providing the country code Running Any Command with CodeCarbon ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to track emissions while running any command or program (not just Python scripts), you can use the ``codecarbon run`` command. +If you want to track emissions while running any command or program (not just Python scripts), you can use the ``codecarbon monitor --`` command. This allows non-Python users to measure machine emissions during the execution of any command: .. code-block:: console - codecarbon run -- + codecarbon monitor -- Do not surround ```` with quotes. The double hyphen ``--`` indicates the end of CodeCarbon options and the beginning of the command to run. @@ -84,22 +84,22 @@ Do not surround ```` with quotes. The double hyphen ``--`` indicat .. code-block:: console # Run a shell script - codecarbon run -- ./benchmark.sh + codecarbon monitor -- ./benchmark.sh # Run a command with arguments (use quotes for special characters) - codecarbon run -- bash -c 'echo "Processing..."; sleep 30; echo "Done!"' + codecarbon monitor -- bash -c 'echo "Processing..."; sleep 30; echo "Done!"' # Run Python scripts - codecarbon run -- python train_model.py + codecarbon monitor -- python train_model.py # Run Node.js applications - codecarbon run -- node app.js + codecarbon monitor -- node app.js # Run tests with output redirection - codecarbon run -- npm run test > output.txt + codecarbon monitor -- npm run test > output.txt # Display the CodeCarbon detailed logs - codecarbon run --log-level debug -- python --version + codecarbon monitor --log-level debug -- python --version **Output:** @@ -123,7 +123,7 @@ When the command completes, CodeCarbon displays a summary report and saves the e ============================================================ .. note:: - The ``codecarbon run`` command tracks process-level emissions (only the specific command), not the + The ``codecarbon monitor --`` command tracks process-level emissions (only the specific command), not the entire machine. For machine-level tracking, use the ``codecarbon monitor`` command. For more fine-grained tracking, implementing CodeCarbon in your code allows you to track the emissions of a specific block of code. From a1e4ff667f82efcef21029f919c309ba498e7685 Mon Sep 17 00:00:00 2001 From: inimaz Date: Fri, 13 Feb 2026 18:31:19 +0100 Subject: [PATCH 5/6] style: pre-commit changes --- codecarbon/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index b69e24ec2..6f28c2309 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -1,4 +1,3 @@ -from codecarbon.cli.monitor import run_and_monitor import os import signal import sys @@ -23,6 +22,7 @@ get_existing_local_exp_id, overwrite_local_config, ) +from codecarbon.cli.monitor import run_and_monitor from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone from codecarbon.core.schemas import ExperimentCreate, OrganizationCreate, ProjectCreate from codecarbon.emissions_tracker import EmissionsTracker, OfflineEmissionsTracker From db49e5657c5499a8f7c10898a2f2a507055e9199 Mon Sep 17 00:00:00 2001 From: inimaz Date: Fri, 13 Feb 2026 19:04:45 +0100 Subject: [PATCH 6/6] tests: tests for the cli --- codecarbon/cli/monitor.py | 9 ++--- tests/test_cli_main.py | 81 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 tests/test_cli_main.py diff --git a/codecarbon/cli/monitor.py b/codecarbon/cli/monitor.py index e7e46c14c..ce606cfb5 100644 --- a/codecarbon/cli/monitor.py +++ b/codecarbon/cli/monitor.py @@ -79,12 +79,9 @@ def run_and_monitor( process = None try: # Run the command, streaming output to console - process = subprocess.Popen( - command, - stdout=sys.stdout, - stderr=sys.stderr, - text=True, - ) + # Let the child inherit the parent's std streams so Click's + # `CliRunner` can capture output (don't pass StringIO objects). + process = subprocess.Popen(command, text=True) # Wait for completion exit_code = process.wait() diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py new file mode 100644 index 000000000..0796b4218 --- /dev/null +++ b/tests/test_cli_main.py @@ -0,0 +1,81 @@ +"""Tests for the CodeCarbon CLI main function.""" + +from typer.testing import CliRunner + +from codecarbon.cli import main as cli_main + + +class FakeApiClient: + def __init__(self, endpoint_url=None): + self.endpoint_url = endpoint_url + self.token = None + + def set_access_token(self, token): + self.token = token + + def get_list_organizations(self): + return [{"id": "1", "name": "fake-org"}] + + +def fake_get_access_token(): + return "fake-token" + + +def test_version_flag(): + runner = CliRunner() + result = runner.invoke(cli_main.codecarbon, ["--version"]) + assert result.exit_code == 0 + assert cli_main.__app_name__ in result.output + assert str(cli_main.__version__) in result.output + + +def test_api_get_calls_api_and_prints(monkeypatch): + runner = CliRunner() + monkeypatch.setattr(cli_main, "ApiClient", FakeApiClient) + monkeypatch.setattr(cli_main, "_get_access_token", fake_get_access_token) + + result = runner.invoke(cli_main.codecarbon, ["test-api"]) + assert result.exit_code == 0 + assert "fake-org" in result.output + + +def test_monitor_offline_requires_country_iso_code(): + runner = CliRunner() + result = runner.invoke(cli_main.codecarbon, ["monitor", "--offline"]) + assert result.exit_code != 0 + assert "country_iso_code is required for offline mode" in result.output + + +def test_detect_monkeypatched_tracker(monkeypatch): + class FakeTracker: + def __init__(self, save_to_file=False, **kwargs): + pass + + def get_detected_hardware(self): + return { + "ram_total_size": 8.0, + "cpu_count": 4, + "cpu_physical_count": 2, + "cpu_model": "Fake CPU", + "gpu_count": 1, + "gpu_model": "Fake GPU", + "gpu_ids": None, + } + + monkeypatch.setattr(cli_main, "EmissionsTracker", FakeTracker) + runner = CliRunner() + result = runner.invoke(cli_main.codecarbon, ["detect"]) + assert result.exit_code == 0 + assert "Detected Hardware" in result.output + assert "Fake CPU" in result.output + + +def test_monitor_run_and_monitor(monkeypatch): + runner = CliRunner() + + # Test with a simple command + result = runner.invoke( + cli_main.codecarbon, ["monitor", "--no-api", "--", "echo", "Hello, World!"] + ) + assert result.exit_code == 0 + assert "Hello, World!" in result.output