From c383f3fc1469fc78c63ee6f0d847dec7ea1d7f72 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 11:59:10 -0400 Subject: [PATCH 1/9] feat(cli): bootstrap default configs on command run --- .../src/data_designer/cli/lazy_group.py | 33 +++++-- .../src/data_designer/cli/runtime.py | 34 +++++++ .../data_designer/cli/utils/config_loader.py | 16 +--- packages/data-designer/tests/cli/test_main.py | 95 +++++++++++++++++++ .../data-designer/tests/cli/test_runtime.py | 38 ++++++++ .../tests/cli/utils/test_config_loader.py | 27 ++++-- 6 files changed, 215 insertions(+), 28 deletions(-) create mode 100644 packages/data-designer/src/data_designer/cli/runtime.py create mode 100644 packages/data-designer/tests/cli/test_main.py create mode 100644 packages/data-designer/tests/cli/test_runtime.py diff --git a/packages/data-designer/src/data_designer/cli/lazy_group.py b/packages/data-designer/src/data_designer/cli/lazy_group.py index 15e0d252..0bce06e8 100644 --- a/packages/data-designer/src/data_designer/cli/lazy_group.py +++ b/packages/data-designer/src/data_designer/cli/lazy_group.py @@ -4,12 +4,15 @@ from __future__ import annotations import importlib +from functools import wraps from typing import Any import click import typer from typer.core import TyperGroup +from data_designer.cli.runtime import ensure_cli_default_model_settings + class _LazyCommand(click.Command): """A click.Command stub that defers module loading until invocation. @@ -46,10 +49,12 @@ def _resolve(self) -> click.Command: # Typer returns a Group when there are multiple commands, but a single # Command when there is only one. Handle both cases. if hasattr(click_cmd, "commands"): - self._resolved = click_cmd.commands[self.name] + resolved = click_cmd.commands[self.name] else: - self._resolved = click_cmd - return self._resolved + resolved = click_cmd + _wrap_command_with_cli_bootstrap(resolved) + self._resolved = resolved + return resolved def make_context( self, @@ -60,9 +65,6 @@ def make_context( ) -> click.Context: return self._resolve().make_context(info_name, args, parent, **extra) - def invoke(self, ctx: click.Context) -> Any: - return self._resolve().invoke(ctx) - def create_lazy_typer_group( lazy_subcommands: dict[str, dict[str, str]], @@ -107,3 +109,22 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None return None return LazyTyperGroup + + +def _wrap_command_with_cli_bootstrap(command: click.Command) -> None: + """Wrap a resolved command callback with one-time CLI bootstrap.""" + if getattr(command, "_dd_bootstrap_wrapped", False): + return + + callback = command.callback + if callback is None: + command._dd_bootstrap_wrapped = True + return + + @wraps(callback) + def wrapped_callback(*args: Any, **kwargs: Any) -> Any: + ensure_cli_default_model_settings() + return callback(*args, **kwargs) + + command.callback = wrapped_callback + command._dd_bootstrap_wrapped = True diff --git a/packages/data-designer/src/data_designer/cli/runtime.py b/packages/data-designer/src/data_designer/cli/runtime.py new file mode 100644 index 00000000..708ac238 --- /dev/null +++ b/packages/data-designer/src/data_designer/cli/runtime.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from data_designer.cli.ui import print_warning +from data_designer.config.default_model_settings import resolve_seed_default_model_settings + +_BOOTSTRAP_COMMAND = ( + 'python -c "from data_designer.config.default_model_settings import ' + 'resolve_seed_default_model_settings; resolve_seed_default_model_settings()"' +) + +_default_model_settings_checked = False + + +def ensure_cli_default_model_settings() -> None: + """Best-effort bootstrap for CLI default model settings.""" + global _default_model_settings_checked + if _default_model_settings_checked: + return + + try: + resolve_seed_default_model_settings() + except Exception as e: + print_warning( + "Could not initialize default model providers and model configs automatically. " + f"The command will continue. Error: {e}. " + f"You can retry setup with `{_BOOTSTRAP_COMMAND}` " + "or configure providers/models manually with `data-designer config providers` " + "and `data-designer config models`." + ) + finally: + _default_model_settings_checked = True diff --git a/packages/data-designer/src/data_designer/cli/utils/config_loader.py b/packages/data-designer/src/data_designer/cli/utils/config_loader.py index 80ccc4b0..04471ebc 100644 --- a/packages/data-designer/src/data_designer/cli/utils/config_loader.py +++ b/packages/data-designer/src/data_designer/cli/utils/config_loader.py @@ -8,8 +8,8 @@ from pathlib import Path from urllib.parse import urlparse +from data_designer.cli.runtime import ensure_cli_default_model_settings from data_designer.config.config_builder import DataDesignerConfigBuilder -from data_designer.config.default_model_settings import resolve_seed_default_model_settings from data_designer.config.utils.io_helpers import VALID_CONFIG_FILE_EXTENSIONS, is_http_url @@ -23,18 +23,6 @@ class ConfigLoadError(Exception): USER_MODULE_FUNC_NAME = "load_config_builder" -_default_settings_initialized = False - - -def _ensure_default_model_settings() -> None: - """Initialize default model/provider files once before loading CLI configs.""" - global _default_settings_initialized - if _default_settings_initialized: - return - resolve_seed_default_model_settings() - _default_settings_initialized = True - - def load_config_builder(config_source: str) -> DataDesignerConfigBuilder: """Load a DataDesignerConfigBuilder from a file path or URL. @@ -52,7 +40,7 @@ def load_config_builder(config_source: str) -> DataDesignerConfigBuilder: Raises: ConfigLoadError: If the file cannot be loaded or is invalid. """ - _ensure_default_model_settings() + ensure_cli_default_model_settings() if is_http_url(config_source): return _load_from_config_url(config_source) diff --git a/packages/data-designer/tests/cli/test_main.py b/packages/data-designer/tests/cli/test_main.py new file mode 100644 index 00000000..6dfbe5a2 --- /dev/null +++ b/packages/data-designer/tests/cli/test_main.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from typer.testing import CliRunner + +from data_designer.cli.main import app + +runner = CliRunner() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +@patch("data_designer.cli.commands.create.GenerationController") +def test_cli_bootstraps_for_create_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: + """CLI bootstrap runs before create command execution.""" + mock_controller = MagicMock() + mock_controller_cls.return_value = mock_controller + + result = runner.invoke(app, ["create", "config.yaml"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_called_once_with() + mock_controller.run_create.assert_called_once() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +@patch("data_designer.cli.commands.preview.GenerationController") +def test_cli_bootstraps_for_preview_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: + """CLI bootstrap runs before preview command execution.""" + mock_controller = MagicMock() + mock_controller_cls.return_value = mock_controller + + result = runner.invoke(app, ["preview", "config.yaml", "--non-interactive"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_called_once_with() + mock_controller.run_preview.assert_called_once() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +@patch("data_designer.cli.commands.models.ModelController") +def test_cli_bootstraps_for_config_models_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: + """CLI bootstrap runs before config models command execution.""" + mock_controller = MagicMock() + mock_controller_cls.return_value = mock_controller + + result = runner.invoke(app, ["config", "models"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_called_once_with() + mock_controller.run.assert_called_once() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +@patch("data_designer.cli.commands.download.DownloadController") +def test_cli_bootstraps_for_download_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: + """CLI bootstrap runs before download personas command execution.""" + mock_controller = MagicMock() + mock_controller_cls.return_value = mock_controller + + result = runner.invoke(app, ["download", "personas", "--list"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_called_once_with() + mock_controller.list_personas.assert_called_once() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +def test_cli_help_does_not_bootstrap(mock_bootstrap: MagicMock) -> None: + """Top-level help remains side-effect free.""" + result = runner.invoke(app, ["--help"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_not_called() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +def test_config_help_does_not_bootstrap(mock_bootstrap: MagicMock) -> None: + """Config group help remains side-effect free.""" + result = runner.invoke(app, ["config", "--help"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_not_called() + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +def test_download_help_does_not_bootstrap(mock_bootstrap: MagicMock) -> None: + """Download group help remains side-effect free.""" + result = runner.invoke(app, ["download", "--help"]) + + assert result.exit_code == 0 + mock_bootstrap.assert_not_called() diff --git a/packages/data-designer/tests/cli/test_runtime.py b/packages/data-designer/tests/cli/test_runtime.py new file mode 100644 index 00000000..3f717514 --- /dev/null +++ b/packages/data-designer/tests/cli/test_runtime.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +import data_designer.cli.runtime as runtime_mod + + +def test_ensure_cli_default_model_settings_runs_once(monkeypatch: pytest.MonkeyPatch) -> None: + """CLI bootstrap only attempts default setup once per process.""" + monkeypatch.setattr(runtime_mod, "_default_model_settings_checked", False) + + with patch("data_designer.cli.runtime.resolve_seed_default_model_settings") as mock_resolve: + runtime_mod.ensure_cli_default_model_settings() + runtime_mod.ensure_cli_default_model_settings() + + mock_resolve.assert_called_once_with() + + +def test_ensure_cli_default_model_settings_warns_and_continues(monkeypatch: pytest.MonkeyPatch) -> None: + """CLI bootstrap prints an actionable warning when setup fails.""" + monkeypatch.setattr(runtime_mod, "_default_model_settings_checked", False) + + with ( + patch("data_designer.cli.runtime.print_warning") as mock_print_warning, + patch("data_designer.cli.runtime.resolve_seed_default_model_settings", side_effect=RuntimeError("boom")), + ): + runtime_mod.ensure_cli_default_model_settings() + + mock_print_warning.assert_called_once() + warning = mock_print_warning.call_args[0][0] + assert "Could not initialize default model providers and model configs automatically." in warning + assert "The command will continue." in warning + assert "resolve_seed_default_model_settings" in warning diff --git a/packages/data-designer/tests/cli/utils/test_config_loader.py b/packages/data-designer/tests/cli/utils/test_config_loader.py index df80946e..683766be 100644 --- a/packages/data-designer/tests/cli/utils/test_config_loader.py +++ b/packages/data-designer/tests/cli/utils/test_config_loader.py @@ -1,12 +1,13 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from pathlib import Path from unittest.mock import MagicMock, patch import pytest -import data_designer.cli.utils.config_loader as config_loader_mod from data_designer.cli.utils.config_loader import ( ConfigLoadError, load_config_builder, @@ -290,11 +291,21 @@ def test_load_config_builder_empty_yaml(tmp_path: Path) -> None: load_config_builder(str(yaml_file)) -def test_ensure_default_model_settings_runs_once(monkeypatch: pytest.MonkeyPatch) -> None: - """_ensure_default_model_settings only calls resolve_seed_default_model_settings once.""" - monkeypatch.setattr(config_loader_mod, "_default_settings_initialized", False) +@patch("data_designer.cli.utils.config_loader.DataDesignerConfigBuilder.from_config") +@patch("data_designer.cli.utils.config_loader.ensure_cli_default_model_settings") +def test_load_config_builder_uses_shared_cli_bootstrap( + mock_ensure_bootstrap: MagicMock, + mock_from_config: MagicMock, + tmp_path: Path, +) -> None: + """Config loader delegates default setup to the shared CLI bootstrap helper.""" + yaml_file = tmp_path / "config.yaml" + yaml_file.write_text("data_designer:\n columns: []\n") + mock_builder = MagicMock() + mock_from_config.return_value = mock_builder + + result = load_config_builder(str(yaml_file)) - with patch("data_designer.cli.utils.config_loader.resolve_seed_default_model_settings") as mock_resolve: - config_loader_mod._ensure_default_model_settings() - config_loader_mod._ensure_default_model_settings() - mock_resolve.assert_called_once() + mock_ensure_bootstrap.assert_called_once_with() + mock_from_config.assert_called_once_with(yaml_file) + assert result is mock_builder From dd51f5e30332bbefa84b5c503ffb0cfe05429a90 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 12:11:46 -0400 Subject: [PATCH 2/9] fix(cli): use active interpreter in bootstrap warning --- packages/data-designer/src/data_designer/cli/runtime.py | 5 ++++- packages/data-designer/tests/cli/test_runtime.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/data-designer/src/data_designer/cli/runtime.py b/packages/data-designer/src/data_designer/cli/runtime.py index 708ac238..6fdaea03 100644 --- a/packages/data-designer/src/data_designer/cli/runtime.py +++ b/packages/data-designer/src/data_designer/cli/runtime.py @@ -3,11 +3,14 @@ from __future__ import annotations +import shlex +import sys + from data_designer.cli.ui import print_warning from data_designer.config.default_model_settings import resolve_seed_default_model_settings _BOOTSTRAP_COMMAND = ( - 'python -c "from data_designer.config.default_model_settings import ' + f'{shlex.quote(sys.executable)} -c "from data_designer.config.default_model_settings import ' 'resolve_seed_default_model_settings; resolve_seed_default_model_settings()"' ) diff --git a/packages/data-designer/tests/cli/test_runtime.py b/packages/data-designer/tests/cli/test_runtime.py index 3f717514..f182f726 100644 --- a/packages/data-designer/tests/cli/test_runtime.py +++ b/packages/data-designer/tests/cli/test_runtime.py @@ -19,6 +19,7 @@ def test_ensure_cli_default_model_settings_runs_once(monkeypatch: pytest.MonkeyP runtime_mod.ensure_cli_default_model_settings() mock_resolve.assert_called_once_with() + assert runtime_mod._default_model_settings_checked is True def test_ensure_cli_default_model_settings_warns_and_continues(monkeypatch: pytest.MonkeyPatch) -> None: From 5714a1af1e0dc1d8780e29bfac7e8ceb607e836a Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 12:34:22 -0400 Subject: [PATCH 3/9] refactor(cli): simplify bootstrap warning flow --- .../src/data_designer/cli/lazy_group.py | 9 ++++--- .../src/data_designer/cli/runtime.py | 13 ++-------- .../tests/cli/test_lazy_group.py | 26 +++++++++++++++++++ .../data-designer/tests/cli/test_runtime.py | 3 ++- 4 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 packages/data-designer/tests/cli/test_lazy_group.py diff --git a/packages/data-designer/src/data_designer/cli/lazy_group.py b/packages/data-designer/src/data_designer/cli/lazy_group.py index 0bce06e8..d706c4a9 100644 --- a/packages/data-designer/src/data_designer/cli/lazy_group.py +++ b/packages/data-designer/src/data_designer/cli/lazy_group.py @@ -6,6 +6,7 @@ import importlib from functools import wraps from typing import Any +from weakref import WeakSet import click import typer @@ -13,6 +14,8 @@ from data_designer.cli.runtime import ensure_cli_default_model_settings +_BOOTSTRAP_WRAPPED_COMMANDS: WeakSet[click.Command] = WeakSet() + class _LazyCommand(click.Command): """A click.Command stub that defers module loading until invocation. @@ -113,12 +116,12 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None def _wrap_command_with_cli_bootstrap(command: click.Command) -> None: """Wrap a resolved command callback with one-time CLI bootstrap.""" - if getattr(command, "_dd_bootstrap_wrapped", False): + if command in _BOOTSTRAP_WRAPPED_COMMANDS: return callback = command.callback if callback is None: - command._dd_bootstrap_wrapped = True + _BOOTSTRAP_WRAPPED_COMMANDS.add(command) return @wraps(callback) @@ -127,4 +130,4 @@ def wrapped_callback(*args: Any, **kwargs: Any) -> Any: return callback(*args, **kwargs) command.callback = wrapped_callback - command._dd_bootstrap_wrapped = True + _BOOTSTRAP_WRAPPED_COMMANDS.add(command) diff --git a/packages/data-designer/src/data_designer/cli/runtime.py b/packages/data-designer/src/data_designer/cli/runtime.py index 6fdaea03..81994811 100644 --- a/packages/data-designer/src/data_designer/cli/runtime.py +++ b/packages/data-designer/src/data_designer/cli/runtime.py @@ -3,17 +3,9 @@ from __future__ import annotations -import shlex -import sys - from data_designer.cli.ui import print_warning from data_designer.config.default_model_settings import resolve_seed_default_model_settings -_BOOTSTRAP_COMMAND = ( - f'{shlex.quote(sys.executable)} -c "from data_designer.config.default_model_settings import ' - 'resolve_seed_default_model_settings; resolve_seed_default_model_settings()"' -) - _default_model_settings_checked = False @@ -29,9 +21,8 @@ def ensure_cli_default_model_settings() -> None: print_warning( "Could not initialize default model providers and model configs automatically. " f"The command will continue. Error: {e}. " - f"You can retry setup with `{_BOOTSTRAP_COMMAND}` " - "or configure providers/models manually with `data-designer config providers` " - "and `data-designer config models`." + "You will need to configure providers and models manually with " + "`data-designer config providers` and `data-designer config models`." ) finally: _default_model_settings_checked = True diff --git a/packages/data-designer/tests/cli/test_lazy_group.py b/packages/data-designer/tests/cli/test_lazy_group.py new file mode 100644 index 00000000..1769d962 --- /dev/null +++ b/packages/data-designer/tests/cli/test_lazy_group.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import click + +from data_designer.cli.lazy_group import _wrap_command_with_cli_bootstrap + + +@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") +def test_wrap_command_with_cli_bootstrap_is_idempotent(mock_bootstrap: MagicMock) -> None: + """Wrapping the same command twice should only add one bootstrap layer.""" + callback = MagicMock() + command = click.Command("test", callback=callback) + + _wrap_command_with_cli_bootstrap(command) + _wrap_command_with_cli_bootstrap(command) + + assert command.callback is not None + command.callback() + + mock_bootstrap.assert_called_once_with() + callback.assert_called_once_with() diff --git a/packages/data-designer/tests/cli/test_runtime.py b/packages/data-designer/tests/cli/test_runtime.py index f182f726..a5f89539 100644 --- a/packages/data-designer/tests/cli/test_runtime.py +++ b/packages/data-designer/tests/cli/test_runtime.py @@ -36,4 +36,5 @@ def test_ensure_cli_default_model_settings_warns_and_continues(monkeypatch: pyte warning = mock_print_warning.call_args[0][0] assert "Could not initialize default model providers and model configs automatically." in warning assert "The command will continue." in warning - assert "resolve_seed_default_model_settings" in warning + assert "data-designer config providers" in warning + assert "data-designer config models" in warning From 64e105a3a42cb315ebc66beb1064938ff029b413 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 12:56:03 -0400 Subject: [PATCH 4/9] refactor(cli): bootstrap defaults in main entrypoint --- .../src/data_designer/cli/lazy_group.py | 33 +------ .../src/data_designer/cli/main.py | 2 + .../tests/cli/test_lazy_group.py | 26 ----- packages/data-designer/tests/cli/test_main.py | 96 +++---------------- 4 files changed, 16 insertions(+), 141 deletions(-) delete mode 100644 packages/data-designer/tests/cli/test_lazy_group.py diff --git a/packages/data-designer/src/data_designer/cli/lazy_group.py b/packages/data-designer/src/data_designer/cli/lazy_group.py index d706c4a9..f498b323 100644 --- a/packages/data-designer/src/data_designer/cli/lazy_group.py +++ b/packages/data-designer/src/data_designer/cli/lazy_group.py @@ -4,18 +4,12 @@ from __future__ import annotations import importlib -from functools import wraps from typing import Any -from weakref import WeakSet import click import typer from typer.core import TyperGroup -from data_designer.cli.runtime import ensure_cli_default_model_settings - -_BOOTSTRAP_WRAPPED_COMMANDS: WeakSet[click.Command] = WeakSet() - class _LazyCommand(click.Command): """A click.Command stub that defers module loading until invocation. @@ -52,12 +46,10 @@ def _resolve(self) -> click.Command: # Typer returns a Group when there are multiple commands, but a single # Command when there is only one. Handle both cases. if hasattr(click_cmd, "commands"): - resolved = click_cmd.commands[self.name] + self._resolved = click_cmd.commands[self.name] else: - resolved = click_cmd - _wrap_command_with_cli_bootstrap(resolved) - self._resolved = resolved - return resolved + self._resolved = click_cmd + return self._resolved def make_context( self, @@ -112,22 +104,3 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None return None return LazyTyperGroup - - -def _wrap_command_with_cli_bootstrap(command: click.Command) -> None: - """Wrap a resolved command callback with one-time CLI bootstrap.""" - if command in _BOOTSTRAP_WRAPPED_COMMANDS: - return - - callback = command.callback - if callback is None: - _BOOTSTRAP_WRAPPED_COMMANDS.add(command) - return - - @wraps(callback) - def wrapped_callback(*args: Any, **kwargs: Any) -> Any: - ensure_cli_default_model_settings() - return callback(*args, **kwargs) - - command.callback = wrapped_callback - _BOOTSTRAP_WRAPPED_COMMANDS.add(command) diff --git a/packages/data-designer/src/data_designer/cli/main.py b/packages/data-designer/src/data_designer/cli/main.py index a45276c4..b4ab7dde 100644 --- a/packages/data-designer/src/data_designer/cli/main.py +++ b/packages/data-designer/src/data_designer/cli/main.py @@ -6,6 +6,7 @@ import typer from data_designer.cli.lazy_group import create_lazy_typer_group +from data_designer.cli.runtime import ensure_cli_default_model_settings _CMD = "data_designer.cli.commands" @@ -105,6 +106,7 @@ def main() -> None: """Main entry point for the CLI.""" + ensure_cli_default_model_settings() app() diff --git a/packages/data-designer/tests/cli/test_lazy_group.py b/packages/data-designer/tests/cli/test_lazy_group.py deleted file mode 100644 index 1769d962..00000000 --- a/packages/data-designer/tests/cli/test_lazy_group.py +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from unittest.mock import MagicMock, patch - -import click - -from data_designer.cli.lazy_group import _wrap_command_with_cli_bootstrap - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -def test_wrap_command_with_cli_bootstrap_is_idempotent(mock_bootstrap: MagicMock) -> None: - """Wrapping the same command twice should only add one bootstrap layer.""" - callback = MagicMock() - command = click.Command("test", callback=callback) - - _wrap_command_with_cli_bootstrap(command) - _wrap_command_with_cli_bootstrap(command) - - assert command.callback is not None - command.callback() - - mock_bootstrap.assert_called_once_with() - callback.assert_called_once_with() diff --git a/packages/data-designer/tests/cli/test_main.py b/packages/data-designer/tests/cli/test_main.py index 6dfbe5a2..2b25e29f 100644 --- a/packages/data-designer/tests/cli/test_main.py +++ b/packages/data-designer/tests/cli/test_main.py @@ -3,93 +3,19 @@ from __future__ import annotations -from unittest.mock import MagicMock, patch +from unittest.mock import Mock, call, patch -from typer.testing import CliRunner +from data_designer.cli.main import main -from data_designer.cli.main import app -runner = CliRunner() +@patch("data_designer.cli.main.app") +@patch("data_designer.cli.main.ensure_cli_default_model_settings") +def test_main_bootstraps_before_running_app(mock_bootstrap: Mock, mock_app: Mock) -> None: + """The CLI entrypoint bootstraps defaults before invoking Typer.""" + call_order = Mock() + call_order.attach_mock(mock_bootstrap, "bootstrap") + call_order.attach_mock(mock_app, "app") + main() -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -@patch("data_designer.cli.commands.create.GenerationController") -def test_cli_bootstraps_for_create_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: - """CLI bootstrap runs before create command execution.""" - mock_controller = MagicMock() - mock_controller_cls.return_value = mock_controller - - result = runner.invoke(app, ["create", "config.yaml"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_called_once_with() - mock_controller.run_create.assert_called_once() - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -@patch("data_designer.cli.commands.preview.GenerationController") -def test_cli_bootstraps_for_preview_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: - """CLI bootstrap runs before preview command execution.""" - mock_controller = MagicMock() - mock_controller_cls.return_value = mock_controller - - result = runner.invoke(app, ["preview", "config.yaml", "--non-interactive"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_called_once_with() - mock_controller.run_preview.assert_called_once() - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -@patch("data_designer.cli.commands.models.ModelController") -def test_cli_bootstraps_for_config_models_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: - """CLI bootstrap runs before config models command execution.""" - mock_controller = MagicMock() - mock_controller_cls.return_value = mock_controller - - result = runner.invoke(app, ["config", "models"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_called_once_with() - mock_controller.run.assert_called_once() - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -@patch("data_designer.cli.commands.download.DownloadController") -def test_cli_bootstraps_for_download_command(mock_controller_cls: MagicMock, mock_bootstrap: MagicMock) -> None: - """CLI bootstrap runs before download personas command execution.""" - mock_controller = MagicMock() - mock_controller_cls.return_value = mock_controller - - result = runner.invoke(app, ["download", "personas", "--list"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_called_once_with() - mock_controller.list_personas.assert_called_once() - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -def test_cli_help_does_not_bootstrap(mock_bootstrap: MagicMock) -> None: - """Top-level help remains side-effect free.""" - result = runner.invoke(app, ["--help"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_not_called() - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -def test_config_help_does_not_bootstrap(mock_bootstrap: MagicMock) -> None: - """Config group help remains side-effect free.""" - result = runner.invoke(app, ["config", "--help"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_not_called() - - -@patch("data_designer.cli.lazy_group.ensure_cli_default_model_settings") -def test_download_help_does_not_bootstrap(mock_bootstrap: MagicMock) -> None: - """Download group help remains side-effect free.""" - result = runner.invoke(app, ["download", "--help"]) - - assert result.exit_code == 0 - mock_bootstrap.assert_not_called() + assert call_order.mock_calls == [call.bootstrap(), call.app()] From cddb9785faf1bc5f890b10d13d3f8a9c76456e6a Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 13:00:57 -0400 Subject: [PATCH 5/9] refactor(cli): keep bootstrap ownership in main --- .../src/data_designer/cli/utils/config_loader.py | 3 --- .../data-designer/tests/cli/utils/test_config_loader.py | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/data-designer/src/data_designer/cli/utils/config_loader.py b/packages/data-designer/src/data_designer/cli/utils/config_loader.py index 04471ebc..9fe37b9f 100644 --- a/packages/data-designer/src/data_designer/cli/utils/config_loader.py +++ b/packages/data-designer/src/data_designer/cli/utils/config_loader.py @@ -8,7 +8,6 @@ from pathlib import Path from urllib.parse import urlparse -from data_designer.cli.runtime import ensure_cli_default_model_settings from data_designer.config.config_builder import DataDesignerConfigBuilder from data_designer.config.utils.io_helpers import VALID_CONFIG_FILE_EXTENSIONS, is_http_url @@ -40,8 +39,6 @@ def load_config_builder(config_source: str) -> DataDesignerConfigBuilder: Raises: ConfigLoadError: If the file cannot be loaded or is invalid. """ - ensure_cli_default_model_settings() - if is_http_url(config_source): return _load_from_config_url(config_source) diff --git a/packages/data-designer/tests/cli/utils/test_config_loader.py b/packages/data-designer/tests/cli/utils/test_config_loader.py index 683766be..61cf9bc2 100644 --- a/packages/data-designer/tests/cli/utils/test_config_loader.py +++ b/packages/data-designer/tests/cli/utils/test_config_loader.py @@ -292,13 +292,11 @@ def test_load_config_builder_empty_yaml(tmp_path: Path) -> None: @patch("data_designer.cli.utils.config_loader.DataDesignerConfigBuilder.from_config") -@patch("data_designer.cli.utils.config_loader.ensure_cli_default_model_settings") -def test_load_config_builder_uses_shared_cli_bootstrap( - mock_ensure_bootstrap: MagicMock, +def test_load_config_builder_delegates_to_from_config_without_bootstrap( mock_from_config: MagicMock, tmp_path: Path, ) -> None: - """Config loader delegates default setup to the shared CLI bootstrap helper.""" + """Config loader focuses on loading config files without CLI bootstrap side effects.""" yaml_file = tmp_path / "config.yaml" yaml_file.write_text("data_designer:\n columns: []\n") mock_builder = MagicMock() @@ -306,6 +304,5 @@ def test_load_config_builder_uses_shared_cli_bootstrap( result = load_config_builder(str(yaml_file)) - mock_ensure_bootstrap.assert_called_once_with() mock_from_config.assert_called_once_with(yaml_file) assert result is mock_builder From db0a66fcc31c2f092c553386fd91e2fb3bc78a4b Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 13:06:29 -0400 Subject: [PATCH 6/9] test(cli): cover lazy dispatch and runtime failure flag --- packages/data-designer/tests/cli/test_main.py | 24 ++++++++++++++++++- .../data-designer/tests/cli/test_runtime.py | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/data-designer/tests/cli/test_main.py b/packages/data-designer/tests/cli/test_main.py index 2b25e29f..7a4ec555 100644 --- a/packages/data-designer/tests/cli/test_main.py +++ b/packages/data-designer/tests/cli/test_main.py @@ -5,7 +5,12 @@ from unittest.mock import Mock, call, patch -from data_designer.cli.main import main +from typer.testing import CliRunner + +from data_designer.cli.main import app, main +from data_designer.config.utils.constants import DEFAULT_NUM_RECORDS + +runner = CliRunner() @patch("data_designer.cli.main.app") @@ -19,3 +24,20 @@ def test_main_bootstraps_before_running_app(mock_bootstrap: Mock, mock_app: Mock main() assert call_order.mock_calls == [call.bootstrap(), call.app()] + + +@patch("data_designer.cli.commands.create.GenerationController") +def test_app_dispatches_lazy_create_command(mock_controller_cls: Mock) -> None: + """The Typer app dispatches lazy-loaded commands through the resolved callback.""" + mock_controller = Mock() + mock_controller_cls.return_value = mock_controller + + result = runner.invoke(app, ["create", "config.yaml"]) + + assert result.exit_code == 0 + mock_controller.run_create.assert_called_once_with( + config_source="config.yaml", + num_records=DEFAULT_NUM_RECORDS, + dataset_name="dataset", + artifact_path=None, + ) diff --git a/packages/data-designer/tests/cli/test_runtime.py b/packages/data-designer/tests/cli/test_runtime.py index a5f89539..3da14513 100644 --- a/packages/data-designer/tests/cli/test_runtime.py +++ b/packages/data-designer/tests/cli/test_runtime.py @@ -38,3 +38,4 @@ def test_ensure_cli_default_model_settings_warns_and_continues(monkeypatch: pyte assert "The command will continue." in warning assert "data-designer config providers" in warning assert "data-designer config models" in warning + assert runtime_mod._default_model_settings_checked is True From 4bfb63df06b6dc23addf5a11cd52f6e26b138662 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 13:24:22 -0400 Subject: [PATCH 7/9] refactor(cli): remove redundant bootstrap state --- .../src/data_designer/cli/runtime.py | 8 ------- .../data-designer/tests/cli/test_runtime.py | 21 +++++++------------ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/data-designer/src/data_designer/cli/runtime.py b/packages/data-designer/src/data_designer/cli/runtime.py index 81994811..23afd3e0 100644 --- a/packages/data-designer/src/data_designer/cli/runtime.py +++ b/packages/data-designer/src/data_designer/cli/runtime.py @@ -6,15 +6,9 @@ from data_designer.cli.ui import print_warning from data_designer.config.default_model_settings import resolve_seed_default_model_settings -_default_model_settings_checked = False - def ensure_cli_default_model_settings() -> None: """Best-effort bootstrap for CLI default model settings.""" - global _default_model_settings_checked - if _default_model_settings_checked: - return - try: resolve_seed_default_model_settings() except Exception as e: @@ -24,5 +18,3 @@ def ensure_cli_default_model_settings() -> None: "You will need to configure providers and models manually with " "`data-designer config providers` and `data-designer config models`." ) - finally: - _default_model_settings_checked = True diff --git a/packages/data-designer/tests/cli/test_runtime.py b/packages/data-designer/tests/cli/test_runtime.py index 3da14513..0eb0578a 100644 --- a/packages/data-designer/tests/cli/test_runtime.py +++ b/packages/data-designer/tests/cli/test_runtime.py @@ -5,27 +5,23 @@ from unittest.mock import patch -import pytest - import data_designer.cli.runtime as runtime_mod -def test_ensure_cli_default_model_settings_runs_once(monkeypatch: pytest.MonkeyPatch) -> None: - """CLI bootstrap only attempts default setup once per process.""" - monkeypatch.setattr(runtime_mod, "_default_model_settings_checked", False) - - with patch("data_designer.cli.runtime.resolve_seed_default_model_settings") as mock_resolve: - runtime_mod.ensure_cli_default_model_settings() +def test_ensure_cli_default_model_settings_attempts_default_setup() -> None: + """CLI bootstrap delegates to default setup when the CLI starts.""" + with ( + patch("data_designer.cli.runtime.print_warning") as mock_print_warning, + patch("data_designer.cli.runtime.resolve_seed_default_model_settings") as mock_resolve, + ): runtime_mod.ensure_cli_default_model_settings() mock_resolve.assert_called_once_with() - assert runtime_mod._default_model_settings_checked is True + mock_print_warning.assert_not_called() -def test_ensure_cli_default_model_settings_warns_and_continues(monkeypatch: pytest.MonkeyPatch) -> None: +def test_ensure_cli_default_model_settings_warns_and_continues() -> None: """CLI bootstrap prints an actionable warning when setup fails.""" - monkeypatch.setattr(runtime_mod, "_default_model_settings_checked", False) - with ( patch("data_designer.cli.runtime.print_warning") as mock_print_warning, patch("data_designer.cli.runtime.resolve_seed_default_model_settings", side_effect=RuntimeError("boom")), @@ -38,4 +34,3 @@ def test_ensure_cli_default_model_settings_warns_and_continues(monkeypatch: pyte assert "The command will continue." in warning assert "data-designer config providers" in warning assert "data-designer config models" in warning - assert runtime_mod._default_model_settings_checked is True From e6df11b30e4451668a463821267c40eaecc007e4 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 13:29:33 -0400 Subject: [PATCH 8/9] test(cli): assert bootstrap warning includes error --- packages/data-designer/tests/cli/test_runtime.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/data-designer/tests/cli/test_runtime.py b/packages/data-designer/tests/cli/test_runtime.py index 0eb0578a..23142891 100644 --- a/packages/data-designer/tests/cli/test_runtime.py +++ b/packages/data-designer/tests/cli/test_runtime.py @@ -24,13 +24,18 @@ def test_ensure_cli_default_model_settings_warns_and_continues() -> None: """CLI bootstrap prints an actionable warning when setup fails.""" with ( patch("data_designer.cli.runtime.print_warning") as mock_print_warning, - patch("data_designer.cli.runtime.resolve_seed_default_model_settings", side_effect=RuntimeError("boom")), + patch( + "data_designer.cli.runtime.resolve_seed_default_model_settings", + side_effect=RuntimeError("boom"), + ) as mock_resolve, ): runtime_mod.ensure_cli_default_model_settings() + mock_resolve.assert_called_once_with() mock_print_warning.assert_called_once() warning = mock_print_warning.call_args[0][0] assert "Could not initialize default model providers and model configs automatically." in warning assert "The command will continue." in warning + assert "boom" in warning assert "data-designer config providers" in warning assert "data-designer config models" in warning From 9fb5b3867a53953e6f7ee94a0a479c24ebc21b9f Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 12 Mar 2026 14:50:45 -0400 Subject: [PATCH 9/9] test: address cli bootstrap review feedback --- .../src/data_designer/cli/runtime.py | 6 +++++- .../tests/cli/utils/test_config_loader.py | 17 ----------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/data-designer/src/data_designer/cli/runtime.py b/packages/data-designer/src/data_designer/cli/runtime.py index 23afd3e0..aabdb920 100644 --- a/packages/data-designer/src/data_designer/cli/runtime.py +++ b/packages/data-designer/src/data_designer/cli/runtime.py @@ -8,7 +8,11 @@ def ensure_cli_default_model_settings() -> None: - """Best-effort bootstrap for CLI default model settings.""" + """Best-effort bootstrap for CLI default model settings. + + Repeated calls are safe because ``resolve_seed_default_model_settings()`` + only writes missing files/directories. + """ try: resolve_seed_default_model_settings() except Exception as e: diff --git a/packages/data-designer/tests/cli/utils/test_config_loader.py b/packages/data-designer/tests/cli/utils/test_config_loader.py index 61cf9bc2..e008290b 100644 --- a/packages/data-designer/tests/cli/utils/test_config_loader.py +++ b/packages/data-designer/tests/cli/utils/test_config_loader.py @@ -289,20 +289,3 @@ def test_load_config_builder_empty_yaml(tmp_path: Path) -> None: with pytest.raises(ConfigLoadError, match="Failed to load config from"): load_config_builder(str(yaml_file)) - - -@patch("data_designer.cli.utils.config_loader.DataDesignerConfigBuilder.from_config") -def test_load_config_builder_delegates_to_from_config_without_bootstrap( - mock_from_config: MagicMock, - tmp_path: Path, -) -> None: - """Config loader focuses on loading config files without CLI bootstrap side effects.""" - yaml_file = tmp_path / "config.yaml" - yaml_file.write_text("data_designer:\n columns: []\n") - mock_builder = MagicMock() - mock_from_config.return_value = mock_builder - - result = load_config_builder(str(yaml_file)) - - mock_from_config.assert_called_once_with(yaml_file) - assert result is mock_builder