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
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ jobs:
- name: Test plotting too
run: .venv/bin/python -m pytest --mpl

mypy:
name: Type check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Setup uv
uses: astral-sh/setup-uv@v8.1.0

- name: Install nox
run: uv tool install nox

- name: Run mypy
run: nox -s mypy

minimums:
name: Check minimums
runs-on: ubuntu-latest
Expand All @@ -87,7 +102,7 @@ jobs:

pass:
if: always()
needs: [pylint, checks, minimums]
needs: [pylint, checks, mypy, minimums]
runs-on: ubuntu-slim
timeout-minutes: 2
steps:
Expand Down
13 changes: 13 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ def tests(session):
session.run("pytest", *args, *session.posargs)


@nox.session(python="3.10", venv_backend="uv")
def mypy(session):
"""
Type check. Pinned to Python 3.10, where NumPy resolves to <2.3 and so
``ndarray`` has no generic defaults -- this catches bare ``np.ndarray``
annotations that newer NumPy stubs silently accept. Mirrors the type check
in boost-histogram's downstream "hist" job.
"""

session.install("-e.", "--group=test", "--group=plot", "mypy", "pandas-stubs")
session.run("mypy", *session.posargs)


@nox.session(venv_backend="uv", default=False)
def minimums(session):
"""
Expand Down
22 changes: 13 additions & 9 deletions src/hist/chunked.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def _validate_dense_view(
*,
shape: tuple[int, ...],
dtype: np.dtype[tp.Any],
) -> np.ndarray:
) -> np.typing.NDArray[Any]:
array = np.asarray(view)
if array.shape != shape:
msg = f"dense view shape mismatch: expected {shape}, got {array.shape}"
Expand All @@ -68,23 +68,25 @@ def _validate_dense_view(
return array


def _accumulate_dense_view(target: np.ndarray, source: np.ndarray) -> None:
def _accumulate_dense_view(
target: np.typing.NDArray[Any], source: np.typing.NDArray[Any]
) -> None:
if target.dtype.fields is None:
target[...] += source
return
for field_name in target.dtype.names or ():
_accumulate_dense_view(target[field_name], source[field_name])


def _zero_dense_view(view: np.ndarray) -> None:
def _zero_dense_view(view: np.typing.NDArray[Any]) -> None:
if view.dtype.fields is None:
view.fill(0)
return
for field_name in view.dtype.names or ():
_zero_dense_view(view[field_name])


def _view_any_nonzero(view: np.ndarray) -> bool:
def _view_any_nonzero(view: np.typing.NDArray[Any]) -> bool:
if view.dtype.fields is None:
return bool(np.any(view))
return any(
Expand Down Expand Up @@ -168,7 +170,7 @@ class ChunkedHist:
_dense_view_nbytes: int = field(init=False)
_chunk_axis_names: tuple[str, ...] = field(init=False)
_scratch_dense_hist: hist.Hist[Any] = field(init=False)
_chunks: dict[ChunkKey, np.ndarray] = field(default_factory=dict)
_chunks: dict[ChunkKey, np.typing.NDArray[Any]] = field(default_factory=dict)

def __init__(
self,
Expand Down Expand Up @@ -327,7 +329,9 @@ def dense_view_dtype(self) -> np.dtype[tp.Any]:
def dense_axes(self) -> tuple[tp.Any, ...]:
return tuple(self._scratch_dense_hist.axes)

def _save_chunk_view(self, key: ChunkKey, chunk_view: np.ndarray) -> None:
def _save_chunk_view(
self, key: ChunkKey, chunk_view: np.typing.NDArray[Any]
) -> None:
array = _validate_dense_view(
chunk_view,
shape=self.dense_view_shape,
Expand Down Expand Up @@ -373,7 +377,7 @@ def split_fill_kwargs(
if name not in self.chunk_axis_names
}

def add_dense_view(self, key: ChunkKey, dense_view: np.ndarray) -> None:
def add_dense_view(self, key: ChunkKey, dense_view: np.typing.NDArray[Any]) -> None:
self._check_chunk_key(key)
dense_view = _validate_dense_view(
dense_view,
Expand Down Expand Up @@ -493,7 +497,7 @@ def empty_like(self) -> Self:
label=self.label,
)

def items(self) -> Iterable[tuple[ChunkKey, np.ndarray]]:
def items(self) -> Iterable[tuple[ChunkKey, np.typing.NDArray[Any]]]:
"""Iterate over ``(chunk key, chunk array)`` pairs.

Like :meth:`chunk_view`, the yielded arrays are live views of the
Expand Down Expand Up @@ -559,7 +563,7 @@ def selection_dict(self, key: ChunkKey) -> dict[str, ChunkScalar]:
def chunk_view(
self,
selection: Mapping[str, ChunkScalar | tp.Iterable[ChunkScalar]],
) -> np.ndarray:
) -> np.typing.NDArray[Any]:
"""Return the live array for one chunk.

The returned array is a view of the internal storage; mutating it
Expand Down