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 )
0 commit comments