diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index eaf11ae09..5fd040ef1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,18 @@ ## Version 1.2 +### Version 1.2.2 + +#### User changes + +* PyPy 3.8 now supported. [#677][] +* The GIL is released a little more often now. [#662][] +* AxesTuple does not allow construction of non-axes. [#680][] + +[#662]: https://github.com/scikit-hep/boost-histogram/pull/662 +[#677]: https://github.com/scikit-hep/boost-histogram/pull/677 +[#680]: https://github.com/scikit-hep/boost-histogram/pull/680 + ### Version 1.2.1 #### User changes diff --git a/src/boost_histogram/_internal/axestuple.py b/src/boost_histogram/_internal/axestuple.py index 909ca2a32..de6b4fdad 100644 --- a/src/boost_histogram/_internal/axestuple.py +++ b/src/boost_histogram/_internal/axestuple.py @@ -1,5 +1,5 @@ from functools import partial -from typing import Any, List, Tuple, TypeVar +from typing import Any, Iterable, List, Tuple, TypeVar import numpy as np @@ -45,6 +45,13 @@ class AxesTuple(tuple): # type: ignore[type-arg] __slots__ = () _MGRIDOPTS = {"sparse": True, "indexing": "ij"} + def __init__(self, __iterable: Iterable[Axis]) -> None: + for item in self: + if not isinstance(item, Axis): + raise TypeError( + f"Only an iterable of Axis supported in AxesTuple, got {item}" + ) + @property def size(self) -> Tuple[int, ...]: return tuple(s.size for s in self) @@ -93,14 +100,15 @@ def __getitem__(self, item: Any) -> Any: result = super().__getitem__(item) return self.__class__(result) if isinstance(result, tuple) else result - def __getattr__(self, attr: str) -> Any: - return self.__class__(getattr(s, attr) for s in self) + def __getattr__(self, attr: str) -> Tuple[Any, ...]: + return tuple(getattr(s, attr) for s in self) def __setattr__(self, attr: str, values: Any) -> None: try: return super().__setattr__(attr, values) except AttributeError: - self.__class__(s.__setattr__(attr, v) for s, v in zip_strict(self, values)) + for s, v in zip_strict(self, values): + s.__setattr__(attr, v) value.__doc__ = Axis.value.__doc__ index.__doc__ = Axis.index.__doc__ diff --git a/tests/test_axes_object.py b/tests/test_axes_object.py index 0528a3119..e67a94d09 100644 --- a/tests/test_axes_object.py +++ b/tests/test_axes_object.py @@ -1,9 +1,19 @@ import numpy as np +import pytest import boost_histogram as bh -def test_axes_all_at_once(): +@pytest.fixture +def h(): + return bh.Histogram( + bh.axis.Regular(10, 0, 10, metadata=2), + bh.axis.Integer(0, 5, metadata="hi"), + bh.axis.StrCategory(["HI", "HO"]), + ) + + +def test_axes_basics(h): h = bh.Histogram( bh.axis.Regular(10, 0, 10, metadata=2), bh.axis.Integer(0, 5, metadata="hi"), @@ -22,6 +32,8 @@ def test_axes_all_at_once(): assert h.axes.metadata == (None, 3, "bye") + +def test_axes_centers(h): centers = h.axes.centers answers = np.ogrid[0.5:10, 0.5:5, 0.5:2] full_answers = np.mgrid[0.5:10, 0.5:5, 0.5:2] @@ -33,6 +45,8 @@ def test_axes_all_at_once(): np.testing.assert_allclose(centers.flatten()[i], answers[i].flatten()) np.testing.assert_allclose(h.axes[i].centers, answers[i].ravel()) + +def test_axes_edges(h): edges = h.axes.edges answers = np.ogrid[0:11, 0:6, 0:3] full_answers = np.mgrid[0:11, 0:6, 0:3] @@ -44,6 +58,8 @@ def test_axes_all_at_once(): np.testing.assert_allclose(edges.ravel()[i], answers[i].ravel()) np.testing.assert_allclose(h.axes[i].edges, answers[i].ravel()) + +def test_axes_widths(h): widths = h.axes.widths answers = np.ogrid[1:1:10j, 1:1:5j, 1:1:2j] full_answers = np.mgrid[1:1:10j, 1:1:5j, 1:1:2j] @@ -54,3 +70,12 @@ def test_axes_all_at_once(): np.testing.assert_allclose(widths.T[i], answers[i].T) np.testing.assert_allclose(widths.ravel()[i], answers[i].ravel()) np.testing.assert_allclose(h.axes[i].widths, answers[i].ravel()) + + +def test_axis_misconstuct(): + inp = [bh.axis.Regular(12, 0, 1)] + ok = bh.axis.AxesTuple(inp) + assert ok[0] == inp[0] + + with pytest.raises(TypeError): + bh.axis.AxesTuple(inp[0])