From 1b6c7e53abd7b7dc4303716568a910841cdaa6f0 Mon Sep 17 00:00:00 2001 From: prasanthmax Date: Fri, 18 Sep 2026 20:58:11 +0530 Subject: [PATCH 1/2] fix(emissions_tracker): raise ValueError for LOGGER output without logging_logger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1412. OutputMethod.LOGGER silently appended None to _output_handlers when logging_logger wasn't provided, causing an AttributeError inside a suppress() block later in flush()/stop() — swallowed silently, giving no error and no output. Now raises a clear ValueError at construction time instead. Added test_logger_output_raises_without_logging_logger and test_logger_output_succeeds_with_logging_logger in test_emissions_tracker.py. Updated two existing tests that relied on the old silent-None behavior without ever supplying a real logger: test_save_to_flags_map_to_output_methods_and_warn and test_decorator_flush. --- codecarbon/emissions_tracker.py | 7 +++++++ tests/test_custom_handler.py | 3 +++ tests/test_emissions_tracker.py | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 02e905cca..0e2c16471 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -639,6 +639,13 @@ def _init_output_methods(self, *, api_key: str = None): ) if OutputMethod.LOGGER in methods: + if self._logging_logger is None: + raise ValueError( + "OutputMethod.LOGGER was requested but no `logging_logger`" + " was provided. Unlike other output methods, LOGGER wraps" + " a logging.Logger instance you supply yourself: pass" + " `logging_logger=LoggerOutput(...)` to EmissionsTracker." + ) self._output_handlers.append(self._logging_logger) if self._emissions_endpoint: diff --git a/tests/test_custom_handler.py b/tests/test_custom_handler.py index 8adcf7c37..c2685ea0b 100644 --- a/tests/test_custom_handler.py +++ b/tests/test_custom_handler.py @@ -1,9 +1,11 @@ +import logging import time import unittest from typing import List from codecarbon.emissions_tracker import EmissionsTracker, track_emissions from codecarbon.output import BaseOutput, EmissionsData +from codecarbon.output_methods.logger import LoggerOutput def heavy_computation(run_time_secs: int = 3): @@ -52,6 +54,7 @@ def test_decorator_flush(self): @track_emissions( project_name=self.project_name, save_to_logger=True, + logging_logger=LoggerOutput(logging.getLogger("test_decorator_flush")), output_handlers=[handler_0, handler_1], api_call_interval=2, measure_power_secs=999, diff --git a/tests/test_emissions_tracker.py b/tests/test_emissions_tracker.py index fd0590a23..516b89afd 100644 --- a/tests/test_emissions_tracker.py +++ b/tests/test_emissions_tracker.py @@ -1,3 +1,4 @@ +import logging import os import shutil import sys @@ -20,6 +21,7 @@ ) from codecarbon.external.geography import CloudMetadata from codecarbon.output import BoAmpsOutput, CodeCarbonAPIOutput, OutputMethod +from codecarbon.output_methods.logger import LoggerOutput from tests.fake_modules import pynvml as fake_pynvml from tests.testdata import ( GEO_METADATA_CANADA, @@ -419,6 +421,7 @@ def test_save_to_flags_map_to_output_methods_and_warn( output_handlers=[], save_to_file=True, save_to_logger=True, + logging_logger=LoggerOutput(logging.getLogger("test_save_to_flags")), ) self.assertIn(OutputMethod.CSV, tracker._output_methods) @@ -1099,6 +1102,40 @@ def test_scheduler_warning_shown_when_running( scheduler_warning_found, "Expected scheduler warning was not found" ) + def test_logger_output_raises_without_logging_logger( + self, + mock_cli_setup, + mock_log_values, + mocked_get_gpu_details, + mocked_env_cloud_details, + mocked_get_gpu_utilization_list, + mocked_is_gpu_details_available, + mocked_is_nvidia_system, + ): + with self.assertRaises(ValueError): + EmissionsTracker( + project_name=self.project_name, + output_methods=["logger"], + ) + + def test_logger_output_succeeds_with_logging_logger( + self, + mock_cli_setup, + mock_log_values, + mocked_get_gpu_details, + mocked_env_cloud_details, + mocked_get_gpu_utilization_list, + mocked_is_gpu_details_available, + mocked_is_nvidia_system, + ): + my_logger = logging.getLogger("test_logger_output") + tracker = EmissionsTracker( + project_name=self.project_name, + output_methods=["logger"], + logging_logger=LoggerOutput(my_logger), + ) + self.assertIsNotNone(tracker) + def test_get_detected_hardware( self, mock_cli_setup, From fbba92271f82f78581064895a121539f30ff61d1 Mon Sep 17 00:00:00 2001 From: prasanthmax Date: Tue, 13 Oct 2026 11:10:01 +0530 Subject: [PATCH 2/2] fix(emissions_tracker): warn and skip LOGGER output when logging_logger is missing Raising in the constructor would break users who enable save_to_logger via config file or env var, since they cannot supply a logger object from there. Log a warning and skip the handler so the other outputs keep working. Revert the test_custom_handler.py change, which is no longer needed. --- codecarbon/emissions_tracker.py | 11 ++++++----- tests/test_custom_handler.py | 3 --- tests/test_emissions_tracker.py | 18 +++++++++++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 0e2c16471..8a68f9cbe 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -640,13 +640,14 @@ def _init_output_methods(self, *, api_key: str = None): if OutputMethod.LOGGER in methods: if self._logging_logger is None: - raise ValueError( + logger.warning( "OutputMethod.LOGGER was requested but no `logging_logger`" - " was provided. Unlike other output methods, LOGGER wraps" - " a logging.Logger instance you supply yourself: pass" - " `logging_logger=LoggerOutput(...)` to EmissionsTracker." + " was provided, so the logger output is skipped. Pass" + " `logging_logger=LoggerOutput(...)` to EmissionsTracker" + " to enable it." ) - self._output_handlers.append(self._logging_logger) + else: + self._output_handlers.append(self._logging_logger) if self._emissions_endpoint: self._output_handlers.append(HTTPOutput(self._emissions_endpoint)) diff --git a/tests/test_custom_handler.py b/tests/test_custom_handler.py index c2685ea0b..8adcf7c37 100644 --- a/tests/test_custom_handler.py +++ b/tests/test_custom_handler.py @@ -1,11 +1,9 @@ -import logging import time import unittest from typing import List from codecarbon.emissions_tracker import EmissionsTracker, track_emissions from codecarbon.output import BaseOutput, EmissionsData -from codecarbon.output_methods.logger import LoggerOutput def heavy_computation(run_time_secs: int = 3): @@ -54,7 +52,6 @@ def test_decorator_flush(self): @track_emissions( project_name=self.project_name, save_to_logger=True, - logging_logger=LoggerOutput(logging.getLogger("test_decorator_flush")), output_handlers=[handler_0, handler_1], api_call_interval=2, measure_power_secs=999, diff --git a/tests/test_emissions_tracker.py b/tests/test_emissions_tracker.py index 516b89afd..71b656c11 100644 --- a/tests/test_emissions_tracker.py +++ b/tests/test_emissions_tracker.py @@ -421,7 +421,6 @@ def test_save_to_flags_map_to_output_methods_and_warn( output_handlers=[], save_to_file=True, save_to_logger=True, - logging_logger=LoggerOutput(logging.getLogger("test_save_to_flags")), ) self.assertIn(OutputMethod.CSV, tracker._output_methods) @@ -1102,8 +1101,10 @@ def test_scheduler_warning_shown_when_running( scheduler_warning_found, "Expected scheduler warning was not found" ) - def test_logger_output_raises_without_logging_logger( + @mock.patch("codecarbon.emissions_tracker.logger") + def test_logger_output_warns_and_skips_without_logging_logger( self, + mock_logger, mock_cli_setup, mock_log_values, mocked_get_gpu_details, @@ -1112,11 +1113,14 @@ def test_logger_output_raises_without_logging_logger( mocked_is_gpu_details_available, mocked_is_nvidia_system, ): - with self.assertRaises(ValueError): - EmissionsTracker( - project_name=self.project_name, - output_methods=["logger"], - ) + tracker = EmissionsTracker( + project_name=self.project_name, + output_methods=["logger"], + ) + + self.assertNotIn(None, tracker._output_handlers) + warnings = [str(c.args[0]) for c in mock_logger.warning.call_args_list] + self.assertTrue(any("logging_logger" in w for w in warnings)) def test_logger_output_succeeds_with_logging_logger( self,