Skip to content

Commit 7752bb0

Browse files
committed
Add type annotations throughout the package
Public API and every plain helper are annotated; the numba jit internals stay untyped, since their signatures belong to numba, not to python. The package now ships a py.typed marker, so the annotations reach type checkers of downstream projects. Types are conservative: inputs are ArrayLike, aggregate returns Any (an ndarray in most implementations, a list in pure python), and the kernel functions of the numpy/ufunc/pandas backends carry their uniform (group_idx, a, size, fill_value, dtype) signature. mypy passes on everything except pre-existing dynamic patterns (numba's operation classes, the tests/__init__ module stubs and the benchmarks).
1 parent 3b85b00 commit 7752bb0

10 files changed

Lines changed: 414 additions & 151 deletions

‎numpy_groupies/__init__.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
15
from .aggregate_purepy import aggregate as aggregate_py
26

7+
if TYPE_CHECKING:
8+
import numpy.typing as npt
9+
310

4-
def dummy_no_impl(*args, **kwargs):
11+
def dummy_no_impl(*args: Any, **kwargs: Any) -> Any:
512
raise NotImplementedError(
613
"You may need to install another package (numpy or numba) to access a working implementation."
714
)
@@ -29,6 +36,7 @@ def dummy_no_impl(*args, **kwargs):
2936
)
3037

3138

39+
aggregate_nb: Any
3240
try:
3341
import numba
3442
except ImportError:
@@ -40,7 +48,7 @@ def dummy_no_impl(*args, **kwargs):
4048
aggregate = aggregate_nb
4149

4250

43-
def uaggregate(group_idx, a, out=None, **kwargs):
51+
def uaggregate(group_idx: npt.ArrayLike, a: npt.ArrayLike, out: Any = None, **kwargs: Any) -> Any:
4452
"""
4553
Aggregate the values of ``a`` by the groups in ``group_idx`` and broadcast the result
4654
back to the size of ``a``, so that every element carries the result of its own group.

‎numpy_groupies/aggregate_common.py‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
before numpy is known to be available, and has to work without it.
55
"""
66

7+
from __future__ import annotations
8+
9+
from collections.abc import Callable, Mapping
10+
from typing import Any
11+
712
nan = float("nan") # nan is just this float
813

914

@@ -127,7 +132,7 @@ def __reduce__(self):
127132
return (_get_default_fill_value, ())
128133

129134

130-
def _get_default_fill_value():
135+
def _get_default_fill_value() -> _DefaultFillValue:
131136
return DEFAULT_FILL_VALUE
132137

133138

@@ -169,7 +174,7 @@ def _get_default_fill_value():
169174
}
170175

171176

172-
def _fill_value_key(func):
177+
def _fill_value_key(func: str | Callable[..., Any]) -> str:
173178
"""Map a function (name, alias or callable) onto a ``_default_fill_values`` key."""
174179
try:
175180
name = aliasing_py[func]
@@ -181,7 +186,7 @@ def _fill_value_key(func):
181186
return name
182187

183188

184-
def _is_inexact_dtype(dtype):
189+
def _is_inexact_dtype(dtype: Any) -> bool:
185190
"""True for everything that can hold nan - numpy dtypes by kind, python types by identity."""
186191
kind = getattr(dtype, "kind", None)
187192
if kind is not None: # a numpy dtype
@@ -191,7 +196,7 @@ def _is_inexact_dtype(dtype):
191196
return isinstance(dtype, type) and issubclass(dtype, (float, complex))
192197

193198

194-
def _is_complex_dtype(dtype):
199+
def _is_complex_dtype(dtype: Any) -> bool:
195200
"""True for complex numpy dtypes, the complex builtin and complex dtype strings."""
196201
kind = getattr(dtype, "kind", None)
197202
if kind is not None: # a numpy dtype
@@ -201,7 +206,7 @@ def _is_complex_dtype(dtype):
201206
return isinstance(dtype, type) and issubclass(dtype, complex)
202207

203208

204-
def resolve_fill_value(func, fill_value, dtype):
209+
def resolve_fill_value(func: str | Callable[..., Any], fill_value: Any, dtype: Any) -> Any:
205210
"""Replace the ``DEFAULT_FILL_VALUE`` sentinel with the default of ``func``.
206211
207212
Anything else than the sentinel is returned untouched. ``nan`` is only
@@ -270,7 +275,7 @@ def resolve_fill_value(func, fill_value, dtype):
270275
}
271276

272277

273-
def get_aliasing(*extra):
278+
def get_aliasing(*extra: dict[Any, str]) -> dict[Any, str]:
274279
"""
275280
Assembles a dictionary that maps both strings and functions to a list of supported function names.
276281
@@ -280,7 +285,7 @@ def get_aliasing(*extra):
280285
281286
This function should only be called during import.
282287
"""
283-
alias = {k: k for k in funcs_common}
288+
alias: dict[Any, str] = {k: k for k in funcs_common}
284289
alias.update(_alias_str)
285290
alias.update((fn, fn) for fn in _alias_builtin.values())
286291
alias.update(_alias_builtin)
@@ -298,7 +303,9 @@ def get_aliasing(*extra):
298303
aliasing_py = get_aliasing()
299304

300305

301-
def get_func(func, aliasing, implementations):
306+
def get_func(
307+
func: str | Callable[..., Any], aliasing: dict[Any, Any], implementations: Any
308+
) -> str | Callable[..., Any]:
302309
"""Return the key of a found implementation or the func itself"""
303310
try:
304311
func_str = aliasing[func]
@@ -315,7 +322,9 @@ def get_func(func, aliasing, implementations):
315322
raise ValueError(f"func {func} is neither a valid function string nor a callable object")
316323

317324

318-
def build_dispatch(impl_dict, aliasing):
325+
def build_dispatch(
326+
impl_dict: Mapping[str, Callable[..., Any]], aliasing: Mapping[Any, str]
327+
) -> dict[Any, tuple[str, Callable[..., Any]]]:
319328
"""Fuse an aliasing and an implementation table for single-lookup dispatch.
320329
321330
Returns a dict mapping every alias (string or callable) onto a
@@ -379,7 +388,7 @@ def build_dispatch(impl_dict, aliasing):
379388
)
380389

381390

382-
def check_complex_dtype(a_dtype, dtype, func_str):
391+
def check_complex_dtype(a_dtype: Any, dtype: Any, func_str: str) -> None:
383392
"""Complain when a non-complex ``dtype`` is requested for complex input.
384393
385394
``resolve_output_dtype`` applies this for the backends that honour ``dtype``;

‎numpy_groupies/aggregate_numba.py‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import functools
22
import hashlib
33
import inspect
4+
import typing
5+
from collections.abc import Callable, Sequence
46

57
import numba as nb
68
import numpy as np
9+
import numpy.typing as npt
710

811
from .aggregate_common import (
912
DEFAULT_FILL_VALUE,
@@ -1041,18 +1044,18 @@ def _dtype_fill_plan(op, a_key, dtype, fill_value, n):
10411044

10421045

10431046
def aggregate(
1044-
group_idx,
1045-
a,
1046-
func="sum",
1047-
size=None,
1048-
fill_value=DEFAULT_FILL_VALUE,
1049-
order="C",
1050-
dtype=None,
1051-
axis=None,
1052-
out=None,
1053-
cache=True,
1054-
**kwargs,
1055-
):
1047+
group_idx: npt.ArrayLike,
1048+
a: npt.ArrayLike,
1049+
func: str | Callable[..., typing.Any] = "sum",
1050+
size: int | Sequence[int] | None = None,
1051+
fill_value: typing.Any = DEFAULT_FILL_VALUE,
1052+
order: str = "C",
1053+
dtype: npt.DTypeLike = None,
1054+
axis: int | None = None,
1055+
out: np.ndarray | None = None,
1056+
cache: bool = True,
1057+
**kwargs: typing.Any,
1058+
) -> typing.Any:
10561059
try:
10571060
aggregate_op = _dispatch[func][1]
10581061
except (KeyError, TypeError):

0 commit comments

Comments
 (0)