Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
9e96466
Add in-place TTree branch addition
Yokubas Jul 27, 2026
81ce9b3
Add in-place TTree branch addition with tests
Yokubas Jul 27, 2026
3f89df6
Add in-place TTree branch addition with tests
Yokubas Jul 27, 2026
56fd418
Add in-place TTree extend method
Yokubas Jul 27, 2026
73d82ab
Add in-place TTree extend and add_branches with tests
Yokubas Jul 27, 2026
990acb0
style: pre-commit fixes
pre-commit-ci[bot] Jul 27, 2026
fbe47d8
Rename test file to test_1690_ttree_inplace.py
Yokubas Jul 27, 2026
f91e25a
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Jul 27, 2026
cdf5d21
style: pre-commit fixes
pre-commit-ci[bot] Jul 27, 2026
daab666
Move imports to top level in writable.py
Yokubas Jul 27, 2026
1a990e0
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Jul 27, 2026
cf5532a
Add accept_new_fields kwarg to extend
Yokubas Jul 27, 2026
b0e7d43
style: pre-commit fixes
pre-commit-ci[bot] Jul 27, 2026
1b303df
Fix extend for multiple sessions and add basket overflow check
Yokubas Jul 27, 2026
9ab366c
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Jul 27, 2026
314ee2f
style: pre-commit fixes
pre-commit-ci[bot] Jul 27, 2026
2a2983d
Use self._file instead of opening uproot.update again
Yokubas Jul 28, 2026
3537114
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Jul 28, 2026
cbb47aa
Fix hardcoded byte range for fBranches TObjArray bcnt search
Yokubas Jul 28, 2026
36429fe
Find TTree fEntries more reliably using unique sequence
Yokubas Jul 28, 2026
ba54114
style: pre-commit fixes
pre-commit-ci[bot] Jul 28, 2026
f012ece
Validate branch length matches tree in add_branches
Yokubas Jul 28, 2026
55f965d
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Jul 28, 2026
6373215
Clean up tests and enforce all branches in extend
Yokubas Jul 28, 2026
f97c03d
style: pre-commit fixes
pre-commit-ci[bot] Jul 28, 2026
314fe05
Fix fEND write size for big files (>2GB)
Yokubas Jul 28, 2026
b7c62d1
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Jul 28, 2026
f34b48c
Merge remote-tracking branch 'upstream/main' into Yokubas/ttree-inpla…
Yokubas Aug 3, 2026
3cb7b1d
Use cascade machinery for extend on existing TTrees
Yokubas Aug 3, 2026
d7ae561
style: pre-commit fixes
pre-commit-ci[bot] Aug 3, 2026
e4c8a04
Merge remote-tracking branch 'upstream/main' into Yokubas/ttree-inpla…
Yokubas Aug 5, 2026
af5ca84
Rewrite add_branches using cascade machinery
Yokubas Aug 5, 2026
7f0327b
Merge branch 'Yokubas/ttree-inplace-v2' of https://github.com/Yokubas…
Yokubas Aug 5, 2026
0fb41a2
Remove dead _extend_inplace code and fully use cascade machinery
Yokubas Aug 6, 2026
8b906e5
Fix metadata_start and basket_metadata_start computation in _load_exi…
Yokubas Aug 6, 2026
69c7a40
Restore accidentally deleted WritableTree properties
Yokubas Aug 7, 2026
49ca9ed
Fix extend validation to skip counter and record branches
Yokubas Aug 7, 2026
7aa10f2
Merge remote-tracking branch 'upstream/main' into Yokubas/ttree-inpla…
Yokubas Aug 7, 2026
04d8bf1
Fix extend validation to handle counter, record, and jagged branches
Yokubas Aug 7, 2026
c99184b
Merge remote-tracking branch 'upstream/main' into Yokubas/ttree-inpla…
Yokubas Aug 10, 2026
84d300f
Update test_writable_vs_readable_tree to reflect new behavior
Yokubas Aug 10, 2026
e5edb36
Use existing tree title in _load_existing_ttree instead of empty string
Yokubas Aug 10, 2026
64cb73e
Fix basket_metadata_start formula for trees with fMaxBaskets != 10
Yokubas Aug 10, 2026
495e10a
Add test for extend after many extends (fMaxBaskets > 10)
Yokubas Aug 10, 2026
3c0dc35
Replace BytesIO approach with sink.read + _ReadForUpdate pattern
Yokubas Aug 10, 2026
ae1f135
Use fIsRange to detect counter branches instead of name pattern matching
Yokubas Aug 12, 2026
85a4281
Use key.cycle instead of hardcoded cycle number 1 in _load_existing_t…
Yokubas Aug 12, 2026
d0f2b8e
Fix subdirectory support in add_branches and _load_existing_ttree
Yokubas Aug 12, 2026
d790b77
Fix counter branch validation in extend
Yokubas Aug 13, 2026
ac16474
Merge remote-tracking branch 'upstream/main' into Yokubas/ttree-inpla…
Yokubas Aug 24, 2026
4fa64d3
fix: derive jagged branch dtype from interpretation content instead o…
Yokubas Aug 24, 2026
c8596a2
fix: locate TTree/branch metadata offsets in _load_existing_ttree via…
Yokubas Aug 24, 2026
5907548
fix: support extending string branches in uproot.update() by recogniz…
Yokubas Aug 24, 2026
2559189
fix: robustly skip TBranchElement branches in _load_existing_ttree in…
Yokubas Aug 24, 2026
d4d563e
fix: derive TTree/branch metadata offsets structurally instead of byt…
Yokubas Aug 24, 2026
0330d70
Merge remote-tracking branch 'upstream/main' into Yokubas/ttree-inpla…
Yokubas Aug 25, 2026
ae8ee2f
fix: correct fWriteBasket for brand-new branches in add_branches inst…
Yokubas Aug 25, 2026
872f9a3
docs: correct add_branches docstring claiming TBranchElement support …
Yokubas Aug 25, 2026
a1dd1df
fix: don't infer a counter branch for a string branch's coincidentall…
Yokubas Aug 25, 2026
5a4ace0
fix: move the WritableFile._trees cache entry when add_branches reloc…
Yokubas Aug 25, 2026
9dbba98
perf: reuse self._cascading in add_branches instead of unconditionall…
Yokubas Aug 25, 2026
a14d8b0
fix: force one full write_anew before extend's first incremental patc…
Yokubas Aug 26, 2026
79dde83
fix: refuse add_branches up front on a tree that already has more tha…
Yokubas Aug 26, 2026
9ea8902
fix: recognize non-record awkward Array values as flat-dict extend da…
Yokubas Aug 26, 2026
6a1e52b
fix: opportunistically flatten nested-record extend() input using the…
Yokubas Aug 27, 2026
51da612
docs: add missing license header to test_1690_ttree_inplace.py
Yokubas Aug 27, 2026
591e152
test: assert KeyInFileError specifically for add_branches on a nonexi…
Yokubas Aug 27, 2026
f2fac6a
test: assert the specific missing-branches ValueError for extend() wi…
Yokubas Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 74 additions & 12 deletions src/uproot/writing/_cascadetree.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def __init__(
self._field_name = field_name
self._basket_capacity = initial_basket_capacity
self._resize_factor = resize_factor
self._has_unsupported_branches = False
self._has_divergent_baskets = False
self._needs_relocation_before_extend = False

if isinstance(branch_types, dict):
branch_types_items = branch_types.items()
Expand Down Expand Up @@ -446,7 +449,55 @@ def num_entries(self):
def num_baskets(self):
return self._num_baskets

def _relocate(self, file, sink):
"""
Rewrites this tree's whole metadata blob from scratch via write_anew(),
relocating it (freeing its old space, allocating new space), and moves
the WritableFile._trees cache entry to match. Shared by the two
situations that need a full rewrite rather than an incremental
write_updates() patch: basket-capacity growth, and (see
_needs_relocation_before_extend) establishing Uproot's own canonical
layout for a tree write_updates() hasn't verified it can trust yet.
"""
# seek_location is the tree's actual physical on-disk position (it's
# what every sink.read(key.seek_location, ...) call in
# _load_existing_ttree uses), and what file._move_tree()/
# WritableFile._trees are keyed by everywhere else in the codebase.
# location coincides with it for any tree Uproot itself has written
# (all our own tests, hence never catching this), but can diverge for
# a preexisting (e.g. ROOT-written) tree's Key object -- silently
# freeing/keying the wrong byte range and cache entry.
oldloc = start = self._key.seek_location
stop = start + self._key.num_bytes + self._key.compressed_bytes

self.write_anew(sink)

newloc = self._key.seek_location
file._move_tree(oldloc, newloc)

self._freesegments.release(start, stop)
sink.set_file_length(self._freesegments.fileheader.end)
sink.flush()

def extend(self, file, sink, data):
if self._needs_relocation_before_extend:
# metadata_start/basket_metadata_start (computed structurally by
# _build_out(), see _load_existing_ttree) are only guaranteed
# correct for a tree actually laid out the way Uproot's own
# write_anew() lays one out. A tree loaded from an existing file
# may have been written by ROOT instead, with a byte-for-byte
# different (but semantically equivalent) layout Uproot's writer
# doesn't replicate -- write_updates() patching at Uproot's
# assumed offsets would then silently corrupt unrelated bytes.
# write_anew() here establishes Uproot's own canonical layout for
# real (the same thing add_branches() already always does, which
# is why it already works on ROOT-written files), so every
# write_updates() patch from here on is guaranteed to target the
# right bytes. Costs one full blob rewrite, but only once, on the
# first extend() after loading -- not on every call.
self._relocate(file, sink)
self._needs_relocation_before_extend = False

# expand capacity if this would REACH (not EXCEED) the existing capacity
# that's because completely a full fBasketEntry has nowhere to put the
# number of entries in the last basket (it's a fencepost principle thing),
Expand Down Expand Up @@ -479,17 +530,7 @@ def extend(self, file, sink, data):
datum["fBasketSeek"][: len(fBasketSeek)] = fBasketSeek
datum["fBasketEntry"][len(fBasketEntry)] = self._num_entries

oldloc = start = self._key.location
stop = start + self._key.num_bytes + self._key.compressed_bytes

self.write_anew(sink)

newloc = self._key.seek_location
file._move_tree(oldloc, newloc)

self._freesegments.release(start, stop)
sink.set_file_length(self._freesegments.fileheader.end)
sink.flush()
self._relocate(file, sink)

provided = None

Expand Down Expand Up @@ -822,7 +863,24 @@ def extend(self, file, sink, data):

self.write_updates(sink)

def write_anew(self, sink):
def _build_out(self):
"""
Serializes this TTree's metadata blob (TTree + TBranches + TLeaves,
everything but the TBaskets themselves) into a list of byte chunks,
exactly as ``write_anew`` writes it.

As a side effect, this sets ``self._metadata_start`` and, for every
branch, ``datum["metadata_start"]``, ``datum["basket_metadata_start"]``,
and ``datum["tleaf_reference_number"]`` — the byte offsets (relative to
this blob) that ``write_updates`` writes at directly, without going
through ``write_anew`` again. This is the single source of truth for
that layout: computing it any other way (e.g. searching a decompressed
blob for the current value of a field) risks disagreeing with it,
especially when that field's current value is 0 and matches unrelated
bytes. ``uproot.writing.writable.WritableDirectory._load_existing_ttree``
calls this once, on a freshly reconstructed cascade, purely to recover
these offsets — the returned ``out`` is discarded, nothing is written.
"""
key_num_bytes = uproot.reading._key_format_big.size + 6
name_asbytes = self._name.encode(errors="surrogateescape")
title_asbytes = self._title.encode(errors="surrogateescape")
Expand Down Expand Up @@ -1200,6 +1258,10 @@ def write_anew(self, sink):

self._metadata_start = sum(len(x) for x in out[:metadata_out_index])

return out

def write_anew(self, sink):
out = self._build_out()
raw_data = b"".join(out)
self._key = self._directory.add_object(
sink,
Expand Down
Loading
Loading