generated from john/python-template
This commit is contained in:
@@ -6,6 +6,9 @@ import os
|
|||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from errno import EBUSY
|
||||||
|
from errno import EPERM
|
||||||
|
from errno import EXDEV
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
@@ -518,6 +521,7 @@ def _upsert_env_key(lines: list[str], key: str, value: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
|
def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
|
||||||
|
temp_path: Path | None = None
|
||||||
try:
|
try:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
content = "\n".join(lines).rstrip("\n")
|
content = "\n".join(lines).rstrip("\n")
|
||||||
@@ -533,7 +537,15 @@ def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
|
|||||||
) as handle:
|
) as handle:
|
||||||
temp_path = Path(handle.name)
|
temp_path = Path(handle.name)
|
||||||
handle.write(content)
|
handle.write(content)
|
||||||
temp_path.replace(path)
|
try:
|
||||||
|
temp_path.replace(path)
|
||||||
|
except OSError as exc:
|
||||||
|
# Single-file bind mounts can reject replace() (cross-device or busy mountpoint).
|
||||||
|
# Fallback to direct write so Runtime Settings can persist to mounted env files.
|
||||||
|
if exc.errno not in {EXDEV, EBUSY, EPERM}:
|
||||||
|
raise
|
||||||
|
with path.open("w", encoding="utf-8", newline="\n") as handle:
|
||||||
|
handle.write(content)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise AppError(
|
raise AppError(
|
||||||
"Runtime settings file is not writable.",
|
"Runtime settings file is not writable.",
|
||||||
@@ -541,6 +553,9 @@ def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
|
|||||||
suggestion="Verify file path and write permissions, then retry.",
|
suggestion="Verify file path and write permissions, then retry.",
|
||||||
detail=f"Failed writing runtime env file {path}: {type(exc).__name__}: {exc}",
|
detail=f"Failed writing runtime env file {path}: {type(exc).__name__}: {exc}",
|
||||||
) from exc
|
) from exc
|
||||||
|
finally:
|
||||||
|
if temp_path is not None:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def _escape_json_string(value: str) -> str:
|
def _escape_json_string(value: str) -> str:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import errno
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -132,3 +133,18 @@ def test_save_runtime_settings_surfaces_unwritable_target(tmp_path: Path, monkey
|
|||||||
with pytest.raises(AppError) as exc:
|
with pytest.raises(AppError) as exc:
|
||||||
save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
|
save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
|
||||||
assert exc.value.message == "Runtime settings file is not writable."
|
assert exc.value.message == "Runtime settings file is not writable."
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_runtime_settings_falls_back_when_atomic_replace_is_unavailable(tmp_path: Path, monkeypatch):
|
||||||
|
settings = _settings_for_runtime_editing(tmp_path)
|
||||||
|
env_path = tmp_path / ".env.production"
|
||||||
|
env_path.write_text("OPENROUTER_API_KEY=test-key\nPORT=8000\n", encoding="utf-8")
|
||||||
|
|
||||||
|
def _replace_cross_device(_self: Path, _target: Path) -> Path:
|
||||||
|
raise OSError(errno.EXDEV, "Invalid cross-device link")
|
||||||
|
|
||||||
|
monkeypatch.setattr(Path, "replace", _replace_cross_device)
|
||||||
|
snapshot = save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
|
||||||
|
|
||||||
|
assert snapshot.env_file_path == env_path
|
||||||
|
assert "PORT=9001" in env_path.read_text(encoding="utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user