Skip to content

Commit 5ed3082

Browse files
committed
DEV/MAINT: set up doctests and clean examples
1 parent 60c1ba7 commit 5ed3082

9 files changed

Lines changed: 311 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.71.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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Configure public API doctests."""
2+
3+
import warnings
4+
from contextlib import contextmanager
5+
6+
from scipy_doctest.conftest import dt_config
7+
8+
9+
@contextmanager
10+
def _doctest_context(_test=None):
11+
with warnings.catch_warnings():
12+
warnings.filterwarnings(
13+
"ignore",
14+
message=r"`xpx\.(broadcast_shapes|expand_dims)` is deprecated.*",
15+
category=DeprecationWarning,
16+
)
17+
yield
18+
19+
20+
dt_config.rtol = 1e-7
21+
dt_config.strict_check = True
22+
dt_config.user_context_mgr = _doctest_context

docs/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",
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",

src/array_api_extra/_delegation.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -907,23 +907,23 @@ def nan_to_num(
907907
--------
908908
>>> import array_api_extra as xpx
909909
>>> import array_api_strict as xp
910-
>>> xpx.nan_to_num(xp.inf)
911-
1.7976931348623157e+308
912-
>>> xpx.nan_to_num(-xp.inf)
913-
-1.7976931348623157e+308
914-
>>> xpx.nan_to_num(xp.nan)
915-
0.0
910+
>>> xpx.nan_to_num(xp.inf, xp=xp)
911+
Array(1.79769313e+308, dtype=array_api_strict.float64)
912+
>>> xpx.nan_to_num(-xp.inf, xp=xp)
913+
Array(-1.79769313e+308, dtype=array_api_strict.float64)
914+
>>> xpx.nan_to_num(xp.nan, xp=xp)
915+
Array(0., dtype=array_api_strict.float64)
916916
>>> x = xp.asarray([xp.inf, -xp.inf, xp.nan, -128, 128])
917917
>>> xpx.nan_to_num(x)
918-
array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, # may vary
919-
-1.28000000e+002, 1.28000000e+002])
918+
Array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000,
919+
-1.28000000e+002, 1.28000000e+002],
920+
dtype=array_api_strict.float64)
920921
>>> y = xp.asarray([complex(xp.inf, xp.nan), xp.nan, complex(xp.nan, xp.inf)])
921-
array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, # may vary
922-
-1.28000000e+002, 1.28000000e+002])
923922
>>> xpx.nan_to_num(y)
924-
array([ 1.79769313e+308 +0.00000000e+000j, # may vary
925-
0.00000000e+000 +0.00000000e+000j,
926-
0.00000000e+000 +1.79769313e+308j])
923+
Array([1.79769313e+308+0.00000000e+000j,
924+
0.00000000e+000+0.00000000e+000j,
925+
0.00000000e+000+1.79769313e+308j],
926+
dtype=array_api_strict.complex128)
927927
"""
928928
if isinstance(fill_value, complex):
929929
msg = "Complex fill values are not supported."
@@ -1818,11 +1818,10 @@ def unravel_index(
18181818
>>> import array_api_extra as xpx
18191819
>>> import array_api_strict as xp
18201820
>>> xs, ys = xpx.unravel_index(xp.asarray([1, 2, 4, 5, 6, 8]), (4, 3))
1821-
>>> xs, ys
1822-
(
1823-
Array([0, 0, 1, 1, 2, 2], dtype=array_api_strict.int64),
1824-
Array([1, 2, 1, 2, 0, 2], dtype=array_api_strict.int64),
1825-
)
1821+
>>> xs
1822+
Array([0, 0, 1, 1, 2, 2], dtype=array_api_strict.int64)
1823+
>>> ys
1824+
Array([1, 2, 1, 2, 0, 2], dtype=array_api_strict.int64)
18261825
>>> [(int(x), int(y)) for x, y in zip(xs, ys)]
18271826
[(0, 1), (0, 2), (1, 1), (1, 2), (2, 0), (2, 2)]
18281827
>>> xs, ys = xpx.unravel_index(xp.arange(6), (2, 2))

src/array_api_extra/_lib/_at.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
7676
7777
You may use two alternate syntaxes::
7878
79-
>>> import array_api_extra as xpx
80-
>>> xpx.at(x, idx).set(value) # or add(value), etc.
81-
>>> xpx.at(x)[idx].set(value)
79+
import array_api_extra as xpx
80+
81+
xpx.at(x, idx).set(value) # or add(value), etc.
82+
xpx.at(x)[idx].set(value)
8283
8384
copy : bool, optional
8485
None (default)
@@ -103,8 +104,9 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
103104
(a) When you omit the ``copy`` parameter, you should never reuse the parameter
104105
array later on; ideally, you should reassign it immediately::
105106
106-
>>> import array_api_extra as xpx
107-
>>> x = xpx.at(x, 0).set(2)
107+
import array_api_extra as xpx
108+
109+
x = xpx.at(x, 0).set(2)
108110
109111
The above best practice pattern ensures that the behaviour won't change depending
110112
on whether ``x`` is writeable or not, as the original ``x`` object is dereferenced
@@ -114,9 +116,9 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
114116
On the reverse, the anti-pattern below must be avoided, as it will result in
115117
different behaviour on read-only versus writeable arrays::
116118
117-
>>> x = xp.asarray([0, 0, 0])
118-
>>> y = xpx.at(x, 0).set(2)
119-
>>> z = xpx.at(x, 1).set(3)
119+
x = xp.asarray([0, 0, 0])
120+
y = xpx.at(x, 0).set(2)
121+
z = xpx.at(x, 1).set(3)
120122
121123
In the above example, both calls to ``xpx.at`` update ``x`` in place *if possible*.
122124
This causes the behaviour to diverge depending on whether ``x`` is writeable or not:
@@ -129,22 +131,23 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
129131
The correct pattern to use if you want diverging outputs from the same input is
130132
to enforce copies::
131133
132-
>>> x = xp.asarray([0, 0, 0])
133-
>>> y = xpx.at(x, 0).set(2, copy=True) # Never updates x
134-
>>> z = xpx.at(x, 1).set(3) # May or may not update x in place
135-
>>> del x # avoid accidental reuse of x as we don't know its state anymore
134+
x = xp.asarray([0, 0, 0])
135+
y = xpx.at(x, 0).set(2, copy=True) # Never updates x
136+
z = xpx.at(x, 1).set(3) # May or may not update x in place
137+
del x # avoid accidental reuse of x as we don't know its state anymore
136138
137139
(b) The array API standard does not support integer array indices.
138140
The behaviour of update methods when the index is an array of integers is
139141
undefined and will vary between backends; this is particularly true when the
140142
index contains multiple occurrences of the same index, e.g.::
141143
142-
>>> import numpy as np
143-
>>> import jax.numpy as jnp
144-
>>> import array_api_extra as xpx
145-
>>> xpx.at(np.asarray([123]), np.asarray([0, 0])).add(1)
144+
import numpy as np
145+
import jax.numpy as jnp
146+
import array_api_extra as xpx
147+
148+
xpx.at(np.asarray([123]), np.asarray([0, 0])).add(1)
146149
array([124])
147-
>>> xpx.at(jnp.asarray([123]), jnp.asarray([0, 0])).add(1)
150+
xpx.at(jnp.asarray([123]), jnp.asarray([0, 0])).add(1)
148151
Array([125], dtype=int32)
149152
150153
See Also
@@ -164,38 +167,39 @@ class at: # pylint: disable=invalid-name # numpydoc ignore=PR02
164167
165168
This pattern::
166169
167-
>>> mask = m(x)
168-
>>> x[mask] = f(x[mask])
170+
mask = m(x)
171+
x[mask] = f(x[mask])
169172
170173
Can't be replaced by `at`, as it won't work on Dask and JAX inside jax.jit::
171174
172-
>>> mask = m(x)
173-
>>> x = xpx.at(x, mask).set(f(x[mask]) # Crash on Dask and jax.jit
175+
mask = m(x)
176+
x = xpx.at(x, mask).set(f(x[mask])) # Crash on Dask and jax.jit
174177
175178
You should instead use::
176179
177-
>>> x = xp.where(m(x), f(x), x)
180+
x = xp.where(m(x), f(x), x)
178181
179182
Examples
180183
--------
181184
Given either of these equivalent expressions::
182185
183-
>>> import array_api_extra as xpx
184-
>>> x = xpx.at(x)[1].add(2)
185-
>>> x = xpx.at(x, 1).add(2)
186+
import array_api_extra as xpx
187+
188+
x = xpx.at(x)[1].add(2)
189+
x = xpx.at(x, 1).add(2)
186190
187191
If x is a JAX array, they are the same as::
188192
189-
>>> x = x.at[1].add(2)
193+
x = x.at[1].add(2)
190194
191195
If x is a read-only NumPy array, they are the same as::
192196
193-
>>> x = x.copy()
194-
>>> x[1] += 2
197+
x = x.copy()
198+
x[1] += 2
195199
196200
For other known backends, they are the same as::
197201
198-
>>> x[1] += 2
202+
x[1] += 2
199203
"""
200204

201205
_x: Array

src/array_api_extra/_lib/_funcs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,12 @@ def apply_where( # numpydoc ignore=PR01,PR02
144144
--------
145145
>>> import array_api_strict as xp
146146
>>> import array_api_extra as xpx
147-
>>> a = xp.asarray([5, 4, 3])
148-
>>> b = xp.asarray([0, 2, 2])
147+
>>> a = xp.asarray([5.0, 4.0, 3.0])
148+
>>> b = xp.asarray([0.0, 2.0, 2.0])
149149
>>> def f(a, b):
150150
... return a // b
151151
>>> xpx.apply_where(b != 0, (a, b), f, fill_value=xp.nan)
152-
array([ nan, 2., 1.])
152+
Array([nan, 2., 1.], dtype=array_api_strict.float64)
153153
"""
154154
# Parse and normalize arguments
155155
if (f2 is None) == (fill_value is None):

src/array_api_extra/testing.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,11 @@ def lazy_xp_function(
149149
150150
In other words, the pattern that is being tested is::
151151
152-
>>> @jax.jit
153-
... def user_func(x):
154-
... y = user_prepares_inputs(x)
155-
... z = func(y, some_static_arg=True)
156-
... return user_consumes(z)
152+
@jax.jit
153+
def user_func(x):
154+
y = user_prepares_inputs(x)
155+
z = func(y, some_static_arg=True)
156+
return user_consumes(z)
157157
158158
Default: True.
159159
static_argnums : Deprecated

0 commit comments

Comments
 (0)