Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/profiles/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
12 changes: 8 additions & 4 deletions src/profiles/core/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
11 changes: 5 additions & 6 deletions src/profiles/core/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
11 changes: 3 additions & 8 deletions tests/core/config/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions tests/core/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()."""
Expand Down
17 changes: 17 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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() ──────────────────────────────────────────────────────────────────

Expand Down
Loading