Skip to content

Commit 622e0fe

Browse files
authored
feat: support constructor conversion from UHI (#1034)
* feat: support constructor conversion from UHI Signed-off-by: Henry Schreiner <[email protected]> * fix: rework implementation to not call init recursively Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent ee7f0a1 commit 622e0fe

File tree

2 files changed

+43
-16
lines changed

2 files changed

+43
-16
lines changed

src/boost_histogram/histogram.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,13 @@ def __init_subclass__(cls, *, family: object | None = None) -> None:
223223
cls._family = family if family is not None else object()
224224

225225
@typing.overload
226-
def __init__(self, *args: Histogram) -> None: ...
226+
def __init__(self, arg: Histogram, /, *, metadata: Any = ...) -> None: ...
227227

228228
@typing.overload
229-
def __init__(self, *args: CppHistogram, metadata: Any = ...) -> None: ...
229+
def __init__(self, arg: dict[str, Any], /, *, metadata: Any = ...) -> None: ...
230+
231+
@typing.overload
232+
def __init__(self, arg: CppHistogram, /, *, metadata: Any = ...) -> None: ...
230233

231234
@typing.overload
232235
def __init__(
@@ -238,8 +241,8 @@ def __init__(
238241

239242
def __init__(
240243
self,
241-
*axes: Axis | CppAxis | Histogram | CppHistogram,
242-
storage: Storage = Double(), # noqa: B008
244+
*axes: Axis | CppAxis | Histogram | CppHistogram | dict[str, Any],
245+
storage: Storage | None = None,
243246
metadata: Any = None,
244247
) -> None:
245248
"""
@@ -259,31 +262,44 @@ def __init__(
259262
Data that is passed along if a new histogram is created
260263
"""
261264
self._variance_known = True
265+
storage_err_msg = "storage= is not allowed with conversion constructor"
262266

263267
# Allow construction from a raw histogram object (internal)
264268
if len(axes) == 1 and isinstance(axes[0], tuple(_histograms)):
269+
if storage is not None:
270+
raise TypeError(storage_err_msg)
265271
cpp_hist: CppHistogram = axes[0] # type: ignore[assignment]
266-
self._from_histogram_cpp(cpp_hist)
267-
if metadata is not None:
268-
self.metadata = metadata
272+
self._from_histogram_cpp(cpp_hist, metadata=None)
269273
return
270274

271275
# If we construct with another Histogram as the only positional argument,
272276
# support that too
273277
if len(axes) == 1 and isinstance(axes[0], Histogram):
274-
normal_hist: Histogram = axes[0]
275-
self._from_histogram_object(normal_hist)
276-
if metadata is not None:
277-
self.metadata = metadata
278+
if storage is not None:
279+
raise TypeError(storage_err_msg)
280+
self._from_histogram_object(axes[0], metadata=metadata)
278281
return
279282

280283
# Support objects that provide a to_boost method, like Uproot
281284
if len(axes) == 1 and hasattr(axes[0], "_to_boost_histogram_"):
282-
self._from_histogram_object(axes[0]._to_boost_histogram_())
285+
if storage is not None:
286+
raise TypeError(storage_err_msg)
287+
self._from_histogram_object(
288+
axes[0]._to_boost_histogram_(), metadata=metadata
289+
)
290+
return
291+
292+
# Support UHI
293+
if len(axes) == 1 and isinstance(axes[0], dict) and "uhi_schema" in axes[0]:
294+
if storage is not None:
295+
raise TypeError(storage_err_msg)
296+
self._from_histogram_object(
297+
serialization.from_uhi(axes[0]), metadata=metadata
298+
)
283299
return
284300

285301
if storage is None:
286-
storage = Double() # type: ignore[unreachable]
302+
storage = Double()
287303

288304
self.metadata = metadata
289305

@@ -356,16 +372,16 @@ def _new_hist(self, _hist: CppHistogram, memo: Any = NOTHING) -> Self:
356372
"""
357373
return self.__class__._clone(_hist, other=self, memo=memo)
358374

359-
def _from_histogram_cpp(self, other: CppHistogram) -> None:
375+
def _from_histogram_cpp(self, other: CppHistogram, *, metadata: Any = None) -> None:
360376
"""
361377
Import a Cpp histogram.
362378
"""
363379
self._variance_known = True
364380
self._hist = other
365-
self.metadata = None
381+
self.metadata = metadata
366382
self.axes = self._generate_axes_()
367383

368-
def _from_histogram_object(self, other: Histogram) -> None:
384+
def _from_histogram_object(self, other: Histogram, *, metadata: Any = None) -> None:
369385
"""
370386
Convert self into a new histogram object based on another, possibly
371387
converting from a different subclass.
@@ -375,6 +391,7 @@ def _from_histogram_object(self, other: Histogram) -> None:
375391
self.axes = self._generate_axes_()
376392
for ax in self.axes:
377393
ax.__dict__ = copy.copy(ax._ax.raw_metadata)
394+
self.metadata = other.metadata if metadata is None else metadata
378395

379396
# Allow custom behavior on either "from" or "to"
380397
other._export_bh_(self)

tests/test_serialization_uhi.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,16 @@ def test_uhi_wrapper():
197197
assert repr(from_uhi(data)) == repr(bh.Histogram._from_uhi_(data))
198198

199199

200+
def test_uhi_direct_conversion():
201+
h = bh.Histogram(
202+
bh.axis.IntCategory([1, 2, 3]),
203+
storage=bh.storage.Int64(),
204+
)
205+
uhi_dict = h._to_uhi_()
206+
h2 = bh.Histogram(uhi_dict)
207+
assert h == h2
208+
209+
200210
def test_round_trip_native() -> None:
201211
h = bh.Histogram(
202212
bh.axis.Integer(0, 10),

0 commit comments

Comments
 (0)