diff --git a/packages/data-designer-config/src/data_designer/logging.py b/packages/data-designer-config/src/data_designer/logging.py index 3a280eae7..c2574b617 100644 --- a/packages/data-designer-config/src/data_designer/logging.py +++ b/packages/data-designer-config/src/data_designer/logging.py @@ -12,6 +12,8 @@ from pythonjsonlogger.json import JsonFormatter +_logging_configured = False + @dataclass class LoggerConfig: @@ -115,6 +117,7 @@ def working() -> str: def configure_logging(config: LoggingConfig | None = None) -> None: + global _logging_configured config = config or LoggingConfig.default() root_logger = logging.getLogger() @@ -137,6 +140,18 @@ def configure_logging(config: LoggingConfig | None = None) -> None: for name in config.to_silence: quiet_noisy_logger(name) + _logging_configured = True + + +def is_logging_configured() -> bool: + """Return whether ``configure_logging()`` has run in this process. + + Only tracks Data Designer's own ``configure_logging()``. Logging configured + through stdlib APIs (e.g. ``logging.basicConfig()`` or manually attached + root handlers) is not detected. + """ + return _logging_configured + def quiet_noisy_logger(name: str) -> None: logger = logging.getLogger(name) diff --git a/packages/data-designer-config/tests/test_logging.py b/packages/data-designer-config/tests/test_logging.py index e52c0f1e1..db8d10c1c 100644 --- a/packages/data-designer-config/tests/test_logging.py +++ b/packages/data-designer-config/tests/test_logging.py @@ -8,12 +8,14 @@ import pytest +import data_designer.logging as logging_mod from data_designer.logging import ( LoggerConfig, LoggingConfig, OutputConfig, RandomEmoji, configure_logging, + is_logging_configured, quiet_noisy_logger, ) @@ -105,6 +107,16 @@ def test_configure_logging_basic(stub_default_logging_config): assert ndd_logger.level == logging.INFO +def test_configure_logging_marks_logging_configured(stub_default_logging_config, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(logging_mod, "_logging_configured", False) + + assert is_logging_configured() is False + + configure_logging(stub_default_logging_config) + + assert is_logging_configured() is True + + def test_configure_logging_with_file(): with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".log") as tmp_file: tmp_path = Path(tmp_file.name) diff --git a/packages/data-designer/src/data_designer/interface/data_designer.py b/packages/data-designer/src/data_designer/interface/data_designer.py index 6db313c23..9b530655b 100644 --- a/packages/data-designer/src/data_designer/interface/data_designer.py +++ b/packages/data-designer/src/data_designer/interface/data_designer.py @@ -73,7 +73,7 @@ DataDesignerProfilingError, ) from data_designer.interface.results import DatasetCreationResults -from data_designer.logging import LOG_INDENT, RandomEmoji, configure_logging +from data_designer.logging import LOG_INDENT, RandomEmoji, configure_logging, is_logging_configured from data_designer.plugins.plugin import PluginType from data_designer.plugins.registry import PluginRegistry @@ -88,12 +88,13 @@ _interface_runtime_initialized = False -def _initialize_interface_runtime() -> None: +def _initialize_interface_runtime(*, auto_configure_logging: bool = True) -> None: """Run one-time runtime initialization for the interface package.""" global _interface_runtime_initialized if _interface_runtime_initialized: return - configure_logging() + if auto_configure_logging and not is_logging_configured(): + configure_logging() resolve_seed_default_model_settings() _interface_runtime_initialized = True @@ -141,6 +142,12 @@ class DataDesigner(DataDesignerInterface[DatasetCreationResults]): mcp_providers: Optional list of MCP provider configurations to enable tool-calling for LLM generation columns. Supports both MCPProvider (remote SSE or Streamable HTTP) and LocalStdioMCPProvider (local subprocess). + auto_configure_logging: Whether to apply Data Designer's default logging + configuration during construction. Set to False when embedding Data + Designer in an application that manages its own logging — Data Designer + will then leave existing root handlers and log levels untouched. + Automatic configuration is also skipped when + `data_designer.logging.configure_logging()` was already called. """ def __init__( @@ -153,8 +160,9 @@ def __init__( managed_assets_path: Path | str | None = None, person_reader: PersonReader | None = None, mcp_providers: list[MCPProviderT] | None = None, + auto_configure_logging: bool = True, ): - _initialize_interface_runtime() + _initialize_interface_runtime(auto_configure_logging=auto_configure_logging) self._secret_resolver = secret_resolver or DEFAULT_SECRET_RESOLVER self._artifact_path = Path(artifact_path) if artifact_path is not None else Path.cwd() / "artifacts" self._run_config = RunConfig() diff --git a/packages/data-designer/tests/interface/test_data_designer.py b/packages/data-designer/tests/interface/test_data_designer.py index b47f8785a..ac04b0b46 100644 --- a/packages/data-designer/tests/interface/test_data_designer.py +++ b/packages/data-designer/tests/interface/test_data_designer.py @@ -16,6 +16,7 @@ import data_designer.interface.data_designer as dd_mod import data_designer.lazy_heavy_imports as lazy +import data_designer.logging as dd_logging from data_designer.config.column_configs import ( ExpressionColumnConfig, LLMTextColumnConfig, @@ -1517,6 +1518,7 @@ def test_validate_raises_error_when_seed_collides( def test_initialize_interface_runtime_runs_once(monkeypatch: pytest.MonkeyPatch) -> None: """_initialize_interface_runtime only runs initialization once.""" monkeypatch.setattr(dd_mod, "_interface_runtime_initialized", False) + monkeypatch.setattr(dd_logging, "_logging_configured", False) with ( patch("data_designer.interface.data_designer.configure_logging") as mock_logging, @@ -1528,6 +1530,94 @@ def test_initialize_interface_runtime_runs_once(monkeypatch: pytest.MonkeyPatch) mock_resolve.assert_called_once() +def test_initialize_interface_runtime_respects_preconfigured_logging(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(dd_mod, "_interface_runtime_initialized", False) + monkeypatch.setattr(dd_logging, "_logging_configured", True) + + with ( + patch("data_designer.interface.data_designer.configure_logging") as mock_logging, + patch("data_designer.interface.data_designer.resolve_seed_default_model_settings") as mock_resolve, + ): + dd_mod._initialize_interface_runtime() + + mock_logging.assert_not_called() + mock_resolve.assert_called_once() + + +def test_initialize_interface_runtime_skips_logging_when_auto_configure_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(dd_mod, "_interface_runtime_initialized", False) + monkeypatch.setattr(dd_logging, "_logging_configured", False) + + with ( + patch("data_designer.interface.data_designer.configure_logging") as mock_logging, + patch("data_designer.interface.data_designer.resolve_seed_default_model_settings") as mock_resolve, + ): + dd_mod._initialize_interface_runtime(auto_configure_logging=False) + + mock_logging.assert_not_called() + mock_resolve.assert_called_once() + + +def test_init_auto_configures_logging_by_default( + monkeypatch: pytest.MonkeyPatch, + stub_artifact_path: Path, + stub_model_providers: list[ModelProvider], +) -> None: + monkeypatch.setattr(dd_mod, "_interface_runtime_initialized", False) + monkeypatch.setattr(dd_logging, "_logging_configured", False) + + with patch("data_designer.interface.data_designer.configure_logging") as mock_logging: + DataDesigner(artifact_path=stub_artifact_path, model_providers=stub_model_providers) + + mock_logging.assert_called_once() + + +def test_init_auto_configure_logging_false_preserves_root_handlers( + monkeypatch: pytest.MonkeyPatch, + stub_artifact_path: Path, + stub_model_providers: list[ModelProvider], +) -> None: + monkeypatch.setattr(dd_mod, "_interface_runtime_initialized", False) + monkeypatch.setattr(dd_logging, "_logging_configured", False) + root_logger = logging.getLogger() + sentinel_handler = logging.NullHandler() + root_logger.addHandler(sentinel_handler) + + try: + DataDesigner( + artifact_path=stub_artifact_path, + model_providers=stub_model_providers, + auto_configure_logging=False, + ) + + assert sentinel_handler in root_logger.handlers + assert dd_logging.is_logging_configured() is False + finally: + root_logger.removeHandler(sentinel_handler) + + +def test_init_preserves_preconfigured_logging( + monkeypatch: pytest.MonkeyPatch, + stub_artifact_path: Path, + stub_model_providers: list[ModelProvider], +) -> None: + monkeypatch.setattr(dd_mod, "_interface_runtime_initialized", False) + monkeypatch.setattr(dd_logging, "_logging_configured", False) + data_designer_logger = logging.getLogger("data_designer") + previous_level = data_designer_logger.level + + try: + dd_logging.configure_logging(dd_logging.LoggingConfig.debug()) + + DataDesigner(artifact_path=stub_artifact_path, model_providers=stub_model_providers) + + assert data_designer_logger.level == logging.DEBUG + finally: + data_designer_logger.setLevel(previous_level) + + def test_create_dataset_e2e_with_directory_seed_source( stub_artifact_path: Path, stub_model_providers: list[ModelProvider],