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
23 changes: 9 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,16 @@ jobs:
contents: read # required for checkout

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: actions/setup-python@v5
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
enable-cache: true

- name: Build stubs
run: |
pip install mypy
stubgen --include-docstrings src/ -o src/
- name: Build package
run: uv build

- name: Build wheels
run: |
pip install build
python3 -m build --wheel

- name: Upload to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Publish to PyPI
# uv publish automatically detects the OIDC token in GitHub Actions
run: uv publish
26 changes: 11 additions & 15 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v4
- name: Run pre-commit
uses: pre-commit/action@v3.0.1
uses: actions/checkout@v6
- name: Run prek
uses: j178/prek-action@v2

build:
needs: [lint]
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.12"]
python-version: ["3.10", "3.14"]
steps:
- uses: actions/setup-python@v5
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- uses: actions/checkout@v4

- name: Test with tox
- name: Test
run: |
pip install tox
tox -- --cov npx --cov-report xml --cov-report term

- name: Submit to codecov
uses: codecov/codecov-action@v4
if: ${{ matrix.python-version == '3.9' }}
uv run pytest --cov npx --cov-report term
9 changes: 5 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.4.10
rev: v0.15.12
hooks:
- id: ruff
- id: ruff-check
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier

- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.4.2
hooks:
- id: codespell
# args: ["-L", "sur,nd"]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.20.2
hooks:
- id: mypy
files: src
Expand Down
9 changes: 6 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ clean:
@rm -rf src/*.egg-info/ build/ dist/ .tox/ .mypy_cache/

format:
ruff --fix src/ tests/
black src/ tests/
ruff format src/ tests/
ruff check --fix src/ tests/
blacken-docs README.md

lint:
pre-commit run --all
prek run --all-files

test *args:
uv run pytest -W error {{args}}
35 changes: 22 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,56 @@ build-backend = "setuptools.build_meta"

[project]
name = "npx"
version = "0.1.6"
authors = [{name = "Nico Schlömer", email = "nico.schloemer@gmail.com"}]
version = "0.1.7"
authors = [{ name = "Nico Schlömer", email = "nico.schloemer@gmail.com" }]
description = "Some useful extensions for NumPy"
readme = "README.md"
license = {file = "LICENSE"}
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
"Topic :: Utilities",
]
requires-python = ">=3.7"
requires-python = ">=3.10"
dependencies = [
"numpy >= 1.20.0",
# "scipy >= 1.8",
]

[dependency-groups]
dev = ["pytest", "pytest-cov"]

[project.urls]
Code = "https://github.com/sigma-py/npx"
Issues = "https://github.com/sigma-py/npx/issues"

[tool.ruff]
src = ["src", "tests"]
line-length = 88
select = ["ALL"]
ignore = [
"ANN", "S101", "D", "T201", "ERA", "N803", "PLR2004"
# "ANN", "C901", "D", "E741", "ERA", "FBT", "INP001",
# "N", "PLR", "S101", "T201", "TID252", "TD", "FIX002"
lint.select = ["ALL"]
lint.ignore = [
"ANN",
"COM812",
"S101",
"D",
"T201",
"ERA",
"N803",
"PLR2004",
# "ANN", "C901", "D", "E741", "ERA", "FBT", "INP001",
# "N", "PLR", "S101", "T201", "TID252", "TD", "FIX002"
]
target-version = "py38"
target-version = "py310"

[tool.mypy]
ignore_missing_imports = true
9 changes: 4 additions & 5 deletions src/npx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
from ._unique import unique, unique_rows

__all__ = [
"add_at",
"dot",
"isin_rows",
"mean",
"outer",
"solve",
"sum_at",
"add_at",
"subtract_at",
"unique_rows",
"isin_rows",
"mean",
"sum_at",
"unique",
"unique_rows",
]
18 changes: 18 additions & 0 deletions src/npx/_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import warnings
from functools import wraps


def deprecated(reason: str):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__}() is deprecated: {reason}",
DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)

return wrapper

return decorator
26 changes: 16 additions & 10 deletions src/npx/_main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
from functools import reduce
from operator import mul
from math import prod

import numpy as np
from numpy.typing import ArrayLike


# math.prod in 3.8
# https://docs.python.org/3/library/math.html#math.prod
def _prod(a):
return reduce(mul, a, 1)
from ._helpers import deprecated


def dot(a: ArrayLike, b: np.ndarray) -> np.ndarray:
Expand Down Expand Up @@ -36,6 +31,7 @@ def solve(A: np.ndarray, x: np.ndarray) -> np.ndarray:
return np.linalg.solve(A, x.reshape(x.shape[0], -1)).reshape(x.shape)


@deprecated("np.add.at is now equally fast. Use that.")
def sum_at(a: ArrayLike, indices: ArrayLike, minlength: int):
"""Sums up values `a` with `indices` into an output array of at least
length `minlength` while treating dimensionality correctly. It's a lot
Expand All @@ -49,6 +45,10 @@ def sum_at(a: ArrayLike, indices: ArrayLike, minlength: int):
`indices` may have arbitrary shape, too, but then `a` has to start out the
same. (Those dimensions are flattened out in the computation.)
"""
return _sum_at(a, indices, minlength)


def _sum_at(a: ArrayLike, indices: ArrayLike, minlength: int):
a = np.asarray(a)
indices = np.asarray(indices)

Expand All @@ -65,7 +65,7 @@ def sum_at(a: ArrayLike, indices: ArrayLike, minlength: int):
out_shape = (minlength, *a.shape[m:])

indices = indices.reshape(-1)
a = a.reshape(_prod(a.shape[:m]), _prod(a.shape[m:]))
a = a.reshape(prod(a.shape[:m]), prod(a.shape[m:]))

# Cast to int; bincount doesn't work for uint64 yet
# https://github.com/numpy/numpy/issues/17760
Expand All @@ -79,16 +79,22 @@ def sum_at(a: ArrayLike, indices: ArrayLike, minlength: int):
).T.reshape(out_shape)


@deprecated("np.add.at is now equally fast. Use that.")
def add_at(a: ArrayLike, indices: ArrayLike, b: ArrayLike):
return _add_at(a, indices, b)


def _add_at(a: ArrayLike, indices: ArrayLike, b: ArrayLike):
a = np.asarray(a)
indices = np.asarray(indices)
b = np.asarray(b)

m = len(indices.shape)
assert a.shape[1:] == b.shape[m:]
a += sum_at(b, indices, a.shape[0])
a += _sum_at(b, indices, a.shape[0])


@deprecated("np.subtract.at is now equally fast. Use that.")
def subtract_at(a: ArrayLike, indices: ArrayLike, b: ArrayLike):
b = np.asarray(b)
add_at(a, indices, -b)
_add_at(a, indices, -b)
5 changes: 4 additions & 1 deletion src/npx/_unique.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
from collections.abc import Callable

from numpy.typing import ArrayLike


Expand Down Expand Up @@ -49,6 +51,7 @@ def unique(


def unique_rows(a: ArrayLike, **kwargs) -> np.ndarray | tuple[np.ndarray, ...]:
# return np.unique(a, axis=0, **kwargs)
# The numpy alternative `np.unique(a, axis=0)` is slow; cf.
# <https://github.com/numpy/numpy/issues/11136>.
a = np.asarray(a)
Expand Down
26 changes: 17 additions & 9 deletions tests/speedtest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# /// script
# dependencies = [
# "npx",
# "numpy",
# "perfplot",
# ]
# ///
import numpy as np
import perfplot

Expand All @@ -9,33 +16,34 @@


def setup(n):
idx = rng.randomint(0, m, n)
idx = rng.integers(0, m, size=n)
b = rng.random(n)
return idx, b


def np_add_at(data):
def np_add_at(idx, b):
a = np.zeros(m)
idx, b = data
np.add.at(a, idx, b)
return a


def npx_add_at(data):
def npx_add_at(idx, b):
a = np.zeros(m)
idx, b = data
npx.add_at(a, idx, b)
return a


def npx_sum_at(data):
idx, b = data
def npx_sum_at(idx, b):
return npx.sum_at(b, idx, minlength=m)


def bincount(idx, b):
return np.bincount(idx, weights=b, minlength=m)


b = perfplot.bench(
setup=setup,
kernels=[np_add_at, npx_add_at, npx_sum_at],
kernels=[np_add_at, npx_add_at, npx_sum_at, bincount],
n_range=[2**k for k in range(23)],
)
b.save("perf-add-at.svg")
b.save("perf-add-at.png", transparent=False)
Loading