diff --git a/src/uproot/writing/_cascadentuple.py b/src/uproot/writing/_cascadentuple.py index d183d5db2..ebca2a72f 100644 --- a/src/uproot/writing/_cascadentuple.py +++ b/src/uproot/writing/_cascadentuple.py @@ -497,7 +497,7 @@ def __init__(self, location, header_checksum): super().__init__(location, None) def __repr__(self): - return f"{type(self).__name__}(extension_field_record_frames = {self.extension_field_record_frames}, column_group_record_frames = {self.extension_column_record_frames}, cluster_summary_record_frames={self.extension_column_record_frames}, cluster_group_record_frames{self.cluster_group_record_frames}, metadata_block_envelope_link = {self.metadata_block_envelope_links})" + return f"{type(self).__name__}(extension_field_record_frames={self.extension_field_record_frames}, extension_column_record_frames={self.extension_column_record_frames}, cluster_group_record_frames={self.cluster_group_record_frames})" def serialize(self): out = [] @@ -756,19 +756,6 @@ def serialize(self): return out -class Ntuple_PageLink: - def __init__(self, num_bytes, offset): - self.num_bytes = num_bytes - self.offset = offset - - def serialize(self): - outbytes = _rntuple_locator_size_format.pack(self.num_bytes, self.offset) - return outbytes - - def __repr__(self): - return f"{type(self).__name__}({self.num_bytes}, {self.offset})" - - class NTuple(CascadeNode): """ Writes a RNTuple, including all fields, columns, and (upon ``extend``) pages. @@ -827,18 +814,10 @@ def name(self): def title(self): return self._key.title - @property - def branch_types(self): - return self._branch_types - @property def freesegments(self): return self._freesegments - @property - def field_name(self): - return self._field_name - @property def location(self): return self._key.location @@ -956,7 +935,10 @@ def extend(self, file, sink, data): old_footer_key = self._footer_key self._freesegments.release( - old_footer_key.location, old_footer_key.location + old_footer_key.allocation + old_footer_key.location, + old_footer_key.location + + old_footer_key.num_bytes + + old_footer_key.compressed_bytes, ) footer_raw_data = self._footer.serialize() self._footer_key = self.add_rblob(sink, footer_raw_data, len(footer_raw_data)) @@ -1042,9 +1024,6 @@ def write(self, sink): self._freesegments.write(sink) - def write_updates(self, sink): - sink.flush() - def _to_packed_form(form): match form.__class__: diff --git a/src/uproot/writing/_cascadetree.py b/src/uproot/writing/_cascadetree.py index 6b80763d3..869b41e0c 100644 --- a/src/uproot/writing/_cascadetree.py +++ b/src/uproot/writing/_cascadetree.py @@ -414,10 +414,6 @@ def name(self): def title(self): return self._key.title - @property - def branch_types(self): - return self._branch_types - @property def freesegments(self): return self._freesegments diff --git a/src/uproot/writing/writable.py b/src/uproot/writing/writable.py index 7d45223f4..f60b1af6a 100644 --- a/src/uproot/writing/writable.py +++ b/src/uproot/writing/writable.py @@ -1056,6 +1056,14 @@ def get_chunk(start, stop): def _del(self, name, cycle): key = self._cascading.data.get_key(name, cycle) + if key is None: + raise uproot.exceptions.KeyInFileError( + name, + cycle="any" if cycle is None else cycle, + keys=self._cascading.data.key_names, + file_path=self.file_path, + object_path=self.object_path, + ) start = key.seek_location stop = start + key.num_bytes + key.compressed_bytes self._cascading.freesegments.release(start, stop) @@ -2127,72 +2135,18 @@ def __exit__(self, exception_type, exception_value, traceback): def compression(self): """ Compression algorithm and level (:doc:`uproot.compression.Compression` or None) - for new blobs added to the RNTuple. - - This property can be changed and doesn't have to be the same as the compression - of the file, which allows you to write different objects with different - compression settings. - - The following are equivalent: + used when writing pages to this RNTuple. - .. code-block:: python - - my_directory["tree"]["branch1"].compression = uproot.ZLIB(1) - my_directory["tree"]["branch2"].compression = uproot.LZMA(9) - - and - - .. code-block:: python - - my_directory["tree"].compression = {"branch1": uproot.ZLIB(1), - "branch2": uproot.LZMA(9)} + RNTuple compression is file-wide and is taken from the file header; individual + per-column compression is not yet supported. """ - out = {} - last = None - for datum in self._cascading._branch_data: - if datum["kind"] != "record": - last = out[datum["fName"]] = datum["compression"] - if all(x == last for x in out.values()): - return last - else: - return out + return self._cascading._freesegments.fileheader.compression @compression.setter def compression(self, value): - if value is None or isinstance(value, uproot.compression.Compression): - for datum in self._cascading._branch_data: - if datum["kind"] != "record": - datum["compression"] = value - - elif ( - isinstance(value, Mapping) - and all( - isinstance(k, str) - and (v is None or isinstance(v, uproot.compression.Compression)) - for k, v in value.items() - ) - and all( - datum["fName"] in value - for datum in self._cascading._branch_data - if datum["kind"] != "record" - ) - and len(value) - == len( - [ - datum - for datum in self._cascading._branch_data - if datum["kind"] != "record" - ] - ) - ): - for datum in self._cascading._branch_data: - if datum["kind"] != "record": - datum["compression"] = value[datum["fName"]] - - else: - raise TypeError( - "compression must be None, a uproot.compression.Compression object, like uproot.ZLIB(4) or uproot.ZSTD(0), or a mapping of branch names to such objects" - ) + raise NotImplementedError( + "per-RNTuple compression is not yet supported; set compression on the file instead" + ) @property def num_entries(self) -> int: diff --git a/tests/test_1650_rntuple_writing_bugfixes.py b/tests/test_1650_rntuple_writing_bugfixes.py new file mode 100644 index 000000000..d2657c85a --- /dev/null +++ b/tests/test_1650_rntuple_writing_bugfixes.py @@ -0,0 +1,25 @@ +# BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE + +from __future__ import annotations + +import numpy as np +import pytest + +import uproot + + +def test_writable_directory_delitem_nonexistent_raises_key_error(tmp_path): + """ + Deleting a nonexistent key from a WritableDirectory must raise a KeyError + subclass (uproot.exceptions.KeyInFileError), not AttributeError. + + Before the fix, _del() called key.seek_location without checking whether + get_key() returned None, resulting in: + AttributeError: 'NoneType' object has no attribute 'seek_location' + """ + fname = tmp_path / "del_nonexistent.root" + f = uproot.recreate(fname) + f["t"] = {"x": np.array([1, 2, 3], dtype="i4")} + with pytest.raises(KeyError): + del f["nonexistent_key"] + f.close()