Skip to content

Commit fa329c8

Browse files
authored
DOC: auto-generate and deploy API documentation via sphinx and github pages (#186)
reviewed at #186
1 parent da72892 commit fa329c8

File tree

11 files changed

+251
-16
lines changed

11 files changed

+251
-16
lines changed

.github/workflows/docs.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Build & Deploy Docs
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
# Build the docs
12+
docs-build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python 3.12
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
cache: 'pip'
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
python -m pip install -e '.[doc]'
25+
26+
- name: Build Docs
27+
run: |
28+
sphinx-build -b html docs/ docs/build/
29+
30+
- name: Upload Artifact
31+
uses: actions/upload-artifact@v4
32+
with:
33+
name: docs-build
34+
path: docs/build/
35+
36+
# Deploy the docs to GitHub Pages
37+
docs-deploy:
38+
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
39+
runs-on: ubuntu-latest
40+
needs: docs-build
41+
42+
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
43+
permissions:
44+
contents: write
45+
pages: write
46+
47+
steps:
48+
- uses: actions/checkout@v4
49+
- name: Download Artifact
50+
uses: actions/download-artifact@v4
51+
with:
52+
name: docs-build
53+
path: docs/build/
54+
55+
- name: Deploy to GitHub pages
56+
uses: JamesIves/github-pages-deploy-action@v4
57+
with:
58+
folder: docs/build/

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ bin/
55
venv/
66
.pytest_cache
77
dist/
8+
build/
9+
.vscode
10+
**/.DS_Store
11+
12+
# Sphinx documentation
13+
docs/_build/
14+
docs/generated/

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ use the command flag
149149
$ pytest --pyargs <your-package> --doctest-modules --doctest-collect=api
150150
```
151151

152-
See [More fine-grained control](https://github.com/scipy/scipy_doctest#More-fine-grained-control) section
152+
See [More fine-grained control](#more-fine-grained-control) section
153153
for details on how to customize the behavior.
154154

155155

@@ -190,7 +190,7 @@ $ python -m scipy_doctest bar.rst
190190

191191
Notice that the command-line usage only uses the default `DTConfig` settings.
192192

193-
193+
(more-fine-grained-control)=
194194
## More fine-grained control
195195

196196
More fine-grained control of the functionality is available via the following
@@ -416,4 +416,4 @@ adding `--assert=plain` is reasonable.
416416
This package is work in progress. Contributions are most welcome!
417417
Please don't hesitate to open an issue in the tracker or send a pull request.
418418

419-
The current location of the issue tracker is https://github.com/scipy/scipy_doctest.
419+
The current location of the issue tracker is <https://github.com/scipy/scipy_doctest>.

docs/conf.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Configuration file for the Sphinx documentation builder.
2+
from typing import Any
3+
4+
project = 'scipy_doctest'
5+
copyright = '2025, Scipy Contributors'
6+
author = 'Scipy Contributors'
7+
8+
extensions = [
9+
"myst_parser",
10+
"sphinx.ext.autodoc",
11+
"sphinx.ext.autosummary",
12+
"sphinx.ext.napoleon",
13+
"sphinx_copybutton",
14+
]
15+
16+
source_suffix = [".rst", ".md"]
17+
exclude_patterns = [
18+
"_build",
19+
".DS_Store"
20+
]
21+
22+
default_role = "literal"
23+
24+
html_theme = "furo"
25+
26+
html_theme_options: dict[str, Any] = {
27+
"footer_icons": [
28+
{
29+
"name": "GitHub",
30+
"url": "https://github.com/scipy/scipy_doctest",
31+
"html": """
32+
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
33+
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
34+
</svg>
35+
""", # noqa: E501
36+
"class": "",
37+
},
38+
],
39+
"source_repository": "https://github.com/scipy/scipy_doctest",
40+
"source_branch": "main",
41+
"source_directory": "docs/",
42+
}
43+
44+
myst_enable_extensions = [
45+
"colon_fence",
46+
]
47+
48+
always_document_param_types = True
49+
50+
51+
# -- Options for HTML output -------------------------------------------------
52+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
53+
54+
html_theme = 'furo'
55+
56+
autosummary_generate = True
57+
numpydoc_show_class_members = False

docs/implementation.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Implementation
2+
3+
```{eval-rst}
4+
.. currentmodule:: scipy_doctest.impl
5+
.. autosummary::
6+
:nosignatures:
7+
:toctree: generated
8+
9+
DTConfig
10+
DTChecker
11+
DTRunner
12+
DebugDTRunner
13+
DTFinder
14+
DTParser
15+
```

docs/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
```{include} ../README.md
2+
```
3+
4+
```{toctree}
5+
:maxdepth: 2
6+
:hidden:
7+
Home <self>
8+
implementation.md
9+
plugin.md
10+
```

docs/plugin.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Plugin
2+
3+
```{eval-rst}
4+
.. currentmodule:: scipy_doctest.plugin
5+
.. autosummary::
6+
:nosignatures:
7+
:toctree: generated
8+
9+
DTModule
10+
DTTextfile
11+
```

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ test = [
2828
"scipy <= 1.14.1", # black-list 1.5.1 ?
2929
"matplotlib"
3030
]
31+
doc = [
32+
"furo==2024.8.6",
33+
"myst-parser==4.0.0",
34+
"sphinx==8.1.3",
35+
"sphinx-copybutton==0.5.2"
36+
]
3137

3238
[project.urls]
3339
Home = "https://github.com/scipy/scipy_doctest"

scipy_doctest/frontend.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def find_doctests(module, strategy=None,
4646
Returns
4747
-------
4848
tests : list
49-
A list of `doctest.DocTest`s that are defined by the module docstring,
49+
A list of `doctest.DocTest` s that are defined by the module docstring,
5050
and by its contained objects’ docstrings. The selection is controlled
5151
by the `strategy` argument.
5252
@@ -141,7 +141,7 @@ def testmod(m=None, name=None, globs=None, verbose=None,
141141
In verbose mode, the summary is detailed, else very brief (in fact,
142142
empty if all tests passed)
143143
Default is True.
144-
verbose : int
144+
verbose : int
145145
Control the run verbosity:
146146
0 means only report failures,
147147
1 means emit object names,
@@ -175,7 +175,7 @@ def testmod(m=None, name=None, globs=None, verbose=None,
175175
(result, history)
176176
`result` is a namedtuple ``TestResult(failed, attempted)``
177177
`history` is a dict with details of which objects were examined (the
178-
keys are object names and values are individual objects' ``TestResult``s)
178+
keys are object names and values are individual objects' ``TestResult`` s)
179179
180180
Examples
181181
--------
@@ -204,7 +204,7 @@ def testmod(m=None, name=None, globs=None, verbose=None,
204204
For complex packages, prefer `strategy='api'`, which works as follows:
205205
- take the names of public objects from the `__all__` attribute of the package.
206206
- if `__all__` is not defined, take `dir(module)` and filter out names
207-
which start with a leading underscore and dunders.
207+
which start with a leading underscore and dunders.
208208
- filter out deprecated items, i.e. those which raise `DeprecationWarning`.
209209
210210
"""
@@ -306,7 +306,7 @@ def testfile(filename, module_relative=True, name=None, package=None,
306306
In verbose mode, the summary is detailed, else very brief (in fact,
307307
empty if all tests passed)
308308
Default is True.
309-
verbose : int
309+
verbose : int
310310
Control the run verbosity:
311311
0 means only report failures,
312312
1 means emit object names,
@@ -335,7 +335,7 @@ def testfile(filename, module_relative=True, name=None, package=None,
335335
(result, history)
336336
`result` is a namedtuple ``TestResult(failed, attempted)``
337337
`history` is a dict with details of which objects were examined (the
338-
keys are object names and values are individual objects' ``TestResult``s)
338+
keys are object names and values are individual objects' ``TestResult`` s)
339339
"""
340340
# initial configuration
341341
if config is None:

scipy_doctest/impl.py

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class DTConfig:
6060
>>> for test in tests:
6161
... with user_context(test):
6262
... runner.run(test)
63+
6364
Default is a noop.
6465
local_resources: dict
6566
If a test needs some local files, list them here. The format is
@@ -217,8 +218,13 @@ def __init__(self, *, # DTChecker configuration
217218

218219

219220
def try_convert_namedtuple(got):
220-
# suppose that "got" is smth like MoodResult(statistic=10, pvalue=0.1).
221-
# Then convert it to the tuple (10, 0.1), so that can later compare tuples.
221+
"""
222+
Converts a string representation of a named tuple into a plain tuple.
223+
224+
Suppose that "got" is smth like MoodResult(statistic=10, pvalue=0.1).
225+
Then convert it to the tuple (10, 0.1), so that can later compare tuples.
226+
"""
227+
222228
num = got.count('=')
223229
if num == 0:
224230
# not a nameduple, bail out
@@ -257,6 +263,8 @@ def try_convert_printed_array(got):
257263

258264

259265
def has_masked(got):
266+
"""Check if a given string represents a NumPy masked array.
267+
"""
260268
return 'masked_array' in got and '--' in got
261269

262270

@@ -284,6 +292,20 @@ def try_split_shape_from_abbrv(s_got):
284292

285293

286294
class DTChecker(doctest.OutputChecker):
295+
"""
296+
A drop-in replacement for `doctest.OutputChecker`.
297+
298+
Allows robust output comparison for numerical values and special
299+
cases involving NumPy arrays, masked arrays, namedtuples, and object
300+
memory addresses. It is configurable via a `DTConfig` object.
301+
302+
Parameters:
303+
-----------
304+
config : DTConfig, optional
305+
Configuration object that controls various aspects of output checking.
306+
If not provided, a default `DTConfig` instance is used.
307+
308+
"""
287309
obj_pattern = re.compile(r'at 0x[0-9a-fA-F]+>')
288310
vanilla = doctest.OutputChecker()
289311

@@ -426,6 +448,25 @@ def _do_check(self, want, got, strict_check):
426448

427449

428450
class DTRunner(doctest.DocTestRunner):
451+
"""
452+
A drop-in replacement for `doctest.DocTestRunner`.
453+
454+
Improves how test names are reported and allows better control over exception handling.
455+
It integrates with `DTConfig` to apply customized settings for doctest execution.
456+
457+
Parameters:
458+
-----------
459+
checker : doctest.OutputChecker, optional
460+
A custom output checker, defaults to `DTConfig.CheckerKlass(config)`.
461+
verbose : bool, optional
462+
If `True`, enables verbose output.
463+
optionflags : int, optional
464+
Bitwise OR of `doctest` option flags; defaults to `DTConfig.optionflags`.
465+
config : DTConfig, optional
466+
A configuration object controlling doctest behavior; a default instance is used if not provided.
467+
468+
"""
469+
429470
DIVIDER = "\n"
430471

431472
def __init__(self, checker=None, verbose=None, optionflags=None, config=None):
@@ -481,7 +522,8 @@ def get_history(self):
481522

482523

483524
class DebugDTRunner(DTRunner):
484-
"""Doctest runner which raises on a first error.
525+
"""
526+
Doctest runner which raises an exception on the first error.
485527
486528
Almost verbatim copy of `doctest.DebugRunner`.
487529
"""
@@ -505,8 +547,24 @@ def report_failure(self, out, test, example, got):
505547

506548

507549
class DTFinder(doctest.DocTestFinder):
508-
"""A Finder with helpful defaults.
509550
"""
551+
A drop-in replacement for `doctest.DocTestFinder` with helpful defaults.
552+
553+
Parameters:
554+
-----------
555+
verbose : bool, optional
556+
If `True`, enables verbose output during doctest discovery.
557+
parser : doctest.DocTestParser, optional
558+
A custom parser for extracting doctests; defaults to `DTParser(config)`.
559+
recurse : bool, default=True
560+
Whether to recursively search for doctests in nested objects.
561+
exclude_empty : bool, default=True
562+
Whether to exclude objects that have no doctests.
563+
config : DTConfig, optional
564+
A configuration object controlling doctest behavior; a default instance is used if not provided.
565+
566+
"""
567+
510568
def __init__(self, verbose=None, parser=None, recurse=True,
511569
exclude_empty=True, config=None):
512570
if config is None:
@@ -534,8 +592,21 @@ def find(self, obj, name=None, module=None, globs=None, extraglobs=None):
534592

535593

536594
class DTParser(doctest.DocTestParser):
537-
"""A Parser with a stopword list.
538595
"""
596+
A drop-in replacement for `doctest.DocTestParser` with a stopword list.
597+
598+
It filters out stopwords, pseudocode, and random markers from doctests.
599+
600+
Parameters:
601+
-----------
602+
config : DTConfig, optional
603+
A configuration object containing:
604+
- `stopwords`: A list of words that signal a doctest should be ignored.
605+
- `pseudocode`: A list of markers indicating non-executable code.
606+
- `rndm_markers`: A list of markers indicating results may vary.
607+
608+
"""
609+
539610
def __init__(self, config=None):
540611
if config is None:
541612
config = DTConfig()

0 commit comments

Comments
 (0)