Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,5 @@ Thumbs.db
*~
*.swp
.vscode

debug.py
4 changes: 4 additions & 0 deletions src/rootfilespec/bootstrap/TList.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ class TObjArray(TSeqCollection):
objects: tuple[ROOTSerializable, ...]
"""List of objects."""


def __iter__(self):
return iter(self.objects)

@classmethod
def update_members(cls, members: Members, buffer: ReadBuffer):
(members["fLowerBound"],), buffer = buffer.unpack(">i")
Expand Down
5 changes: 5 additions & 0 deletions src/rootfilespec/bootstrap/TStreamerInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ def member_definition(self, parent: TStreamerInfo): # noqa: ARG002
fmt = ElementType(self.fType - ElementType.kOffsetL).as_fmt()
atype = f"FixedSizeArray({fmt!r}, {self.fArrayLength})"
return f"{self.member_name()}: Annotated[np.ndarray, {atype}]", []

# if double32, use double32serde
# add a breakpoint, use debug mode on pytest
# run "uproot-Event.root", "uproot-double32-float16.root", "uproot-issue-308.root"

fmt = self.fType.as_fmt()
pytype = _structtype_to_pytype(fmt).__name__
return f"{self.member_name()}: Annotated[{pytype}, Fmt({fmt!r})]", []
Expand Down
30 changes: 30 additions & 0 deletions src/rootfilespec/bootstrap/double32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from rootfilespec.bootstrap.streamedobject import StreamedObject
from rootfilespec.buffer import ReadBuffer
from rootfilespec.dispatch import DICTIONARY
from rootfilespec.serializable import Members, ROOTSerializable, serializable
from rootfilespec.serializable import MemberSerDe

from typing import Optional
import dataclasses

@dataclasses.dataclass
class Double32Reader:
fname: str
factor: Optional[float]
xmin: Optional[float]
nbits: Optional[int]

def __call__(self, members: Members, buffer: ReadBuffer) -> tuple[Members, ReadBuffer]:
# Float32 Read
(val,), buffer = buffer.unpack(">f")
members[self.fname] = float(val)
return members, buffer

@dataclasses.dataclass
class Double32Serde(MemberSerDe):
factor: Optional[float] = None
xmin: Optional[float] = None
nbits: Optional[int] = None

def build_reader(self, fname: str, ftype: type):
return Double32Reader(fname, self.factor, self.xmin, self.nbits)
175 changes: 175 additions & 0 deletions src/rootfilespec/bootstrap/uproot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from typing import Any

from uproot.model import Model # type: ignore[import-not-found]

from rootfilespec.serializable import ROOTSerializable, _get_annotations


class UprootModelAdapter(Model): # type: ignore[misc]
"""
Adapter for Uproot models to be used with the rootfilespec library.
This class allows Uproot to read ROOTSerializable objects
"""

_model: ROOTSerializable
_file: Any

def __init__(self, model: Any) -> None:
self._model = model

def __getattr__(self, name: str):
# Try the wrapped model first
if hasattr(self._model, name):
return getattr(self._model, name)

# Then try the class we're adapting to (Uproot behavior/model internals)
behavior_cls = getattr(type(self), "__behavior_cls__", None)
if behavior_cls and hasattr(behavior_cls, name):
return getattr(behavior_cls, name)

# Fall back to error
raise AttributeError(f"{self.__class__.__name__} has no attribute {name!r}")


@property
def _fields(self):
return _get_annotations(type(self._model))

@property
def encoded_classname(self):
name = type(self._model).__name__.replace("3a3a", "_3a3a_")
return "Model_" + name

def num_members(self):
return len(self._fields)

def member_name(self, index):
try:
return list(self._fields.keys())[index]
except IndexError:
err = f"Member index {index} out of range"
raise IndexError(err) from None

def member(self, name, all: bool = True, none_if_missing: bool = False):
if all:
value = getattr(self._model, name, None)
else:
# Check only declared fields on this type (not bases)
fields = _get_annotations(type(self._model))
value = getattr(self._model, name, None) if name in fields else None

if none_if_missing and value is None:
msg = f"Member {name} not found in {self._model.__class__.__name__}"
raise AttributeError(msg)

return self._adapt_value(value)

def _adapt_value(self, value):
"""Normalize ROOTSerializable values into Uproot-friendly objects."""
if isinstance(value, ROOTSerializable):
if value.__class__.__name__ == "TString":
return value.fString.decode("utf-8")

adapter_cls = create_adapter_class(type(value))
return adapter_cls(value)

elif isinstance(value, (list, tuple)):
return type(value)(self._adapt_value(v) for v in value)

elif isinstance(value, dict):
return {k: self._adapt_value(v) for k, v in value.items()}

return value


from typing import Any

def create_adapter_class(model_cls):
"""
Wrap a Model class (like Model_TTree_v20) into an Adapter that exposes
lookup, bases, cache_key, and underscored aliases safely.
"""
try:
behavior_subclasses = model_cls.behavior
except AttributeError:
behavior_subclasses = (
base
for base in model_cls.__bases__
if "uproot.behavior" in base.__module__
)
print(model_cls.__bases__)

# TODO: Check if we can use model_cls instead of *behavior_subclasses
class Adapter(UprootModelAdapter, *behavior_subclasses):
def __init__(self, model_instance):
super().__init__(model_instance)
self._internal_lookup = getattr(model_instance, "_lookup", {})
self._internal_bases = getattr(model_instance, "_bases", [])
self._internal_cache_key = getattr(model_instance, "cache_key", None)

@property
def name(self):
return getattr(self._model, "name", None)

@property
def class_version(self):
return getattr(self._model, "class_version", None)

def __enter__(self):
return self

def __exit__(self, exc_type, exc, tb):
try:
close = getattr(self, "close", None)
if callable(close):
close()
else:
mclose = getattr(self._model, "close", None)
if callable(mclose):
mclose()
except Exception:
return False
return False

# def __getitem__(self, key):
# try:
# return self.lookup[key]
# except Exception:
# return self._internal_lookup[key]

# @property
# def lookup(self):
# val = getattr(self.model, "lookup", None)
# if val is None or isinstance(val, property):
# return self._internal_lookup
# return val

@property
def bases(self):
val = getattr(self.model, "bases", None)
if val is None or isinstance(val, property):
return self._internal_bases
return val

@property
def cache_key(self):
val = getattr(self.model, "cache_key", None)
if val is None or isinstance(val, property):
return self._internal_cache_key
return val

# @property
# def _lookup(self):
# return self.lookup

@property
def _bases(self):
return self.bases

@property
def _cache_key(self):
return self.cache_key

Adapter.__name__ = f"Adapter_{model_cls.__name__}"

return Adapter
2 changes: 1 addition & 1 deletion src/rootfilespec/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class FixedSizeArray(MemberSerDe):
size: int

def build_reader(self, fname: str, ftype: type): # noqa: ARG002
if self.fmt in ("float16", "double32", "charstar"):
if self.fmt in ("float16", "charstar"):
msg = f"Unimplemented format {self.fmt}"
raise NotImplementedError(msg)
return _FixedSizeArrayReader(fname, np.dtype(self.fmt), self.size)
Expand Down
4 changes: 2 additions & 2 deletions src/rootfilespec/structutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from rootfilespec.buffer import ReadBuffer
from rootfilespec.serializable import Members, MemberSerDe

from rootfilespec.bootstrap.double32 import Double32

@dataclasses.dataclass
class _FmtReader:
Expand All @@ -13,7 +13,7 @@ class _FmtReader:
def __call__(
self, members: Members, buffer: ReadBuffer
) -> tuple[Members, ReadBuffer]:
if self.fmt in ("float16", "double32", "charstar"):
if self.fmt in ("float16", "charstar"):
msg = f"Unimplemented format {self.fmt}"
raise NotImplementedError(msg)
tup, buffer = buffer.unpack(self.fmt)
Expand Down
Loading