Skip to content
Merged
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
8 changes: 7 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@

#### User changes

* Arbitrary items can be set on an axis or histogram. [#450][]
* Arbitrary items can be set on an axis or histogram. [#450][], [#456][]
* Subclasses can customize the conversion procedure. [#456][]

#### Bug fixes

* Fixed reading pickles from boost-histogram 0.6-0.8 [#445][]
* Minor correctness fix [#446][]
* Accidental install of typing on Python 3.5+ fixed
* Scalar ND fill fixed [#453][]

#### Developer changes
* Updated to Boost 1.74 [#442][]
* CMake installs version.py now too [#449][]
* Updated setuptools infrastructure no longer requires NumPy [#451][]
* Some basic clang-tidy checks are now being run [#455][]


[#442]: https://github.com/scikit-hep/boost-histogram/pull/442
Expand All @@ -25,6 +28,9 @@
[#449]: https://github.com/scikit-hep/boost-histogram/pull/449
[#450]: https://github.com/scikit-hep/boost-histogram/pull/450
[#451]: https://github.com/scikit-hep/boost-histogram/pull/451
[#453]: https://github.com/scikit-hep/boost-histogram/pull/453
[#455]: https://github.com/scikit-hep/boost-histogram/pull/455
[#456]: https://github.com/scikit-hep/boost-histogram/pull/456


## Version 0.10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,6 @@ class AxesTuple(tuple):
def size(self):
return tuple(s.size for s in self)

@property
def metadata(self):
return tuple(s.metadata for s in self)

@metadata.setter
def metadata(self, values):
for s, v in zip(self, values):
s.metadata = v

@property
def extent(self):
return tuple(s.extent for s in self)
Expand Down Expand Up @@ -102,6 +93,12 @@ def __getitem__(self, item):
result = super(AxesTuple, self).__getitem__(item)
return self.__class__(result) if isinstance(result, tuple) else result

def __getattr__(self, attr):
return self.__class__(s.__getattr__(attr) for s in self)

def __setattr__(self, attr, values):
return self.__class__(s.__setattr__(attr, v) for s, v in zip(self, values))

# Python 2 support - remove after 1.0
def __getslice__(self, start, stop):
result = super(AxesTuple, self).__getslice__(start, stop)
Expand Down
10 changes: 5 additions & 5 deletions src/boost_histogram/_internal/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ def __getattr__(self, item):
elif item == "metadata":
return None
else:
raise AttributeError(
"'{}' object has no attribute '{}'".format(type(self).__name__, item)
msg = "'{}' object has no attribute '{}' in {}".format(
type(self).__name__, item, set(self._ax.metadata)
)
raise AttributeError(msg)

def __setattr__(self, item, value):
if item == "_ax":
Axis.__dict__[item].__set__(self, value)
return

self._ax.metadata[item] = value
else:
self._ax.metadata[item] = value

def __dir__(self):
metadata = list(self._ax.metadata)
Expand Down
32 changes: 29 additions & 3 deletions src/boost_histogram/_internal/hist.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from .. import _core
from .axis import Axis
from .axistuple import AxesTuple
from .axestuple import AxesTuple
from .kwargs import KWArgs
from .sig_tools import inject_signature
from .six import string_types
Expand Down Expand Up @@ -112,10 +112,11 @@ def __init__(self, *axes, **kwargs):
self.axes = self._generate_axes_()
return

# If we construct with another Histogram, support that too
# If we construct with another Histogram as the only positional argument,
# support that too
if len(axes) == 1 and isinstance(axes[0], Histogram):
self.__init__(axes[0]._hist)
self.metadata = axes[0].metadata
self._from_histogram_object(axes[0])
return

# Keyword only trick (change when Python2 is dropped)
Expand Down Expand Up @@ -151,6 +152,31 @@ def __init__(self, *axes, **kwargs):

raise TypeError("Unsupported storage")

def _from_histogram_object(self, h):
self.__dict__ = copy.copy(h.__dict__)
self.axes = self._generate_axes_()
for ax in self.axes:
ax._ax.metadata = copy.copy(ax._ax.metadata)

# Allow custom behavior on either "from" or "to"
h._export_bh_(self)
self._import_bh_()

def _import_bh_(self):
"""
If any post-processing is needed to pass a histogram between libraries, a
subclass can implement it here. self is the new instance in the current
(converted-to) class.
"""

@classmethod
def _export_bh_(cls, self):
"""
If any preparation is needed to pass a histogram between libraries, a subclass can
implement it here. cls is the current class being converted from, and self is the
instance in the class being converted to.
"""

def _generate_axes_(self):
"""
This is called to fill in the axes. Subclasses can override it if they need
Expand Down
2 changes: 1 addition & 1 deletion src/boost_histogram/axis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
StrCategory,
Boolean,
)
from .._internal.axistuple import ArrayTuple, AxesTuple
from .._internal.axestuple import ArrayTuple, AxesTuple
from .._core.axis import options
from . import transform

Expand Down
4 changes: 2 additions & 2 deletions tests/test_histogram_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def test_pick_int_category():

def test_axes_tuple():
h = bh.Histogram(bh.axis.Regular(10, 0, 1))
assert isinstance(h.axes[:1], bh._internal.axistuple.AxesTuple)
assert isinstance(h.axes[:1], bh._internal.axestuple.AxesTuple)
assert isinstance(h.axes[0], bh.axis.Regular)

(before,) = h.axes.centers[:1]
Expand All @@ -344,7 +344,7 @@ def test_axes_tuple_Nd():
h = bh.Histogram(
bh.axis.Integer(0, 5), bh.axis.Integer(0, 4), bh.axis.Integer(0, 6)
)
assert isinstance(h.axes[:2], bh._internal.axistuple.AxesTuple)
assert isinstance(h.axes[:2], bh._internal.axestuple.AxesTuple)
assert isinstance(h.axes[1], bh.axis.Integer)

b1, b2 = h.axes.centers[1:3]
Expand Down
145 changes: 145 additions & 0 deletions tests/test_minihist_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
# The point of this test is to make sure that the infrastructure for supporting
# custom attributes, like title in Hist, is working.

import pytest
import boost_histogram as bh


# First, make a new family to identify your library
CUSTOM_FAMILY = object()


# Add named axes
class NamedAxesTuple(bh.axis.AxesTuple):
__slots__ = ()

def _get_index_by_name(self, name):
if isinstance(name, str):
for i, ax in enumerate(self):
if ax.name == name:
return i
raise KeyError("{} not found in axes".format(name))
else:
return name

def __getitem__(self, item):
if isinstance(item, slice):
item = slice(
self._get_index_by_name(item.start),
self._get_index_by_name(item.stop),
self._get_index_by_name(item.step),
)
else:
item = self._get_index_by_name(item)

return super(NamedAxesTuple, self).__getitem__(item)

@property
def name(self):
"""
The names of the axes. May be empty strings.
"""
return tuple(ax.name for ax in self)


# When you subclass Histogram or an Axes, you should register your family so
# boost-histogram will know what to convert C++ objects into.


class AxesMixin(object):
__slots__ = ()

@property
def name(self):
"""
Get the name for the Regular axis
"""
return self._ax.metadata.get("name", "")


# The order of the mixin is important here - it must be first
# to override bh.axis.Regular
@bh.utils.set_family(CUSTOM_FAMILY)
class Regular(bh.axis.Regular, AxesMixin):
__slots__ = ()

def __init__(
self, bins, start, stop, name,
):

super(Regular, self).__init__(
bins, start, stop,
)

self._ax.metadata["name"] = name


@bh.utils.set_family(CUSTOM_FAMILY)
class CustomHist(bh.Histogram):
def _generate_axes_(self):
return NamedAxesTuple(self._axis(i) for i in range(self.ndim))

def __init__(self, *args, **kwargs):
super(CustomHist, self).__init__(*args, **kwargs)
valid_names = [ax.name for ax in self.axes if ax.name]
if len(valid_names) != len(set(valid_names)):
msg = "{} instance cannot contain axes with duplicated names".format(
self.__class__.__name__
)
raise KeyError(msg)


def test_hist_creation():
hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b"))
assert hist_1.axes[0].name == "a"
assert hist_1.axes[1].name == "b"

hist_2 = CustomHist(Regular(10, 0, 1, name=""), Regular(20, 0, 4, name=""))
assert hist_2.axes[0].name == ""
assert hist_2.axes[1].name == ""

with pytest.raises(KeyError):
CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="a"))


def test_hist_index():
hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b"))
assert hist_1.axes[0].name == "a"
assert hist_1.axes[1].name == "b"


def test_hist_convert():
hist_1 = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b"))
hist_bh = bh.Histogram(hist_1)

assert type(hist_bh.axes[0]) == bh.axis.Regular
assert hist_bh.axes[0].name == "a"
assert hist_bh.axes[1].name == "b"

hist_2 = CustomHist(hist_bh)

assert type(hist_2.axes[0]) == Regular
assert hist_2.axes[0].name == "a"
assert hist_2.axes[1].name == "b"

# Just verify no-op status
hist_3 = CustomHist(hist_1)

assert type(hist_3.axes[0]) == Regular
assert hist_3.axes[0].name == "a"
assert hist_3.axes[1].name == "b"


def test_access():
hist = CustomHist(Regular(10, 0, 1, name="a"), Regular(20, 0, 4, name="b"))

assert hist.axes["a"] == hist.axes[0]
assert hist.axes["b"] == hist.axes[1]

from_bh = bh.Histogram(bh.axis.Regular(10, 0, 1), bh.axis.Regular(20, 0, 4))
from_bh.axes.name = "a", "b"
hist_conv = CustomHist(from_bh)

assert hist_conv.axes["a"] == hist_conv.axes[0]
assert hist_conv.axes["b"] == hist_conv.axes[1]