DEV Community

Dakota Huang
Dakota Huang

Posted on

Pin Exception Class and Sibling Files Before One Loop Extract

Pin Exception Class and Sibling Files Before One Loop Extract

A loop extract is unsafe until failure atomicity is pinned. Partial files and exception class must stay stable. Source shape can change only after those pins pass.

A tidy helper can still break the crash contract. The caller sees a new type, or a deleted sibling. Both count as behavior changes, not as style changes.

What this pass locks

Mid-batch failure has three facts you can assert. Sibling files must still exist after the error. The fault row file must stay absent or present as today.

The exception class must escape unchanged to the caller. Message text is a weaker pin than class. Handler wording drifts when a model rewrites copy.

Pin the class, then one stable message token. Do not pin full tracebacks in this pass. Paths and line numbers change during an extract.

Those pins fail even when behavior stays correct. Do not pin wall-clock time in this pass. This article locks files and exception types only.

Why a shorter function is not the gate

A shorter function is not a passing test. Talk about clear code is only adjacent to this pass. Clarity matters after the crash contract is frozen.

This pass does not delete comments or rename for taste. It extracts one loop body and nothing else. If the diff touches policy, reject the diff.

Fixture status

The module below is a proposal, not a captured run. No timings, quotas, or customer results are claimed. Execute it locally before you trust a result.

Label the tree as unexecuted example code only. Replace names if your batch uses other types. Keep the three pins even if names change.

Step 1. Freeze the messy batch as it behaves now

Keep the current fault behavior, even if it looks wrong. Characterization locks today, then a later change can fix it. Fixing atomicity inside an extract hides two edits.

# proposal: unexecuted example, not a measured production module
from pathlib import Path

class BatchError(Exception):
    """Current public error type. Callers must not wrap it."""

def write_batch(rows, out_dir):
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)
    written = []
    for row in rows:
        if row.get("bad"):
            raise BatchError("row rejected")
        path = out / f"{row['id']}.txt"
        path.write_text(row["body"], encoding="utf-8")
        written.append(path)
        if row.get("explode_after_write"):
            raise BatchError("post-write fault")
    return written
Enter fullscreen mode Exit fullscreen mode

Row one writes a.txt and that file survives. Row two writes b.txt and then raises BatchError. The partial file remains, because nothing deletes it.

That leftover is the contract until you schedule a fix. A later ticket can delete it under a new test. This ticket must not smuggle that deletion in.

Step 2. Pin siblings, bytes, and exception class

Use a temp directory so the test owns the files. Assert presence, exact bytes, and the exception class. Also assert that no third output file appears.

# proposal: unexecuted characterization test
import pytest

def test_post_write_fault_keeps_siblings_and_class(tmp_path):
    rows = [
        {"id": "a", "body": "alpha"},
        {"id": "b", "body": "beta", "explode_after_write": True},
        {"id": "c", "body": "gamma"},
    ]
    with pytest.raises(BatchError, match="post-write fault") as caught:
        write_batch(rows, tmp_path)
    assert type(caught.value) is BatchError
    assert (tmp_path / "a.txt").read_text(encoding="utf-8") == "alpha"
    assert (tmp_path / "b.txt").read_text(encoding="utf-8") == "beta"
    assert not (tmp_path / "c.txt").exists()
    names = sorted(p.name for p in tmp_path.iterdir())
    assert names == ["a.txt", "b.txt"]
Enter fullscreen mode Exit fullscreen mode

The exact type check rejects a wrapper subclass. A bare raises clause also accepts unexpected subclasses. Keep the strict check if callers use exact type tests.

The match argument is a regex substring, not full equality. A rewritten sentence can still contain the token. Pin the token, not the whole sentence layout.

Add a second case for the reject-before-write path. The bad row must not create a file. Earlier sibling files must still remain on disk.

def test_reject_before_write_creates_no_partial(tmp_path):
    rows = [
        {"id": "a", "body": "alpha"},
        {"id": "b", "body": "beta", "bad": True},
    ]
    with pytest.raises(BatchError, match="row rejected"):
        write_batch(rows, tmp_path)
    assert (tmp_path / "a.txt").read_text(encoding="utf-8") == "alpha"
    assert not (tmp_path / "b.txt").exists()
Enter fullscreen mode Exit fullscreen mode

Run both characterization tests before any extract begins. Record the command and the pass count in the change note. Do not record a duration as a product claim.

python -m pytest tests/test_batch_fault.py -q
Enter fullscreen mode Exit fullscreen mode

Two passed tests are the gate for this pass. A red pin means you stop the extract. Repair the fixture, not the production policy, unless policy is the ticket.

Step 3. Score every diff with a reject table

Use this table before you accept any helper extract. Each row is a reject rule, not a style note. If one row fails, discard the whole patch.

Check after the fault Required result Reject the extract when
Sibling a.txt Present, bytes alpha Missing, renamed, or rewritten
Fault file b.txt Present, bytes beta Deleted, truncated, or empty
Unreached c.txt Absent Created by eager prefetch
Exception class Exact BatchError Exception, OSError, or a wrapper
Message token post-write fault Token removed or translated
Extra paths None Logs, temps, or marker files
Success return Same path list order None or a changed order

File encoding stays utf-8 for this whole pass. Newline policy stays the default behavior of write_text. Do not mix a newline ticket into the extract.

Step 4. Extract only the item write

The smallest safe change is one private function. The loop, the error type, and the writes stay. No cleanup, no retry, and no new dependency.

# proposal: smallest extract, still unexecuted
def _write_one(out, row):
    if row.get("bad"):
        raise BatchError("row rejected")
    path = out / f"{row['id']}.txt"
    path.write_text(row["body"], encoding="utf-8")
    if row.get("explode_after_write"):
        raise BatchError("post-write fault")
    return path

def write_batch(rows, out_dir):
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)
    written = []
    for row in rows:
        written.append(_write_one(out, row))
    return written
Enter fullscreen mode Exit fullscreen mode

Notice the fault still happens after the write. The partial file still remains for the caller. That is intentional until a new test expects deletion.

Step 5. Draft on a free model, rerun on a free server

A free MonkeyCode model can draft that extract. A free server can rerun the same two tests. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Treat both options as current availability claims, not as a quota promise. Recheck the offer on the day you run. Do not assume a named model, a hardware size, or a time limit.

Give the model the tests and the table, not a vague clean-up prompt. Ask for a diff that keeps every required result. Reject any patch that adds deletion or a new exception type.

proposal prompt, not a measured session:
Keep write_batch behavior identical.
Extract _write_one only.
Do not delete partial files.
Do not wrap BatchError.
Do not add dependencies.
Tests in tests/test_batch_fault.py must stay green.
Enter fullscreen mode Exit fullscreen mode

Run the suite on the free server only if the fixture has no secrets. Copy the same pytest command you used locally. Compare the pass count, not a leaderboard score.

If the remote tree lacks pytest, install it in that job and stop. Do not widen the task into a framework upgrade. A missing tool is a setup failure, not a behavior change.

Step 6. Review the draft like any other patch

Read the diff before you run the remote job if the patch is large. A five-line extract should not touch imports beyond Path. New try blocks are a smell in this pass.

Reject these common helpful edits before you run. A finally block that unlinks b.txt is a reject. A raise that wraps BatchError is a reject.

A switch from write_text to binary mode is a reject. A prefetch that creates c.txt early is a reject. Also reject drive-by renames of the BatchError type.

Existing callers may catch that exact name today. A rename is a second change with its own pin. If tests pass and the table matches, keep the extract.

If tests pass but a new file has no row, stop. Add a table row before you accept that file. Green tests can still miss an unlisted path.

What these two tests do not prove

These two tests do not prove thread safety. These two tests do not prove disk-full behavior. These two tests do not prove cross-platform path rules.

write_text replacement rules differ by platform line endings. This fixture uses short ASCII bodies without newline checks. Add a newline pin before you touch encoding.

The remote job filesystem may be case-sensitive now. Your laptop filesystem may not be case-sensitive at all. A pin on A.txt versus a.txt can disagree across machines.

Remote runs can omit your local environment variables. That omission helps only when the batch must not read env. If your real batch reads env, pin that map in another test.

No benchmark is claimed for model speed or server speed. Pass count is the only result this workflow records. A faster reply is not a safer extract.

Who should skip this pass

Skip this pass when the ticket is to change atomicity. You need a new test that expects deletion or rename. Pinning current leftovers would block the intended fix.

Skip the free server when inputs contain secrets or private data. A characterization fixture should use synthetic rows only. Customer files do not belong in a demo prompt.

Skip model drafts when you cannot read the diff. The tests catch many wrapper bugs, not all of them. An unread patch is still an unread patch.

Skip this pass if commit order is the real contract. Database commit order needs its own characterization test. File leftovers are a poor stand-in for rollbacks.

Close

Freeze siblings, bytes, and exception class before the loop moves. Extract one writer only after both pins pass. Schedule atomicity fixes as a later, separate change.

When the pins exist, MonkeyCode's free model can draft the extract. Its free server can rerun the suite on synthetic fixtures. Recheck the current offer, then keep only a diff that stays green.

Top comments (0)