diff --git a/src/profiles/app.py b/src/profiles/app.py index e704ef0..11abf63 100644 --- a/src/profiles/app.py +++ b/src/profiles/app.py @@ -523,13 +523,11 @@ def init_default_config(dest: Path | None = None) -> Path: ) sys.exit(1) - from profiles.core.config.template import STARTER_CONFIG_TEMPLATE + from profiles.core.actions import ActionStatus, write_starter_config - body = STARTER_CONFIG_TEMPLATE.format(cwd=str(Path.cwd())) - try: - target.write_text(body, encoding="utf-8") - except OSError as exc: - print(f"Error: Could not write configuration file: {exc}", file=sys.stderr) + result = write_starter_config(target) + if result.status is not ActionStatus.SUCCESS: + print(f"Error: Could not write configuration file: {result.message}", file=sys.stderr) sys.exit(1) print(f"Default configuration created: {target}") diff --git a/src/profiles/core/actions.py b/src/profiles/core/actions.py index 4cb7219..74dd8f5 100644 --- a/src/profiles/core/actions.py +++ b/src/profiles/core/actions.py @@ -17,6 +17,7 @@ from enum import Enum from pathlib import Path +from profiles.core.config.io.yaml_io import write_text_atomic from profiles.core.config.models import AppConfig, WorkflowStep from profiles.core.config.template import STARTER_CONFIG_TEMPLATE from profiles.core.environment.matcher import select_most_specific_pattern @@ -296,26 +297,29 @@ def launch_selected_file( def write_starter_config( path: Path, *, + cwd: Path | str | None = None, logger: logging.Logger | None = None, ) -> ActionResult: """Write the documented starter ``.profiles`` to *path*. The parent directory must already exist. The template embeds - ``str(Path.cwd())`` as the default ``search_dir`` so the GUI's - Directory field always has a usable fallback. + ``str(Path.cwd())`` (or *cwd* if provided) as the default ``search_dir`` + so the GUI's Directory field always has a usable fallback. Args: path: Target path for the new ``.profiles`` file. + cwd: Optional directory for template substitution (default: CWD). logger: Optional logger used to record the write. Returns: ``ActionResult`` describing the outcome. """ target = Path(path) - body = STARTER_CONFIG_TEMPLATE.format(cwd=str(Path.cwd())) + search_cwd = str(cwd) if cwd is not None else str(Path.cwd()) + body = STARTER_CONFIG_TEMPLATE.format(cwd=search_cwd) try: - target.write_text(body, encoding="utf-8") + write_text_atomic(target, body) except OSError as exc: if logger is not None: events.config_create_failed(logger, error=str(exc)) diff --git a/src/profiles/core/config/loader.py b/src/profiles/core/config/loader.py index 832b17e..e0f59cd 100644 --- a/src/profiles/core/config/loader.py +++ b/src/profiles/core/config/loader.py @@ -72,16 +72,15 @@ def propose_config_creation() -> bool: print(f"Warning: {target} already exists. Creation skipped.") return False - from profiles.core.config.template import STARTER_CONFIG_TEMPLATE + from profiles.core.actions import ActionStatus, write_starter_config - body = STARTER_CONFIG_TEMPLATE.format(cwd=str(Path.cwd())) - try: - target.write_text(body, encoding="utf-8") + result = write_starter_config(target) + if result.status is ActionStatus.SUCCESS: print(f"\n✓ Default configuration created: {target}") print("\nYou can now customize this file to suit your needs.") return True - except OSError as exc: - print(f"\n✗ Error creating configuration file: {exc}", file=sys.stderr) + else: + print(f"\n✗ Error creating configuration file: {result.message}", file=sys.stderr) return False print("\nContinuing with default settings.") diff --git a/tests/core/config/test_loader.py b/tests/core/config/test_loader.py index ecc3a67..d98f8b8 100644 --- a/tests/core/config/test_loader.py +++ b/tests/core/config/test_loader.py @@ -135,13 +135,8 @@ def test_write_error_returns_false( monkeypatch.chdir(tmp_path) monkeypatch.setattr("builtins.input", lambda: "y") - real_write = Path.write_text - - def failing_write(self, *args, **kwargs): # noqa: ANN001 + def failing_write(*args, **kwargs): # noqa: ANN001 raise OSError("denied") - monkeypatch.setattr(Path, "write_text", failing_write) - try: - assert propose_config_creation() is False - finally: - monkeypatch.setattr(Path, "write_text", real_write) + monkeypatch.setattr("profiles.core.actions.write_text_atomic", failing_write) + assert propose_config_creation() is False diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index 77f6b04..75e87c4 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -214,6 +214,27 @@ def test_writes_documented_file(self, tmp_path: Path) -> None: assert config.configurations assert config.configurations[0].match.hostname == ("*",) + def test_uses_atomic_write(self, tmp_path: Path, mocker) -> None: + """write_starter_config delegates to write_text_atomic for atomicity.""" + target = tmp_path / ".profiles" + mock_atomic = mocker.patch("profiles.core.actions.write_text_atomic") + + result = write_starter_config(target) + + assert result.status is ActionStatus.SUCCESS + mock_atomic.assert_called_once() + assert mock_atomic.call_args[0][0] == target + + def test_writes_with_custom_cwd(self, tmp_path: Path) -> None: + """Custom cwd is embedded in search_dir instead of Path.cwd().""" + target = tmp_path / ".profiles" + custom_dir = tmp_path / "custom_workdir" + result = write_starter_config(target, cwd=custom_dir) + + assert result.status is ActionStatus.SUCCESS + body = target.read_text(encoding="utf-8") + assert str(custom_dir) in body + class TestSaveCsvExport: """Tests for save_csv_export().""" diff --git a/tests/test_app.py b/tests/test_app.py index cd0bb57..3ee8b53 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -572,6 +572,23 @@ def test_default_dest_is_cwd(self, tmp_path: Path, monkeypatch) -> None: assert result == tmp_path / ".profiles" assert result.exists() + def test_delegates_to_write_starter_config(self, tmp_path: Path, mocker) -> None: + """init_default_config delegates writing to write_starter_config.""" + from profiles.core.actions import ActionResult, ActionStatus + target = tmp_path / ".profiles" + mock_writer = mocker.patch( + "profiles.core.actions.write_starter_config", + return_value=ActionResult( + status=ActionStatus.SUCCESS, + message="written", + path=target, + ), + ) + + result = init_default_config(dest=tmp_path) + assert result == target + mock_writer.assert_called_once_with(target) + # ── main() ──────────────────────────────────────────────────────────────────