diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 233ae3f7..0bf69e4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" @@ -78,7 +77,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version: '3.10' - name: Setup uv uses: astral-sh/setup-uv@v7 diff --git a/docs/user-guide/notebooks/SVGHistogram.ipynb b/docs/user-guide/notebooks/SVGHistogram.ipynb index 94455736..0e349ca3 100644 --- a/docs/user-guide/notebooks/SVGHistogram.ipynb +++ b/docs/user-guide/notebooks/SVGHistogram.ipynb @@ -235,7 +235,7 @@ "\n", " boxes = []\n", " for height, left_edge, right_edge in zip(\n", - " norm_vals, norm_edges[:-1], norm_edges[1:]\n", + " norm_vals, norm_edges[:-1], norm_edges[1:], strict=True\n", " ):\n", " boxes.append(\n", " rect.pad(\n", @@ -340,10 +340,13 @@ "\n", " boxes = []\n", " for r, up_edge, bottom_edge in zip(\n", - " range(len(norm_edges[1])), norm_edges[1][:-1], norm_edges[1][1:]\n", + " range(len(norm_edges[1])), norm_edges[1][:-1], norm_edges[1][1:], strict=True\n", " ):\n", " for c, left_edge, right_edge in zip(\n", - " range(len(norm_edges[0])), norm_edges[0][:-1], norm_edges[0][1:]\n", + " range(len(norm_edges[0])),\n", + " norm_edges[0][:-1],\n", + " norm_edges[0][1:],\n", + " strict=True,\n", " ):\n", " opacity = 1 - norm_vals[r][c]\n", " boxes.append(\n", diff --git a/noxfile.py b/noxfile.py index a6c4411f..81939883 100755 --- a/noxfile.py +++ b/noxfile.py @@ -55,6 +55,7 @@ def minimums(session): """ session.install("-e.", "--group=test", "--resolution=lowest-direct") + session.run("uv", "pip", "list") session.run("pytest", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml index 1d949654..d6f81dad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -38,11 +37,11 @@ keywords = [ "boost-histogram", "dask-histogram", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "boost-histogram>=1.5,<1.7", "histoprint>=2.2.0", - 'numpy>=1.19.3', + 'numpy>=1.21.3', 'typing-extensions>=4;python_version<"3.11"', ] dynamic = ["version"] @@ -60,16 +59,16 @@ Changelog = "https://hist.readthedocs.io/en/latest/changelog.html" [project.optional-dependencies] # Keep in sync with dependency-groups below mpl = [ - "matplotlib >=3.8", + "matplotlib >=3.10.7", "mplhep >=0.3.33", ] plot = [ - "matplotlib >=3.8", + "matplotlib >=3.10.7", "mplhep >=0.3.33", ] fit = [ - "scipy >=1.5.4", - "iminuit >=2", + "scipy >=1.7.0", + "iminuit >=2.9.0", ] dask = [ "dask[dataframe] >=2022,<2025", @@ -78,13 +77,12 @@ dask = [ [dependency-groups] plot = [ - "matplotlib >=3.8", + "matplotlib >=3.10.7", "mplhep >=0.3.33", - "pyparsing>=3.0,<3.3; python_version<'3.10'" ] fit = [ - "scipy >=1.5.4", - "iminuit >=2; python_version<'3.14'", + "scipy >=1.7.0", + "iminuit >=2.9.0", ] dask = [ "dask[dataframe] >=2022,<2025", @@ -141,7 +139,7 @@ filterwarnings = [ [tool.mypy] warn_unused_configs = true files = "src" -python_version = "3.9" +python_version = "3.10" strict = true enable_error_code = ["ignore-without-code", "truthy-bool", "redundant-expr"] warn_unreachable = true @@ -161,7 +159,7 @@ ignore_missing_imports = true [tool.pylint] -py-version = "3.9" +py-version = "3.10" extension-pkg-allow-list = ["boost_histogram._core"] reports.output-format = "colorized" similarities.ignore-imports = "yes" diff --git a/src/hist/_compat/builtins.py b/src/hist/_compat/builtins.py deleted file mode 100644 index c0c0967e..00000000 --- a/src/hist/_compat/builtins.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import sys - -if sys.version_info < (3, 10): - import builtins - import itertools - from collections.abc import Iterator - from typing import Any - - def zip(*iterables: Any, strict: bool = False) -> Iterator[tuple[Any, ...]]: - if strict: - marker = object() - for each in itertools.zip_longest(*iterables, fillvalue=marker): - for val in each: - if val is marker: - raise ValueError("zip() arguments are not the same length") - yield each - else: - yield from builtins.zip(*iterables) - -else: - from builtins import zip # noqa: UP029 - -__all__ = ["zip"] - - -def __dir__() -> list[str]: - return __all__ diff --git a/src/hist/axestuple.py b/src/hist/axestuple.py index fc0470a6..b2ca4998 100644 --- a/src/hist/axestuple.py +++ b/src/hist/axestuple.py @@ -6,8 +6,6 @@ from boost_histogram.axis import ArrayTuple, AxesTuple -from ._compat.builtins import zip - __all__ = ("ArrayTuple", "AxesTuple", "NamedAxesTuple") @@ -48,7 +46,6 @@ def name(self) -> tuple[str]: @name.setter def name(self, values: Iterable[str]) -> None: - # strict = True from Python 3.10 for ax, val in zip(self, values, strict=True): ax._raw_metadata["name"] = val diff --git a/src/hist/basehist.py b/src/hist/basehist.py index 191b0b62..511511cb 100644 --- a/src/hist/basehist.py +++ b/src/hist/basehist.py @@ -6,10 +6,9 @@ import operator import typing import warnings -from collections.abc import Generator, Iterator, Mapping, Sequence +from collections.abc import Callable, Generator, Iterator, Mapping, Sequence from typing import ( Any, - Callable, Protocol, SupportsIndex, Union, @@ -45,9 +44,9 @@ def __lt__(self, __other: Any) -> bool: ... InnerIndexing = Union[ SupportsIndex, str, Callable[[bh.axis.Axis], int], slice, "ellipsis" ] -IndexingWithMapping = Union[InnerIndexing, Mapping[Union[int, str], InnerIndexing]] -IndexingExpr = Union[IndexingWithMapping, tuple[IndexingWithMapping, ...]] -AxisTypes = Union[AxisProtocol, tuple[int, float, float]] +IndexingWithMapping = InnerIndexing | Mapping[int | str, InnerIndexing] +IndexingExpr = IndexingWithMapping | tuple[IndexingWithMapping, ...] +AxisTypes = AxisProtocol | tuple[int, float, float] # Workaround for bug in mplhep @@ -234,11 +233,11 @@ def from_columns( for ax in axes: if isinstance(ax, str): assert ax in data, f"{ax} must be present in data={list(data)}" - cats = set(data[ax]) + cats = set(data[ax]) # type: ignore[arg-type] if all(isinstance(a, str) for a in cats): - axes_list.append(hist.axis.StrCategory(sorted(cats), name=ax)) + axes_list.append(hist.axis.StrCategory(sorted(cats), name=ax)) # type: ignore[arg-type] elif all(isinstance(a, int) for a in cats): - axes_list.append(hist.axis.IntCategory(sorted(cats), name=ax)) + axes_list.append(hist.axis.IntCategory(sorted(cats), name=ax)) # type: ignore[arg-type] else: raise TypeError( f"{ax} must be all int or strings if axis not given" @@ -252,7 +251,7 @@ def from_columns( self = cls(*axes_list, storage=storage) data_list = {x.name: data[x.name] for x in axes_list} - self.fill(**data_list, weight=weight_arr) + self.fill(**data_list, weight=weight_arr) # type: ignore[arg-type] return self def project(self, *args: int | str) -> Self | float | bh.accumulators.Accumulator: @@ -311,7 +310,7 @@ def fill( and args and hasattr(weight, "__len__") and hasattr(args[0], "__len__") - and len(weight) != len(args[0]) + and len(weight) != len(args[0]) # type: ignore[arg-type] ): raise ValueError( "Weight array must match the length of the input data for a given data" @@ -350,7 +349,7 @@ def fill_flattened( user_args_broadcast = broadcast[:1] user_kwargs_broadcast = {} non_user_kwargs_broadcast = dict( - zip(non_user_kwargs.keys(), broadcast[1:]) + zip(non_user_kwargs.keys(), broadcast[1:], strict=True) ) else: # Result must be broadcast, so unpack and rebuild @@ -361,11 +360,13 @@ def fill_flattened( user_args_broadcast = () user_kwargs_broadcast = { k: v - for k, v in zip(destructured, broadcast[: len(destructured)]) + for k, v in zip( + destructured, broadcast[: len(destructured)], strict=True + ) if k in axis_names } non_user_kwargs_broadcast = dict( - zip(non_user_kwargs, broadcast[len(destructured) :]) + zip(non_user_kwargs, broadcast[len(destructured) :], strict=True) ) # Multiple args: broadcast and flatten! else: @@ -373,10 +374,10 @@ def fill_flattened( broadcast = interop.broadcast_and_flatten(inputs) user_args_broadcast = broadcast[: len(args)] user_kwargs_broadcast = dict( - zip(kwargs, broadcast[len(args) : len(args) + len(kwargs)]) + zip(kwargs, broadcast[len(args) : len(args) + len(kwargs)], strict=True) ) non_user_kwargs_broadcast = dict( - zip(non_user_kwargs, broadcast[len(args) + len(kwargs) :]) + zip(non_user_kwargs, broadcast[len(args) + len(kwargs) :], strict=True) ) return self.fill( *user_args_broadcast, @@ -734,7 +735,7 @@ def stack(self, axis: int | str) -> hist.stack.Stack: stack_histograms: Iterator[BaseHist] = [ # type: ignore[assignment] self[{axis: i}] for i in range(len(self.axes[axis])) ] - for name, h in zip(self.axes[axis], stack_histograms): + for name, h in zip(self.axes[axis], stack_histograms, strict=True): h.name = name return hist.stack.Stack(*stack_histograms) diff --git a/src/hist/interop.py b/src/hist/interop.py index 9663789f..c97343be 100644 --- a/src/hist/interop.py +++ b/src/hist/interop.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Iterator, Sequence -from typing import Any, Callable, Protocol, TypeVar, cast +from collections.abc import Callable, Iterator, Sequence +from typing import Any, Protocol, TypeVar, cast import numpy as np diff --git a/src/hist/namedhist.py b/src/hist/namedhist.py index 8561d9e0..aa802cb8 100644 --- a/src/hist/namedhist.py +++ b/src/hist/namedhist.py @@ -98,19 +98,23 @@ def fill_flattened( # type: ignore[override] # Partition into user and non-user args user_kwargs_broadcast = { k: v - for k, v in zip(destructured, broadcast[: len(destructured)]) + for k, v in zip( + destructured, broadcast[: len(destructured)], strict=True + ) if k in axis_names } non_user_kwargs_broadcast = dict( - zip(non_user_kwargs, broadcast[len(destructured) :]) + zip(non_user_kwargs, broadcast[len(destructured) :], strict=True) ) # Multiple args: broadcast and flatten! else: inputs = (*kwargs.values(), *non_user_kwargs.values()) broadcast = interop.broadcast_and_flatten(inputs) - user_kwargs_broadcast = dict(zip(kwargs, broadcast[: len(kwargs)])) + user_kwargs_broadcast = dict( + zip(kwargs, broadcast[: len(kwargs)], strict=True) + ) non_user_kwargs_broadcast = dict( - zip(non_user_kwargs, broadcast[len(kwargs) :]) + zip(non_user_kwargs, broadcast[len(kwargs) :], strict=True) ) return self.fill( **user_kwargs_broadcast, diff --git a/src/hist/plot.py b/src/hist/plot.py index 6df39574..444efa35 100644 --- a/src/hist/plot.py +++ b/src/hist/plot.py @@ -2,8 +2,8 @@ import inspect import sys -from collections.abc import Iterable -from typing import Any, Callable, Literal, NamedTuple, Union +from collections.abc import Callable, Iterable +from typing import Any, Literal, NamedTuple, TypeAlias import numpy as np @@ -56,10 +56,10 @@ class PullArtists(NamedTuple): patch_artist: list[matplotlib.patches.Rectangle] -MainAxisArtists = Union[FitResultArtists, Hist1DArtists] +MainAxisArtists: TypeAlias = FitResultArtists | Hist1DArtists -RatioArtists = Union[RatioErrorbarArtists, RatioBarArtists] -RatiolikeArtists = Union[RatioArtists, PullArtists] +RatioArtists = RatioErrorbarArtists | RatioBarArtists +RatiolikeArtists = RatioArtists | PullArtists def __dir__() -> tuple[str, ...]: @@ -694,7 +694,7 @@ def _plot_ratiolike( perr = np.sqrt(np.diagonal(pcov)) fp_label = "Fit" - for name, value, error in zip(parnames, popt, perr): + for name, value, error in zip(parnames, popt, perr, strict=True): fp_label += "\n " fp_label += fit_fmt.format(name=name, value=value, error=error) fp_kwargs["label"] = fp_label diff --git a/src/hist/quick_construct.py b/src/hist/quick_construct.py index 999e69ac..24609368 100644 --- a/src/hist/quick_construct.py +++ b/src/hist/quick_construct.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Any import numpy as np diff --git a/src/hist/svgplots.py b/src/hist/svgplots.py index 981a35ff..9149e176 100644 --- a/src/hist/svgplots.py +++ b/src/hist/svgplots.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import Any, Callable +import itertools +from collections.abc import Callable +from typing import Any import numpy as np from boost_histogram.axis import Axis @@ -70,7 +72,7 @@ def svg_hist_1d(h: hist.BaseHist) -> svg: (edges,) = h.axes.edges norm_edges = (edges - edges[0]) / (edges[-1] - edges[0]) density = h.density() - max_dens: float = np.amax(density) or 1 # type: ignore[redundant-expr, unreachable] + max_dens: float = np.amax(density) or 1 norm_vals: np.typing.NDArray[Any] = density / max_dens arr: np.typing.NDArray[np.float64] = np.empty( @@ -121,7 +123,7 @@ def svg_hist_1d_c(h: hist.BaseHist) -> svg: (edges,) = h.axes.edges norm_edges = (edges - edges[0]) / (edges[-1] - edges[0]) * np.pi * 2 density = h.density() - max_dens = np.amax(density) or 1 # type: ignore[redundant-expr, var-annotated, unreachable] + max_dens = np.amax(density) or 1 norm_vals: np.typing.NDArray[Any] = density / max_dens arr: np.typing.NDArray[np.float64] = np.empty((2, len(norm_vals) * 2), dtype=float) @@ -132,7 +134,7 @@ def svg_hist_1d_c(h: hist.BaseHist) -> svg: xs = arr[1] * np.cos(arr[0]) ys = arr[1] * np.sin(arr[0]) - points = " ".join(f"{x:3g},{y:.3g}" for x, y in zip(xs, ys)) + points = " ".join(f"{x:3g},{y:.3g}" for x, y in zip(xs, ys, strict=True)) bins = polygon(points=points, style="fill:none; stroke:currentColor;") center = circle( @@ -155,13 +157,13 @@ def svg_hist_2d(h: hist.BaseHist) -> svg: ey = -(e1 - e1[0]) / (e1[-1] - e1[0]) * height density = h.density() - max_dens = np.amax(density) or 1 # type: ignore[redundant-expr, var-annotated, unreachable] + max_dens = np.amax(density) or 1 norm_vals: np.typing.NDArray[Any] = density / max_dens boxes = [] - for r, (up_edge, bottom_edge) in enumerate(zip(ey[:-1], ey[1:])): + for r, (up_edge, bottom_edge) in enumerate(itertools.pairwise(ey)): ht = up_edge - bottom_edge - for c, (left_edge, right_edge) in enumerate(zip(ex[:-1], ex[1:])): + for c, (left_edge, right_edge) in enumerate(itertools.pairwise(ex)): opacity = norm_vals[c, r] wt = left_edge - right_edge boxes.append( diff --git a/tests/test_dask.py b/tests/test_dask.py index ab655bb5..f4806b7b 100644 --- a/tests/test_dask.py +++ b/tests/test_dask.py @@ -61,11 +61,11 @@ def test_unnamed_5D_strcat_intcat_rectangular(unnamed_dask_hist, use_weights): assert len(h.axes[0]) == 2 assert len(control.axes[0]) == 2 - assert all(cx == hx for cx, hx in zip(control.axes[0], h.axes[0])) + assert all(cx == hx for cx, hx in zip(control.axes[0], h.axes[0], strict=True)) assert len(h.axes[1]) == 2 assert len(control.axes[1]) == 2 - assert all(cx == hx for cx, hx in zip(control.axes[1], h.axes[1])) + assert all(cx == hx for cx, hx in zip(control.axes[1], h.axes[1], strict=True)) @pytest.mark.parametrize("use_weights", [True, False]) @@ -120,8 +120,8 @@ def test_named_5D_strcat_intcat_rectangular(named_dask_hist, use_weights): assert len(h.axes[0]) == 2 assert len(control.axes[0]) == 2 - assert all(cx == hx for cx, hx in zip(control.axes[0], h.axes[0])) + assert all(cx == hx for cx, hx in zip(control.axes[0], h.axes[0], strict=True)) assert len(h.axes[1]) == 2 assert len(control.axes[1]) == 2 - assert all(cx == hx for cx, hx in zip(control.axes[1], h.axes[1])) + assert all(cx == hx for cx, hx in zip(control.axes[1], h.axes[1], strict=True)) diff --git a/tests/test_stacks.py b/tests/test_stacks.py index 627fca91..0e6a8ac1 100644 --- a/tests/test_stacks.py +++ b/tests/test_stacks.py @@ -58,7 +58,7 @@ ids = ("reg", "boo", "var", "int", "icat", "scat") -@pytest.fixture(params=zip(axs, fills), ids=ids) +@pytest.fixture(params=zip(axs, fills, strict=True), ids=ids) def hist_1d(request): def make_hist(): ax, fill = request.param