Skip to content

Commit 83ca762

Browse files
authored
Merge pull request #18 from CyrilJl/improve/professional-quality
Raise package quality and reliability standards
2 parents fd965c6 + 55ccfa6 commit 83ca762

25 files changed

Lines changed: 373 additions & 127 deletions

.github/workflows/publish-to-pypi.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ name: Publish to PyPI
33
on:
44
push:
55
tags:
6-
- '*'
6+
- "v*"
77
workflow_dispatch:
88

9+
permissions:
10+
contents: read
11+
912
jobs:
1013
publish:
1114
runs-on: ubuntu-latest
@@ -17,7 +20,8 @@ jobs:
1720
- name: Set up Python
1821
uses: actions/setup-python@v6
1922
with:
20-
python-version: 3.x
23+
python-version: "3.14"
24+
cache: pip
2125

2226
- name: Install dependencies
2327
run: |
@@ -28,9 +32,12 @@ jobs:
2832
run: |
2933
python -m build
3034
35+
- name: Validate distribution metadata
36+
run: twine check dist/*
37+
3138
- name: Publish to PyPI
3239
env:
3340
TWINE_USERNAME: __token__
3441
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
3542
run: |
36-
twine upload dist/*
43+
twine upload dist/*

.github/workflows/pytest.yml

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
1-
name: Pytest
1+
name: Tests
22

33
on:
4+
push:
5+
branches: [main]
46
pull_request:
5-
branches: [ "main" ]
7+
branches: [main]
68
workflow_dispatch:
79

10+
permissions:
11+
contents: read
12+
813
jobs:
914
test:
15+
name: Python ${{ matrix.python-version }}
1016
runs-on: ubuntu-latest
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1121

1222
steps:
13-
- name: Checkout repository
14-
uses: actions/checkout@v5
15-
16-
- name: Set up Python
17-
uses: actions/setup-python@v6
18-
with:
19-
python-version: '3.12'
23+
- name: Check out repository
24+
uses: actions/checkout@v5
2025

21-
- name: Install dependencies
22-
run: |
23-
python -m pip install --upgrade pip
24-
pip install pytest numpy build
26+
- name: Set up Python
27+
uses: actions/setup-python@v6
28+
with:
29+
python-version: ${{ matrix.python-version }}
30+
cache: pip
2531

26-
- name: Install batchstats
27-
run: |
28-
python -m build
29-
pip install .
32+
- name: Install package and test dependencies
33+
run: python -m pip install ".[test]"
3034

31-
- name: Run Pytest
32-
run: |
33-
pytest tests
35+
- name: Run tests with coverage
36+
run: python -m pytest --cov=batchstats
Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,72 @@
1-
name: Format with Ruff
1+
name: Quality
22

33
on:
4+
push:
5+
branches: [main]
46
pull_request:
5-
branches: [ main ]
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
612

713
jobs:
8-
format:
14+
lint:
915
runs-on: ubuntu-latest
1016
steps:
11-
- uses: actions/checkout@v5
17+
- name: Check out repository
18+
uses: actions/checkout@v5
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v6
1222
with:
13-
ref: ${{ github.head_ref }}
23+
python-version: "3.14"
24+
cache: pip
1425

1526
- name: Install Ruff
16-
run: pip install ruff
17-
18-
- name: Run ruff format
19-
run: ruff format .
20-
21-
- name: Commit changes
22-
if: success()
23-
run: |
24-
git diff --quiet || (git add . && \
25-
git -c user.name="GitHub Actions" \
26-
-c user.email="actions@github.com" \
27-
commit -m "Format code with Ruff" && \
28-
git push)
27+
run: python -m pip install "ruff>=0.11,<1"
28+
29+
- name: Lint
30+
run: python -m ruff check .
31+
32+
- name: Check formatting
33+
run: python -m ruff format --check .
34+
35+
package:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Check out repository
39+
uses: actions/checkout@v5
40+
41+
- name: Set up Python
42+
uses: actions/setup-python@v6
43+
with:
44+
python-version: "3.14"
45+
cache: pip
46+
47+
- name: Install build tools
48+
run: python -m pip install "build>=1.2,<2" "twine>=6,<8"
49+
50+
- name: Build distributions
51+
run: python -m build
52+
53+
- name: Validate distribution metadata
54+
run: python -m twine check dist/*
55+
56+
docs:
57+
runs-on: ubuntu-latest
58+
steps:
59+
- name: Check out repository
60+
uses: actions/checkout@v5
61+
62+
- name: Set up Python
63+
uses: actions/setup-python@v6
64+
with:
65+
python-version: "3.14"
66+
cache: pip
67+
68+
- name: Install package and documentation dependencies
69+
run: python -m pip install ".[docs]"
70+
71+
- name: Build documentation
72+
run: python -m sphinx -W --keep-going -b html docs/source docs/_build/html

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<img src="https://raw.githubusercontent.com/CyrilJl/BatchStats/main/docs/source/_static/logo_batchstats.svg" alt="Logo BatchStats" width="200">
33

44
[![PyPI Version](https://img.shields.io/pypi/v/batchstats.svg)](https://pypi.org/project/batchstats/)
5+
[![Python Versions](https://img.shields.io/pypi/pyversions/batchstats.svg)](https://pypi.org/project/batchstats/)
56
[![conda Version](https://anaconda.org/conda-forge/batchstats/badges/version.svg)](https://anaconda.org/conda-forge/batchstats)
67
[![Documentation Status](https://img.shields.io/readthedocs/batchstats?logo=read-the-docs)](https://batchstats.readthedocs.io/en/latest/?badge=latest)
78
[![Unit tests](https://github.com/CyrilJl/BatchStats/actions/workflows/pytest.yml/badge.svg)](https://github.com/CyrilJl/BatchStats/actions/workflows/pytest.yml)
@@ -10,7 +11,9 @@
1011

1112
# BatchStats
1213

13-
BatchStats computes statistics on data that arrives in batches, so you can stream or process large datasets without loading everything into memory. Feed batches with `update_batch`, then call the object to get the final result.
14+
BatchStats computes statistics on data that arrives in batches, so you can stream or process large datasets without loading everything into memory. Its incremental algorithms expose a small NumPy-friendly API and support merging independently computed accumulators.
15+
16+
BatchStats requires Python 3.10 or newer.
1417

1518
## Installation
1619

@@ -30,7 +33,8 @@ conda install -c conda-forge batchstats
3033
import numpy as np
3134
from batchstats import BatchMean, BatchVar
3235

33-
data_stream = (np.random.randn(100, 10) for _ in range(10))
36+
rng = np.random.default_rng(0)
37+
data_stream = (rng.standard_normal((100, 10)) for _ in range(10))
3438

3539
batch_mean = BatchMean()
3640
batch_var = BatchVar()
@@ -61,3 +65,16 @@ print(f"Variance shape: {variance.shape}")
6165
* `BatchCorr`
6266

6367
Docs: https://batchstats.readthedocs.io
68+
69+
## Development
70+
71+
Install the development dependencies and run the local quality gates:
72+
73+
```console
74+
python -m pip install -e ".[dev]"
75+
python -m ruff check .
76+
python -m ruff format --check .
77+
python -m pytest --cov=batchstats
78+
python -m build
79+
python -m twine check dist/*
80+
```

batchstats/__init__.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
from importlib.metadata import version
22

3+
from ._misc import (
4+
DifferentAxisError,
5+
DifferentShapesError,
6+
DifferentStatsError,
7+
NoValidSamplesError,
8+
UnequalSamplesNumber,
9+
)
310
from .base import BatchNanStat, BatchStat
411
from .nanstats import BatchNanMax, BatchNanMean, BatchNanMin, BatchNanPeakToPeak, BatchNanSum
512
from .stats import (
@@ -22,19 +29,24 @@
2229
"BatchMax",
2330
"BatchMean",
2431
"BatchMin",
32+
"BatchNanMax",
33+
"BatchNanMean",
34+
"BatchNanMin",
35+
"BatchNanPeakToPeak",
36+
"BatchNanStat",
37+
"BatchNanSum",
2538
"BatchPeakToPeak",
2639
"BatchStat",
2740
"BatchStd",
2841
"BatchSum",
2942
"BatchVar",
3043
"BatchWeightedMean",
3144
"BatchWeightedSum",
32-
"BatchNanMax",
33-
"BatchNanMean",
34-
"BatchNanMin",
35-
"BatchNanPeakToPeak",
36-
"BatchNanStat",
37-
"BatchNanSum",
45+
"DifferentAxisError",
46+
"DifferentShapesError",
47+
"DifferentStatsError",
48+
"NoValidSamplesError",
49+
"UnequalSamplesNumber",
3850
]
3951

4052
__version__ = version("batchstats")

batchstats/_misc.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import warnings
2-
31
import numpy as np
42

5-
# Customize the warning format
6-
warnings.formatwarning = lambda msg, *args, **kwargs: str(msg) + "\n"
7-
83

94
class NoValidSamplesError(ValueError):
105
"""
@@ -48,7 +43,7 @@ def any_nan(x, axis=None):
4843
numpy.ndarray: Boolean array indicating NaN presence.
4944
5045
"""
51-
return np.isnan(np.add.reduce(array=x, axis=axis))
46+
return np.isnan(x).any(axis=axis)
5247

5348

5449
def check_params(param, params=None, types=None):
@@ -70,8 +65,3 @@ def check_params(param, params=None, types=None):
7065

7166
# Return the parameter if it passes the checks
7267
return param
73-
74-
75-
def warning(msg):
76-
# Trigger a warning with the provided message
77-
return warnings.warn(msg)

batchstats/base/batch_nan_stat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __init__(self, axis=0):
4242

4343
def _process_batch(self, batch):
4444
"""
45-
Process the input batch, counting valid (finite) values.
45+
Process the input batch, counting non-NaN values.
4646
4747
Args:
4848
batch (numpy.ndarray): Input batch.
@@ -53,7 +53,7 @@ def _process_batch(self, batch):
5353
5454
"""
5555
batch = np.atleast_2d(np.asarray(batch))
56-
n_valid = np.isfinite(batch).sum(axis=self.axis)
56+
n_valid = np.count_nonzero(~np.isnan(batch), axis=self.axis)
5757
self._add_valid_count(n_valid)
5858
return batch, n_valid
5959

batchstats/base/batch_stat.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,11 @@ def __repr__(self):
113113
return f"{self.__class__.__name__}()"
114114

115115
def merge_test(self, other, field: str):
116-
if type(self) != type(other):
116+
if type(self) is not type(other):
117117
raise DifferentStatsError()
118118
if self.axis != other.axis:
119119
raise DifferentAxisError()
120-
if hasattr(self, field) and hasattr(other, field):
121-
if getattr(self, field).shape != getattr(other, field).shape:
122-
raise DifferentShapesError()
120+
left = getattr(self, field, None)
121+
right = getattr(other, field, None)
122+
if left is not None and right is not None and left.shape != right.shape:
123+
raise DifferentShapesError()

batchstats/nanstats/nan_max.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def __add__(self, other):
134134
else:
135135
ret = BatchNanMax(axis=self.axis)
136136
ret.n_samples = self.n_samples + other.n_samples
137-
ret.max = np.maximum(self.max, other.max)
137+
ret.max = np.fmax(self.max, other.max)
138138
return ret
139139

140140
def merge_test(self, other, field=None):

batchstats/nanstats/nan_mean.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import numpy as np
2+
13
from ..base import BatchNanStat
24
from .nan_sum import BatchNanSum
35

@@ -93,4 +95,5 @@ def __call__(self):
9395
numpy.ndarray: Mean of the batches.
9496
9597
"""
96-
return self.sum() / self.sum.n_samples
98+
with np.errstate(divide="ignore", invalid="ignore"):
99+
return self.sum() / self.sum.n_samples

0 commit comments

Comments
 (0)