Skip to content

Commit 23777f0

Browse files
authored
fix: allow copy to work on overloaded init's (#759)
* fix: allow copy to work on overloaded init's Signed-off-by: Henry Schreiner <[email protected]> * fix(types): adapting to change Signed-off-by: Henry Schreiner <[email protected]> * fix(types): avoid type check on Any Signed-off-by: Henry Schreiner <[email protected]> * docs: update changelog
1 parent 8740b16 commit 23777f0

File tree

5 files changed

+117
-40
lines changed

5 files changed

+117
-40
lines changed

docs/changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## Version 1.3
44

5+
### WIP
6+
7+
#### Bug Fixes
8+
9+
* Avoid the constructor when creating new histograms from existing ones. [#759][]
10+
11+
[#759]: https://github.com/scikit-hep/boost-histogram/pull/759
12+
513
### Version 1.3.2
614

715
* Include PyPy 3.9 binary wheels [#730][]

src/boost_histogram/_core/axis/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class _BaseAxis:
2929
def traits_ordered(self) -> bool: ...
3030
@property
3131
def metadata(self) -> Any: ...
32+
@metadata.setter
33+
def metadata(self, item: Any) -> None: ...
3234
@property
3335
def size(self) -> int: ...
3436
@property

src/boost_histogram/_core/hist.pyi

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ _axes_limit: int
1212

1313
class _BaseHistogram:
1414
_storage_type: ClassVar[Type[storage._BaseStorage]]
15-
def __init__(self, axis: axis._BaseAxis, storage: storage._BaseStorage) -> None: ...
15+
# Note that storage has a default simply because subclasses always handle it.
16+
def __init__(
17+
self, axis: list[axis._BaseAxis], storage: storage._BaseStorage = ...
18+
) -> None: ...
1619
def rank(self) -> int: ...
1720
def size(self) -> int: ...
1821
def reset(self) -> None: ...
@@ -25,10 +28,17 @@ class _BaseHistogram:
2528
def to_numpy(self, flow: bool = ...) -> Tuple["np.typing.NDArray[Any]", ...]: ...
2629
def view(self, flow: bool = ...) -> "np.typing.NDArray[Any]": ...
2730
def axis(self, i: int = ...) -> axis._BaseAxis: ...
28-
def fill(self, *args: ArrayLike, weight: ArrayLike | None = ...) -> None: ...
31+
def fill(
32+
self,
33+
*args: ArrayLike,
34+
weight: ArrayLike | None = ...,
35+
sample: ArrayLike | None = ...,
36+
) -> None: ...
2937
def empty(self, flow: bool = ...) -> bool: ...
3038
def reduce(self: T, *args: Any) -> T: ...
3139
def project(self: T, *args: int) -> T: ...
40+
def sum(self, flow: bool = ...) -> Any: ...
41+
def at(self, *args: int) -> Any: ...
3242

3343
class any_int64(_BaseHistogram):
3444
def __idiv__(self: T, other: any_int64) -> T: ...
@@ -73,7 +83,7 @@ class any_mean(_BaseHistogram):
7383
self,
7484
*args: ArrayLike,
7585
weight: ArrayLike | None = ...,
76-
sample: ArrayLike | None = ...
86+
sample: ArrayLike | None = ...,
7787
) -> None: ...
7888

7989
class any_weighted_mean(_BaseHistogram):
@@ -84,5 +94,5 @@ class any_weighted_mean(_BaseHistogram):
8494
self,
8595
*args: ArrayLike,
8696
weight: ArrayLike | None = ...,
87-
sample: ArrayLike | None = ...
97+
sample: ArrayLike | None = ...,
8898
) -> None: ...

src/boost_histogram/_internal/hist.py

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
TYPE_CHECKING,
1010
Any,
1111
Callable,
12+
ClassVar,
1213
Dict,
1314
Iterable,
1415
List,
@@ -129,7 +130,10 @@ class Histogram:
129130
)
130131
# .metadata and ._variance_known are part of the dict
131132

132-
_family: object = boost_histogram
133+
_family: ClassVar[object] = boost_histogram
134+
135+
axes: AxesTuple
136+
_hist: CppHistogram
133137

134138
def __init_subclass__(cls, *, family: Optional[object] = None) -> None:
135139
"""
@@ -185,22 +189,24 @@ def __init__(
185189

186190
# Allow construction from a raw histogram object (internal)
187191
if len(axes) == 1 and isinstance(axes[0], tuple(_histograms)):
188-
self._hist: Any = axes[0]
189-
self.metadata = metadata
190-
self.axes = self._generate_axes_()
192+
cpp_hist: CppHistogram = axes[0] # type: ignore[assignment]
193+
self._from_histogram_cpp(cpp_hist)
194+
if metadata:
195+
self.metadata = metadata
191196
return
192197

193198
# If we construct with another Histogram as the only positional argument,
194199
# support that too
195200
if len(axes) == 1 and isinstance(axes[0], Histogram):
196-
# Special case - we can recursively call __init__ here
197-
self.__init__(axes[0]._hist) # type: ignore[misc] # pylint: disable=non-parent-init-called
198-
self._from_histogram_object(axes[0])
201+
normal_hist: Histogram = axes[0]
202+
self._from_histogram_object(normal_hist)
203+
if metadata:
204+
self.metadata = metadata
199205
return
200206

201207
# Support objects that provide a to_boost method, like Uproot
202208
if len(axes) == 1 and hasattr(axes[0], "_to_boost_histogram_"):
203-
self.__init__(axes[0]._to_boost_histogram_()) # type: ignore[misc, union-attr] # pylint: disable=non-parent-init-called
209+
self._from_histogram_object(axes[0]._to_boost_histogram_()) # type: ignore[union-attr]
204210
return
205211

206212
if storage is None:
@@ -232,6 +238,60 @@ def __init__(
232238

233239
raise TypeError("Unsupported storage")
234240

241+
@classmethod
242+
def _clone(
243+
cls: Type[H],
244+
_hist: "Histogram | CppHistogram",
245+
*,
246+
other: "Histogram | None" = None,
247+
memo: Any = NOTHING,
248+
) -> H:
249+
"""
250+
Clone a histogram (possibly of a different base). Does not trigger __init__.
251+
This will copy data from `other=` if non-None, otherwise metadata gets copied from the input.
252+
"""
253+
254+
self = cls.__new__(cls)
255+
if isinstance(_hist, tuple(_histograms)):
256+
self._from_histogram_cpp(_hist) # type: ignore[arg-type]
257+
if other is not None:
258+
return cls._clone(self, other=other, memo=memo)
259+
return self
260+
261+
assert isinstance(_hist, Histogram)
262+
263+
if other is None:
264+
other = _hist
265+
266+
self._from_histogram_object(_hist)
267+
268+
if memo is NOTHING:
269+
self.__dict__ = copy.copy(other.__dict__)
270+
else:
271+
self.__dict__ = copy.deepcopy(other.__dict__, memo)
272+
273+
for ax in self.axes:
274+
if memo is NOTHING:
275+
ax.__dict__ = copy.copy(ax._ax.metadata)
276+
else:
277+
ax.__dict__ = copy.deepcopy(ax._ax.metadata, memo)
278+
return self
279+
280+
def _new_hist(self: H, _hist: CppHistogram, memo: Any = NOTHING) -> H:
281+
"""
282+
Return a new histogram given a new _hist, copying current metadata.
283+
"""
284+
return self.__class__._clone(_hist, other=self, memo=memo)
285+
286+
def _from_histogram_cpp(self, other: CppHistogram) -> None:
287+
"""
288+
Import a Cpp histogram.
289+
"""
290+
self._variance_known = True
291+
self._hist = other
292+
self.metadata = None
293+
self.axes = self._generate_axes_()
294+
235295
def _from_histogram_object(self, other: "Histogram") -> None:
236296
"""
237297
Convert self into a new histogram object based on another, possibly
@@ -270,32 +330,12 @@ def _generate_axes_(self) -> AxesTuple:
270330

271331
return AxesTuple(self._axis(i) for i in range(self.ndim))
272332

273-
def _new_hist(self: H, _hist: CppHistogram, memo: Any = NOTHING) -> H:
274-
"""
275-
Return a new histogram given a new _hist, copying metadata.
276-
"""
277-
278-
other = self.__class__(_hist)
279-
if memo is NOTHING:
280-
other.__dict__ = copy.copy(self.__dict__)
281-
else:
282-
other.__dict__ = copy.deepcopy(self.__dict__, memo)
283-
other.axes = other._generate_axes_()
284-
285-
for ax in other.axes:
286-
if memo is NOTHING:
287-
ax.__dict__ = copy.copy(ax._ax.metadata)
288-
else:
289-
ax.__dict__ = copy.deepcopy(ax._ax.metadata, memo)
290-
291-
return other
292-
293333
@property
294334
def ndim(self) -> int:
295335
"""
296336
Number of axes (dimensions) of the histogram.
297337
"""
298-
return self._hist.rank() # type: ignore[no-any-return]
338+
return self._hist.rank()
299339

300340
def view(
301341
self, flow: bool = False
@@ -469,7 +509,7 @@ def fill(
469509
threads = cpu_count()
470510

471511
if threads is None or threads == 1:
472-
self._hist.fill(*args_ars, weight=weight_ars, sample=sample_ars)
512+
self._hist.fill(*args_ars, weight=weight_ars, sample=sample_ars) # type: ignore[arg-type]
473513
return self
474514

475515
if self._hist._storage_type in {
@@ -640,7 +680,7 @@ def _compute_uhi_index(self, index: InnerIndexing, axis: int) -> SimpleIndexing:
640680
if isinstance(index, SupportsIndex):
641681
if abs(int(index)) >= self._hist.axis(axis).size:
642682
raise IndexError("histogram index is out of range")
643-
return index % self._hist.axis(axis).size # type: ignore[no-any-return]
683+
return int(index) % self._hist.axis(axis).size
644684

645685
return index
646686

@@ -719,7 +759,7 @@ def to_numpy(
719759
hist, *edges = self._hist.to_numpy(flow)
720760
hist = self.view(flow=flow) if view else self.values(flow=flow)
721761

722-
return (hist, edges) if dd else (hist, *edges)
762+
return (hist, edges) if dd else (hist, *edges) # type: ignore[return-value]
723763

724764
def copy(self: H, *, deep: bool = True) -> H:
725765
"""
@@ -742,7 +782,7 @@ def empty(self, flow: bool = False) -> bool:
742782
Check to see if the histogram has any non-default values.
743783
You can use flow=True to check flow bins too.
744784
"""
745-
return self._hist.empty(flow) # type: ignore[no-any-return]
785+
return self._hist.empty(flow)
746786

747787
def sum(self, flow: bool = False) -> Union[float, Accumulator]:
748788
"""
@@ -758,7 +798,7 @@ def size(self) -> int:
758798
"""
759799
Total number of bins in the histogram (including underflow/overflow).
760800
"""
761-
return self._hist.size() # type: ignore[no-any-return]
801+
return self._hist.size()
762802

763803
@property
764804
def shape(self) -> Tuple[int, ...]:
@@ -779,7 +819,7 @@ def __getitem__( # noqa: C901
779819
if not hasattr(indexes, "items") and all(
780820
isinstance(a, SupportsIndex) for a in indexes
781821
):
782-
return self._hist.at(*indexes) # type: ignore[no-any-return]
822+
return self._hist.at(*indexes) # type: ignore[no-any-return, arg-type]
783823

784824
integrations: Set[int] = set()
785825
slices: List[_core.algorithm.reduce_command] = []
@@ -885,7 +925,7 @@ def __getitem__( # noqa: C901
885925
if ax.traits_overflow and ax.size not in pick_set[i]:
886926
selection.append(ax.size)
887927

888-
new_axis = axes[i].__class__([axes[i].value(j) for j in pick_set[i]])
928+
new_axis = axes[i].__class__([axes[i].value(j) for j in pick_set[i]]) # type: ignore[call-arg]
889929
new_axis.metadata = axes[i].metadata
890930
axes[i] = new_axis
891931
reduced_view = np.take(reduced_view, selection, axis=i)

tests/test_subclassing.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,20 @@ class MyHist(bh.Histogram):
3232

3333
assert type(h) == MyHist
3434
assert type(h.axes[0]) == bh.axis.Regular
35+
36+
37+
def test_copy():
38+
class MyHist(bh.Histogram):
39+
def __init__(self, var, bins, weight, **kwargs):
40+
super().__init__(
41+
bh.axis.Regular(*bins), storage=bh.storage.Weight(), **kwargs
42+
)
43+
44+
self.fill(var, weight=weight)
45+
46+
b = (2, 0, 1)
47+
v = [0.1, 0.5, 0.9]
48+
w = [1, 0.5, 1]
49+
hist = MyHist(v, b, w)
50+
51+
hist.copy()

0 commit comments

Comments
 (0)