write-then-rename 原子写:先写到同目录的临时文件再 os.replace 重命名。 保证别的进程读 target 时只能看到完整旧内容或完整新内容,不会读到半截。 Windows 兼容:目标文件被另一进程持 read handle 时 os.replace 抛 PermissionError, 重试 3 次(50ms 间隔);最终失败保留 tmp 文件供手动收拾。
(target: Path, content: str, encoding: str = "utf-8")
| 24 | |
| 25 | |
| 26 | def _atomic_write_text(target: Path, content: str, encoding: str = "utf-8") -> None: |
| 27 | """write-then-rename 原子写:先写到同目录的临时文件再 os.replace 重命名。 |
| 28 | 保证别的进程读 target 时只能看到完整旧内容或完整新内容,不会读到半截。 |
| 29 | |
| 30 | Windows 兼容:目标文件被另一进程持 read handle 时 os.replace 抛 PermissionError, |
| 31 | 重试 3 次(50ms 间隔);最终失败保留 tmp 文件供手动收拾。 |
| 32 | """ |
| 33 | import time as _time |
| 34 | |
| 35 | target.parent.mkdir(parents=True, exist_ok=True) |
| 36 | tmp_fd, tmp_path = tempfile.mkstemp( |
| 37 | dir=str(target.parent), |
| 38 | prefix=f".{target.name}.", |
| 39 | suffix=".tmp", |
| 40 | ) |
| 41 | try: |
| 42 | with os.fdopen(tmp_fd, "w", encoding=encoding, newline="") as f: |
| 43 | f.write(content) |
| 44 | except Exception: |
| 45 | try: |
| 46 | os.unlink(tmp_path) |
| 47 | except OSError: |
| 48 | pass |
| 49 | raise |
| 50 | |
| 51 | last_exc: Exception | None = None |
| 52 | for _ in range(3): |
| 53 | try: |
| 54 | os.replace(tmp_path, str(target)) |
| 55 | return |
| 56 | except PermissionError as e: |
| 57 | last_exc = e |
| 58 | _time.sleep(0.05) |
| 59 | # 3 次都失败:清理 tmp 避免污染 git status;给 stderr 提示重试 |
| 60 | try: |
| 61 | os.unlink(tmp_path) |
| 62 | except OSError: |
| 63 | print( |
| 64 | f"[_atomic_write_text] 警告:临时文件无法清理:{tmp_path}", |
| 65 | file=sys.stderr, |
| 66 | ) |
| 67 | print( |
| 68 | f"[_atomic_write_text] os.replace 失败 3 次(目标 {target} 可能被另一进程持锁);" |
| 69 | f"请稍后重试", |
| 70 | file=sys.stderr, |
| 71 | ) |
| 72 | if last_exc is not None: |
| 73 | raise last_exc |
| 74 | |
| 75 | try: |
| 76 | from markitdown import MarkItDown |