Skip to content

Commit 4bb0f6a

Browse files
committed
Get all the field names and types from an RNTuple header.
1 parent 85f219a commit 4bb0f6a

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/uproot/models/RNTuple.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@
88

99
import struct
1010

11+
try:
12+
import queue
13+
except ImportError:
14+
import Queue as queue
15+
1116
import uproot
1217

1318
_rntuple_format1 = struct.Struct(">IIQIIQIIQ")
1419

20+
# https://github.com/jblomer/root/blob/ntuple-binary-format-v1/tree/ntuple/v7/doc/specifications.md#envelopes
21+
_rntuple_frame_format = struct.Struct("<HHI")
22+
_rntuple_feature_flag_format = struct.Struct("<Q")
23+
_rntuple_num_bytes_fields = struct.Struct("<II")
24+
1525

1626
class Model_ROOT_3a3a_Experimental_3a3a_RNTuple(uproot.model.Model):
1727
"""
@@ -40,6 +50,107 @@ def read_members(self, chunk, cursor, context, file):
4050
self._members["fReserved"],
4151
) = cursor.fields(chunk, _rntuple_format1, context)
4252

53+
seek, nbytes = self._members["fSeekHeader"], self._members["fNBytesHeader"]
54+
header_range = (seek, seek + nbytes)
55+
56+
seek, nbytes = self._members["fSeekFooter"], self._members["fNBytesFooter"]
57+
footer_range = (seek, seek + nbytes)
58+
59+
notifications = queue.Queue()
60+
compressed_header_chunk, compressed_footer_chunk = file.source.chunks(
61+
[header_range, footer_range], notifications=notifications
62+
)
63+
64+
if self._members["fNBytesHeader"] == self._members["fLenHeader"]:
65+
self._header_chunk = compressed_header_chunk
66+
self._header_cursor = uproot.source.cursor.Cursor(
67+
self._members["fSeekHeader"]
68+
)
69+
else:
70+
self._header_chunk = uproot.compression.decompress(
71+
compressed_header_chunk,
72+
uproot.source.cursor.Cursor(self._members["fSeekHeader"]),
73+
context,
74+
self._members["fNBytesHeader"],
75+
self._members["fLenHeader"],
76+
)
77+
self._header_cursor = uproot.source.cursor.Cursor(0)
78+
79+
if self._members["fNBytesFooter"] == self._members["fLenFooter"]:
80+
self._footer_chunk = compressed_footer_chunk
81+
self._footer_cursor = uproot.source.cursor.Cursor(
82+
self._members["fSeekFooter"]
83+
)
84+
else:
85+
self._footer_chunk = uproot.compression.decompress(
86+
compressed_footer_chunk,
87+
uproot.source.cursor.Cursor(self._members["fSeekFooter"]),
88+
context,
89+
self._members["fNBytesFooter"],
90+
self._members["fLenFooter"],
91+
)
92+
self._footer_cursor = uproot.source.cursor.Cursor(0)
93+
94+
self._header, self._footer = None, None
95+
96+
@property
97+
def header(self):
98+
if self._header is None:
99+
cursor = self._header_cursor.copy()
100+
context = {}
101+
102+
self._header = {}
103+
self._header["frame"] = self._frame(self._header_chunk, cursor, context)
104+
105+
# https://github.com/jblomer/root/blob/ntuple-binary-format-v1/tree/ntuple/v7/doc/specifications.md#header-envelope
106+
self._header["feature_flag"] = cursor.field(
107+
self._header_chunk, _rntuple_feature_flag_format, context
108+
)
109+
self._header["name"] = cursor.rntuple_string(self._header_chunk, context)
110+
self._header["description"] = cursor.rntuple_string(
111+
self._header_chunk, context
112+
)
113+
self._header["author"] = cursor.rntuple_string(self._header_chunk, context)
114+
115+
cursor.skip(68) # ???
116+
num_fields_plus_one = cursor.field(
117+
self._header_chunk, struct.Struct("<Q"), context
118+
)
119+
120+
self._header["fields"] = [None] * (num_fields_plus_one - 1)
121+
while any(x is None for x in self._header["fields"]):
122+
field = {}
123+
124+
pos = cursor.index
125+
field["num_bytes"] = cursor.field(
126+
self._header_chunk, struct.Struct("<I"), context
127+
)
128+
field["id"] = cursor.field(
129+
self._header_chunk, struct.Struct("<Q"), context
130+
)
131+
self._header["fields"][field["id"] - 1] = field
132+
133+
cursor.skip(48) # ???
134+
field["name"] = cursor.rntuple_string(self._header_chunk, context)
135+
field["description"] = cursor.rntuple_string(
136+
self._header_chunk, context
137+
)
138+
field["type"] = cursor.rntuple_string(self._header_chunk, context)
139+
140+
cursor.move_to(pos + field["num_bytes"])
141+
142+
return self._header
143+
144+
@property
145+
def footer(self):
146+
raise NotImplementedError
147+
148+
def _frame(self, chunk, cursor, context):
149+
version, min_version, num_bytes = cursor.fields(
150+
chunk, _rntuple_frame_format, context
151+
)
152+
return {"version": version, "min_version": min_version, "num_bytes": num_bytes}
153+
43154

44155
uproot.classes[
45156
"ROOT::Experimental::RNTuple"

src/uproot/source/cursor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import absolute_import
1010

11+
import datetime
1112
import struct
1213
import sys
1314

@@ -22,6 +23,10 @@
2223
_raw_double32 = struct.Struct(">f")
2324
_raw_float16 = struct.Struct(">BH")
2425

26+
# https://github.com/jblomer/root/blob/ntuple-binary-format-v1/tree/ntuple/v7/doc/specifications.md#basic-types
27+
_rntuple_string_length = struct.Struct("<I")
28+
_rntuple_datetime = struct.Struct("<Q")
29+
2530

2631
class Cursor(object):
2732
"""
@@ -461,6 +466,20 @@ def classname(self, chunk, context, move=True):
461466
else:
462467
return out.decode(errors="surrogateescape")
463468

469+
def rntuple_string(self, chunk, context, move=True):
470+
if move:
471+
length = self.field(chunk, _rntuple_string_length, context)
472+
return self.string_with_length(chunk, context, length)
473+
else:
474+
index = self._index
475+
out = self.rntuple_string(chunk, context, move=True)
476+
self._index = index
477+
return out
478+
479+
def rntuple_datetime(self, chunk, context, move=True):
480+
raw = self.field(chunk, _rntuple_datetime, context, move=move)
481+
return datetime.datetime.fromtimestamp(raw)
482+
464483
def debug(
465484
self,
466485
chunk,

0 commit comments

Comments
 (0)