Conversation
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (85.71%) is below the target coverage (98.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files
|
a94227a to
918dd9f
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
create can destructively truncate existing file-like objects; restrict truncation to recreate or reject that combination.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
Fixes uproot.recreate so existing files are physically truncated while preserving create/update behavior.
Changes:
- Adds
create,recreate, andupdatesink modes. - Validates options before opening or truncating files.
- Adds regression coverage for truncation and mode behavior.
| File | Summary |
|---|---|
tests/test_1697_recreate_truncates.py |
Tests truncation and mode behavior. |
src/uproot/writing/writable.py |
Shares create/recreate setup and validates options early. |
src/uproot/sink/file.py |
Implements mode-specific creation, truncation, and update behavior; create currently truncates file-like objects. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
a7517b1 to
6695e40
Compare
Before the fsspec migration (scikit-hep#1016, scikit-hep#1034), uproot.recreate opened the path with mode "w", truncating it. Afterwards the truncation moved into FileSink, which only calls _truncate_file when the file does *not* already exist, so recreate over a pre-existing file opened it "r+b" and left every byte past the new fEND in place. Recreating a 100 kB path as an empty ROOT file left the physical size at 100 kB with fEND at 1658. Truncate the path in recreate, restoring the documented "RECREATE" semantics. uproot.update is unaffected and keeps its "r+b" behavior, and uproot.create still raises FileExistsError before reaching this point. Recreating from a file-like object is unchanged, as it was before the migration too. Assisted-by: claude-code:claude-opus-5[1m]
FileSink now takes a mode ("create", "recreate", or "update"), so the
decision to truncate lives in one place and also covers file-like
objects. create uses exclusive creation ("xb"), falling back to an
existence check on filesystems that do not support it. Options are
validated before the sink is opened, so a misspelled option no longer
truncates an existing file before raising.
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
…files Truncating with fs.touch before opening the file "r+b" emptied an existing file on filesystems that can write but not open "r+b" (such as S3 and XRootD), and only then failed. Now the file is opened first and truncated through the handle, so such filesystems fail with the file intact, as before. Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
6695e40 to
df2ee9c
Compare

🤖 AI text below 🤖
Addresses finding 3 of "PR 1" in #1688. This is a regression, with a clear origin.
Before the fsspec migration,
uproot.recreatetruncated the path itself:#1016 / #1034 moved that into
FileSink.__init__, which only truncates when the file does not already exist:https://github.com/scikit-hep/uproot5/blob/main/src/uproot/sink/file.py#L52-L58
so recreating over an existing file opens it
"r+b"and leaves every byte past the newfENDin place. Recreating a 100 kB path as an empty ROOT file left the physical size at 100 kB withfENDat 1658.Changes
FileSinktakes amode:"create","recreate", or"update"(default, so existing callers are unchanged), mirroring ROOT'sTFileoptions. The truncate/create decision lives only there."recreate"truncates an existing path, and now also a file-like object, which previously kept its stale trailing bytes too.truncateis not required of file-like objects, so ones without it are written as before.An existing path is truncated through the file opened
"r+b", not withfs.touchbeforehand. Filesystems that can write whole files but not open them"r+b"(S3, XRootD) still fail as onmain, but with the file intact: truncating first would have emptied the file and then failed."create"creates the file with exclusive mode"xb", so a file that appears between the existence check and the creation is never overwritten (atomic on local filesystems). Filesystems that don't support"xb"fall back to an existence check. A file-like object has no path to check, so it is written into as is (not truncated), as onmain."update"keeps its"r+b"behavior.uproot.createanduproot.recreateshare one implementation, and unrecognized options are rejected before the sink is opened. Otherwiseuproot.recreate(path, compresion=...)would truncate the existing file and only then raiseTypeError.uproot.updatevalidates first as well.url_to_fs) instead of up to three times with repeatedexistschecks.Out of scope, but noticed along the way: extra keyword arguments meant as fsspec
storage_optionsare never popped fromoptions, socreate/recreate/updatealways raiseTypeErrorfor them (e.g.uproot.recreate(p, auto_mkdir=True)). I left that unchanged here; see #1729.