Skip to content

Fix atomic writes replacing destinations after command failures - #1918

Open
wterrr wants to merge 1 commit into
fastapi:masterfrom
wterrr:fix/atomic-write-rollback
Open

Fix atomic writes replacing destinations after command failures#1918
wterrr wants to merge 1 commit into
fastapi:masterfrom
wterrr:fix/atomic-write-rollback

Conversation

@wterrr

@wterrr wterrr commented Aug 2, 2026

Copy link
Copy Markdown

Summary

  • register atomic file parameters as exception-aware context resources
  • forward callback exceptions through lazy atomic files
  • discard temporary output instead of replacing the destination after failures
  • add regression tests for lazy and eager files

Problem

File parameters configured with atomic=True replace the destination even
when the command callback raises an exception after writing partial content.

This defeats the purpose of atomic output and can replace an existing valid
file with incomplete data.

The issue affects both lazy=True and lazy=False.

Fix

Atomic files are now registered with Context.with_resource() so their
context managers receive the active exception.

LazyFile forwards the exception information to its underlying atomic
stream, and _AtomicFile.close(delete=True) removes the temporary file
instead of replacing the destination.

Non-atomic file cleanup behavior is unchanged.

Tests

Regression coverage verifies both lazy modes when:

  • the destination already exists
  • the destination does not exist
  • the command writes and flushes partial content before raising
  • the original callback exception is preserved
  • no temporary file is leaked

Validation results:

  • atomic file tests: 10 passed
  • full test suite: 1390 passed, 12 skipped, 2 xfailed
  • Ruff lint and formatting checks passed

Related upstream report: pallets/click#3221

@kramlipi kramlipi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated code review by kramlipi code-agent.

Review notes from kramlipi code-agent (@kramlipi_code_review_bot) — grounded scanners + critic, not a rubber stamp.

Free for open-source / public repos. Ready-made GitHub Actions: code-review CI setup · Product: www.kramlipi.com · Docs: https://kramlipi.github.io/

Comment thread typer/_click/_compat.py
return # pragma: no cover
self._f.close()
os.replace(self._tmp_filename, self._real_filename)
if delete:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix · reliability · typer/_click/_compat.py:419 · Potential resource leak on os.replace failure

What: The close method uses os.replace without ensuring the temporary file is cleaned up if the replacement fails (e.g., cross-device link error or permission issue).
Why it matters: Leaving orphaned temporary files in the filesystem can lead to disk space exhaustion or security issues if sensitive data is left in a predictable location.
Do this: Wrap os.replace in a try/finally block to ensure os.unlink(self._tmp_filename) is called if the replacement fails.

Suggestion:

Suggested change
if delete:
try:
os.replace(self._tmp_filename, self._real_filename)
finally:
if os.path.exists(self._tmp_filename):
os.unlink(self._tmp_filename)

Callers that may care:

  • typer/testing.py:33__init__super().__init__()
  • typer/testing.py:65__init__super().__init__(buffer, **kwargs)
  • tests/test_prepare_release.py:34__init__new_content = update_version_file(content, "0.26.3", Path("typer/__init__.py"))
  • tests/test_prepare_release.py:43__init__update_version_file(content, "0.26.2", Path("typer/__init__.py"))
  • pyproject.toml:92__init__version = { source = "file", path = "typer/__init__.py" }

Comment thread tests/test_atomic_file.py
from pathlib import Path

import pytest
import typer

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a look · consistency · tests/test_atomic_file.py:6 · PR title/diff inconsistency

What: PR title emphasizes topics that do not appear in changed paths or added code (destinations, failures, replacing, writes).

Why it matters: Align the message or the diff. (scanner:commit_consistency)

Do this: Either fix it in this PR, or reply here explaining why the current behavior is intentional.

Evidence: title='Fix atomic writes replacing destinations after command failures'; missing_in_diff=['destinations', 'failures', 'replacing', 'writes']

Comment thread typer/_click/types.py

if ctx is not None:
ctx.call_on_close(lf.close_intelligently)
if self.atomic:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a look · design · typer/_click/types.py:542 · No behavior risk: api_signature

What: No behavior risk: api_signature

Why it matters: The changes to File.convert and _AtomicFile logic are internal implementation details of how resources are managed within the Click context, and do not alter the public API signature of File or ParamType.

Do this: Either fix it in this PR, or reply here explaining why the current behavior is intentional.

Evidence: typer/_click/types.py:542

Callers that may care:

  • typer/testing.py:33__init__super().__init__()
  • typer/testing.py:65__init__super().__init__(buffer, **kwargs)
  • tests/test_prepare_release.py:34__init__new_content = update_version_file(content, "0.26.3", Path("typer/__init__.py"))
  • tests/test_prepare_release.py:43__init__update_version_file(content, "0.26.2", Path("typer/__init__.py"))
  • pyproject.toml:92__init__version = { source = "file", path = "typer/__init__.py" }

@kramlipi

kramlipi commented Aug 5, 2026

Copy link
Copy Markdown

Skipped findings (line not in PR diff or over max_comments)

  • typer/_click/_compat.py:404 [info] No blast-radius risk

Review notes from kramlipi code-agent (@kramlipi_code_review_bot) — grounded scanners + critic, not a rubber stamp.

Free for open-source / public repos. Ready-made GitHub Actions: code-review CI setup · Product: www.kramlipi.com · Docs: https://kramlipi.github.io/

@kramlipi

kramlipi commented Aug 5, 2026

Copy link
Copy Markdown

Review notes

3 note(s) — 0 must-fix · 1 should-fix · 2 worth a look · 1 from deterministic scanners

Themes: reliability (1), consistency (1), design (1)

Start here:

  • typer/_click/_compat.py:419 (should-fix) — Potential resource leak on os.replace failure
  • tests/test_atomic_file.py:6 (look) — PR title/diff inconsistency
  • typer/_click/types.py:542 (look) — No behavior risk: api_signature

Review notes from kramlipi code-agent (@kramlipi_code_review_bot) — grounded scanners + critic, not a rubber stamp.

Free for open-source / public repos. Ready-made GitHub Actions: code-review CI setup · Product: www.kramlipi.com · Docs: https://kramlipi.github.io/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants