Skip to content

Commit 484ca6e

Browse files
committed
DEV/MAINT: set up doctests and clean examples
1 parent 647ce53 commit 484ca6e

10 files changed

Lines changed: 313 additions & 331 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ jobs:
3838
- name: Lint (if this step fails, please 'pixi run lint' locally and push the changes)
3939
run: pixi run -e lint lint
4040

41+
doctests:
42+
name: Doctests
43+
runs-on: ubuntu-slim
44+
steps:
45+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
46+
with:
47+
persist-credentials: false
48+
49+
- uses: prefix-dev/setup-pixi@a09b6247153796b190642a2b53fac4241043cf6f # v0.10.0
50+
with:
51+
pixi-version: v0.76.2
52+
cache: true
53+
environments: tests
54+
55+
- name: Test public API examples
56+
run: pixi run -e tests doctests
57+
4158
checks:
4259
name: ${{ matrix.environment }} (${{ matrix.platform }})
4360
runs-on: >-

conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Configure public API doctests."""
2+
3+
import warnings
4+
from collections.abc import Iterator
5+
from contextlib import contextmanager
6+
7+
from scipy_doctest.conftest import dt_config
8+
9+
10+
@contextmanager
11+
def _doctest_context( # numpydoc ignore=PR01
12+
_test: object | None = None,
13+
) -> Iterator[None]:
14+
"""
15+
Suppress expected warnings in public API doctests.
16+
"""
17+
with warnings.catch_warnings():
18+
warnings.filterwarnings(
19+
"ignore",
20+
message=r"`xpx\.(broadcast_shapes|expand_dims)` is deprecated.*",
21+
category=DeprecationWarning,
22+
)
23+
yield
24+
25+
26+
dt_config.rtol = 1e-7
27+
dt_config.strict_check = True
28+
dt_config.user_context_mgr = _doctest_context

docs/sphinx/contributing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ All development tasks are then available via `pixi run`:
1616
```bash
1717
pixi run tests # run the tests
1818
pixi run open-docs # build and preview the docs
19+
pixi run doctests # run the doctests in the docs
1920
pixi run lint # run the full lint suite
2021
pixi run ipython # spawn an ipython prompt with array-api-extra installed
2122
pixi run hooks # install pre-commit hooks

pixi.lock

Lines changed: 207 additions & 276 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pixi.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,19 @@ hypothesis = ">=6.155.7"
220220
array-api-strict = ">=2.6.1"
221221
numpy = ">=1.22.0"
222222
scipy = ">=1.15.2"
223+
scipy-doctest = ">=2,<3"
223224

224225
[feature.tests.tasks]
225226
tests = {
226227
description = "Run tests",
227228
cmd = "pytest -v tests/main",
228229
default-environment = "tests",
229230
}
231+
doctests = {
232+
description = "Run public API doctests",
233+
cmd = "pytest --pyargs array_api_extra --doctest-modules --doctest-collect=api --doctest-only-doctests=true",
234+
default-environment = "tests",
235+
}
230236
tests-cov = {
231237
description = "Run tests with coverage",
232238
cmd = "pytest -v -ra --cov --cov-report=xml --cov-report=term --durations=20 tests/main",

src/array_api_extra/_agnostic/_elementwise.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ def apply_where( # numpydoc ignore=PR01,PR02
108108
--------
109109
>>> import array_api_strict as xp
110110
>>> import array_api_extra as xpx
111-
>>> a = xp.asarray([5, 4, 3])
112-
>>> b = xp.asarray([0, 2, 2])
111+
>>> a = xp.asarray([5.0, 4.0, 3.0])
112+
>>> b = xp.asarray([0.0, 2.0, 2.0])
113113
>>> def f(a, b):
114114
... return a // b
115115
>>> xpx.apply_where(b != 0, (a, b), f, fill_value=xp.nan)
116-
array([ nan, 2., 1.])
116+
Array([nan, 2., 1.], dtype=array_api_strict.float64)
117117
"""
118118
# Parse and normalize arguments
119119
if (f2 is None) == (fill_value is None):

src/array_api_extra/_at.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
6767
6868
You may use two alternate syntaxes::
6969
70-
>>> import array_api_extra as xpx
71-
>>> xpx.at(x, idx).set(value) # or add(value), etc.
72-
>>> xpx.at(x)[idx].set(value)
70+
import array_api_extra as xpx
71+
xpx.at(x, idx).set(value) # or add(value), etc.
72+
xpx.at(x)[idx].set(value)
7373
7474
copy : bool, optional
7575
None (default)
@@ -94,8 +94,8 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
9494
(a) When you omit the ``copy`` parameter, you should never reuse the parameter
9595
array later on; ideally, you should reassign it immediately::
9696
97-
>>> import array_api_extra as xpx
98-
>>> x = xpx.at(x, 0).set(2)
97+
import array_api_extra as xpx
98+
x = xpx.at(x, 0).set(2)
9999
100100
The above best practice pattern ensures that the behaviour won't change depending
101101
on whether ``x`` is writeable or not, as the original ``x`` object is dereferenced
@@ -105,9 +105,9 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
105105
On the reverse, the anti-pattern below must be avoided, as it will result in
106106
different behaviour on read-only versus writeable arrays::
107107
108-
>>> x = xp.asarray([0, 0, 0])
109-
>>> y = xpx.at(x, 0).set(2)
110-
>>> z = xpx.at(x, 1).set(3)
108+
x = xp.asarray([0, 0, 0])
109+
y = xpx.at(x, 0).set(2)
110+
z = xpx.at(x, 1).set(3)
111111
112112
In the above example, both calls to ``xpx.at`` update ``x`` in place *if possible*.
113113
This causes the behaviour to diverge depending on whether ``x`` is writeable or not:
@@ -120,22 +120,22 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
120120
The correct pattern to use if you want diverging outputs from the same input is
121121
to enforce copies::
122122
123-
>>> x = xp.asarray([0, 0, 0])
124-
>>> y = xpx.at(x, 0).set(2, copy=True) # Never updates x
125-
>>> z = xpx.at(x, 1).set(3) # May or may not update x in place
126-
>>> del x # avoid accidental reuse of x as we don't know its state anymore
123+
x = xp.asarray([0, 0, 0])
124+
y = xpx.at(x, 0).set(2, copy=True) # Never updates x
125+
z = xpx.at(x, 1).set(3) # May or may not update x in place
126+
del x # avoid accidental reuse of x as we don't know its state anymore
127127
128128
(b) The array API standard does not support integer array indices.
129129
The behaviour of update methods when the index is an array of integers is
130130
undefined and will vary between backends; this is particularly true when the
131131
index contains multiple occurrences of the same index, e.g.::
132132
133-
>>> import numpy as np
134-
>>> import jax.numpy as jnp
135-
>>> import array_api_extra as xpx
136-
>>> xpx.at(np.asarray([123]), np.asarray([0, 0])).add(1)
133+
import numpy as np
134+
import jax.numpy as jnp
135+
import array_api_extra as xpx
136+
xpx.at(np.asarray([123]), np.asarray([0, 0])).add(1)
137137
array([124])
138-
>>> xpx.at(jnp.asarray([123]), jnp.asarray([0, 0])).add(1)
138+
xpx.at(jnp.asarray([123]), jnp.asarray([0, 0])).add(1)
139139
Array([125], dtype=int32)
140140
141141
See Also
@@ -155,38 +155,38 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
155155
156156
This pattern::
157157
158-
>>> mask = m(x)
159-
>>> x[mask] = f(x[mask])
158+
mask = m(x)
159+
x[mask] = f(x[mask])
160160
161161
Can't be replaced by `at`, as it won't work on Dask and JAX inside jax.jit::
162162
163-
>>> mask = m(x)
164-
>>> x = xpx.at(x, mask).set(f(x[mask]) # Crash on Dask and jax.jit
163+
mask = m(x)
164+
x = xpx.at(x, mask).set(f(x[mask])) # Crash on Dask and jax.jit
165165
166166
You should instead use::
167167
168-
>>> x = xp.where(m(x), f(x), x)
168+
x = xp.where(m(x), f(x), x)
169169
170170
Examples
171171
--------
172172
Given either of these equivalent expressions::
173173
174-
>>> import array_api_extra as xpx
175-
>>> x = xpx.at(x)[1].add(2)
176-
>>> x = xpx.at(x, 1).add(2)
174+
import array_api_extra as xpx
175+
x = xpx.at(x)[1].add(2)
176+
x = xpx.at(x, 1).add(2)
177177
178178
If x is a JAX array, they are the same as::
179179
180-
>>> x = x.at[1].add(2)
180+
x = x.at[1].add(2)
181181
182182
If x is a read-only NumPy array, they are the same as::
183183
184-
>>> x = x.copy()
185-
>>> x[1] += 2
184+
x = x.copy()
185+
x[1] += 2
186186
187187
For other known backends, they are the same as::
188188
189-
>>> x[1] += 2
189+
x[1] += 2
190190
"""
191191

192192
_x: Array

src/array_api_extra/_elementwise.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,23 +236,23 @@ def nan_to_num(
236236
--------
237237
>>> import array_api_extra as xpx
238238
>>> import array_api_strict as xp
239-
>>> xpx.nan_to_num(xp.inf)
240-
1.7976931348623157e+308
241-
>>> xpx.nan_to_num(-xp.inf)
242-
-1.7976931348623157e+308
243-
>>> xpx.nan_to_num(xp.nan)
244-
0.0
239+
>>> xpx.nan_to_num(xp.inf, xp=xp)
240+
Array(1.79769313e+308, dtype=array_api_strict.float64)
241+
>>> xpx.nan_to_num(-xp.inf, xp=xp)
242+
Array(-1.79769313e+308, dtype=array_api_strict.float64)
243+
>>> xpx.nan_to_num(xp.nan, xp=xp)
244+
Array(0., dtype=array_api_strict.float64)
245245
>>> x = xp.asarray([xp.inf, -xp.inf, xp.nan, -128, 128])
246246
>>> xpx.nan_to_num(x)
247-
array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, # may vary
248-
-1.28000000e+002, 1.28000000e+002])
247+
Array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000,
248+
-1.28000000e+002, 1.28000000e+002],
249+
dtype=array_api_strict.float64)
249250
>>> y = xp.asarray([complex(xp.inf, xp.nan), xp.nan, complex(xp.nan, xp.inf)])
250-
array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, # may vary
251-
-1.28000000e+002, 1.28000000e+002])
252251
>>> xpx.nan_to_num(y)
253-
array([ 1.79769313e+308 +0.00000000e+000j, # may vary
254-
0.00000000e+000 +0.00000000e+000j,
255-
0.00000000e+000 +1.79769313e+308j])
252+
Array([1.79769313e+308+0.00000000e+000j,
253+
0.00000000e+000+0.00000000e+000j,
254+
0.00000000e+000+1.79769313e+308j],
255+
dtype=array_api_strict.complex128)
256256
"""
257257
if isinstance(fill_value, complex):
258258
msg = "Complex fill values are not supported."

src/array_api_extra/_indexing.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,10 @@ def unravel_index(
233233
>>> import array_api_extra as xpx
234234
>>> import array_api_strict as xp
235235
>>> xs, ys = xpx.unravel_index(xp.asarray([1, 2, 4, 5, 6, 8]), (4, 3))
236-
>>> xs, ys
237-
(
238-
Array([0, 0, 1, 1, 2, 2], dtype=array_api_strict.int64),
239-
Array([1, 2, 1, 2, 0, 2], dtype=array_api_strict.int64),
240-
)
236+
>>> xs
237+
Array([0, 0, 1, 1, 2, 2], dtype=array_api_strict.int64)
238+
>>> ys
239+
Array([1, 2, 1, 2, 0, 2], dtype=array_api_strict.int64)
241240
>>> [(int(x), int(y)) for x, y in zip(xs, ys)]
242241
[(0, 1), (0, 2), (1, 1), (1, 2), (2, 0), (2, 2)]
243242
>>> xs, ys = xpx.unravel_index(xp.arange(6), (2, 2))

src/array_api_extra/testing/_testing.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ def lazy_xp_function(
139139
140140
In other words, the pattern that is being tested is::
141141
142-
>>> @jax.jit
143-
... def user_func(x):
144-
... y = user_prepares_inputs(x)
145-
... z = func(y, some_static_arg=True)
146-
... return user_consumes(z)
142+
@jax.jit
143+
def user_func(x):
144+
y = user_prepares_inputs(x)
145+
z = func(y, some_static_arg=True)
146+
return user_consumes(z)
147147
148148
Default: True.
149149
static_argnums : Deprecated

0 commit comments

Comments
 (0)