Skip to content

Commit

Permalink
Add --doctest-ufunc option to doctest Numpy ufuncs
Browse files Browse the repository at this point in the history
This absorbs the functionality of the pytest-doctest-ufunc package,
which was heavily based on pytest-doctestplus to begin with.

pytest-doctest-ufunc will be retired.

Fixes #123.
  • Loading branch information
lpsinger committed Feb 7, 2022
1 parent ae2cd94 commit 3345ea7
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
0.12.0 (unreleased)
===================

- Add ``--doctest-ufunc`` option to run doctests in docstrings of Numpy ufuncs.
[#123]

0.11.2 (2021-12-09)
===================

Expand Down
12 changes: 8 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ providing the following features:
* handling doctests that use remote data in conjunction with the
`pytest-remotedata`_ plugin (see `Remote Data`_)
* optional inclusion of ``*.rst`` files for doctests (see `Setup and Configuration`_)
* optional inclusion of doctests in docstrings of Numpy ufuncs (see `Setup and Configuration`_)

.. _pytest-remotedata: https://github.com/astropy/pytest-remotedata

Expand Down Expand Up @@ -70,12 +71,15 @@ Usage
Setup and Configuration
~~~~~~~~~~~~~~~~~~~~~~~

This plugin provides two command line options: ``--doctest-plus`` for enabling
the advanced features mentioned above, and ``--doctest-rst`` for including
``*.rst`` files in doctest collection.
This plugin provides three command line options: ``--doctest-plus`` for enabling
the advanced features mentioned above, ``--doctest-rst`` for including
``*.rst`` files in doctest collection, and ``--doctest-ufunc`` for including
docstrings of Numpy ufuncs.

This plugin can also be enabled by default by adding ``doctest_plus = enabled``
to the ``[tool:pytest]`` section of the package's ``setup.cfg`` file.
to the ``[tool:pytest]`` section of the package's ``setup.cfg`` file. Similarly,
the ``doctest_ufunc = enabled`` option is supported to include docstrings of
Numpy ufuncs.

The plugin is applied to all directories and files that ``pytest`` collects.
This means that configuring ``testpaths`` and ``norecursedirs`` in
Expand Down
36 changes: 34 additions & 2 deletions pytest_doctestplus/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ def pytest_addoption(parser):
"This is no longer recommended, use --doctest-glob instead."
))

parser.addoption("--doctest-ufunc", action="store_true",
help=(
"Enable running doctests in docstrings of Numpy ufuncs. "
"Implies usage of doctest-plus."
))

# Defaults to `atol` parameter from `numpy.allclose`.
parser.addoption("--doctest-plus-atol", action="store",
help="set the absolute tolerance for float comparison",
Expand Down Expand Up @@ -129,6 +135,10 @@ def pytest_addoption(parser):
"Run the doctests in the rst documentation",
default=False)

parser.addini("doctest_ufunc",
"Run doctests in docstrings of Numpy ufuncs",
default=False)

parser.addini("doctest_plus_atol",
"set the absolute tolerance for float comparison",
default=1e-08)
Expand Down Expand Up @@ -157,11 +167,26 @@ def get_optionflags(parent):
return flag_int


def _is_numpy_ufunc(method):
try:
import numpy as np
except ModuleNotFoundError:
# If Numpy is not installed, then there can't be any ufuncs!
return False
while True:
try:
method = method.__wrapped__
except AttributeError:
break
return isinstance(method, np.ufunc)


def pytest_configure(config):
doctest_plugin = config.pluginmanager.getplugin('doctest')
run_regular_doctest = config.option.doctestmodules and not config.option.doctest_plus
use_ufunc = config.getini('doctest_ufunc') or config.option.doctest_ufunc
use_doctest_plus = config.getini(
'doctest_plus') or config.option.doctest_plus or config.option.doctest_only
'doctest_plus') or config.option.doctest_plus or config.option.doctest_only or use_ufunc
if doctest_plugin is None or run_regular_doctest or not use_doctest_plus:
return

Expand Down Expand Up @@ -238,7 +263,14 @@ def collect(self):
runner = doctest.DebugRunner(
verbose=False, optionflags=options, checker=OutputChecker())

for test in finder.find(module):
tests = finder.find(module)
if use_ufunc:
for method in module.__dict__.values():
if _is_numpy_ufunc(method):
found = finder.find(method, module=module)
tests += found

for test in tests:
if test.examples: # skip empty doctests
ignore_warnings_context_needed = False
show_warnings_context_needed = False
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ install_requires =

[options.extras_require]
test =
numpy
pytest-remotedata>=0.3.2
sphinx

Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import pytest

collect_ignore = ['ufunc_example']


def _wrap_docstring_in_func(func_name, docstring):
template = textwrap.dedent(r"""
Expand Down
42 changes: 42 additions & 0 deletions tests/test_doctest_ufunc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import sys
import glob

import pytest

pytest.importorskip('numpy')

pytest_plugins = ['pytester']


def test_help_message(testdir):
result = testdir.runpytest(
'--help',
)
# fnmatch_lines does an assertion internally
result.stdout.fnmatch_lines([
'*--doctest-ufunc*Enable running doctests in '
'docstrings of Numpy ufuncs.',
])


def test_example(testdir):
# Create and build example module
testdir.copy_example('tests/ufunc_example/_module2.c')
testdir.copy_example('tests/ufunc_example/module1.py')
testdir.copy_example('tests/ufunc_example/module2.py')
testdir.copy_example('tests/ufunc_example/setup.py')
testdir.run(sys.executable, 'setup.py', 'build')
build_dir, = glob.glob(str(testdir.tmpdir / 'build/lib.*'))

# Run pytest without doctests: 0 tests run
result = testdir.runpytest(build_dir)
result.assert_outcomes(passed=0, failed=0)

# Run pytest with doctests: 1 test run
result = testdir.runpytest(build_dir, '--doctest-modules')
result.assert_outcomes(passed=1, failed=0)

# Run pytest with doctests including ufuncs: 2 tests run
result = testdir.runpytest(build_dir, '--doctest-plus', '--doctest-modules', '--doctest-ufunc')
result.assert_outcomes(passed=2, failed=0)
68 changes: 68 additions & 0 deletions tests/ufunc_example/_module2.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION

#include <numpy/arrayobject.h>
#include <numpy/ufuncobject.h>
#include <Python.h>


static double foo_inner(double a, double b)
{
return a + b;
}


static void foo_loop(
char **args,
const npy_intp *dimensions,
const npy_intp *steps,
void *NPY_UNUSED(data)
) {
const npy_intp n = dimensions[0];
for (npy_intp i = 0; i < n; i ++)
{
*(double *) &args[2][i * steps[2]] = foo_inner(
*(double *) &args[0][i * steps[0]],
*(double *) &args[1][i * steps[1]]);
}
}


static PyUFuncGenericFunction foo_loops[] = {foo_loop};
static char foo_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE};
static void *foo_data[] = {NULL};
static const char foo_name[] = "foo";
static const char foo_docstring[] = ">>> foo(1, 2)\n3.0";

static PyModuleDef moduledef = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_module2",
.m_size = -1
};


PyMODINIT_FUNC PyInit__module2(void)
{
import_array();
import_ufunc();

PyObject *module = PyModule_Create(&moduledef);
if (!module)
return NULL;

PyObject *obj = PyUFunc_FromFuncAndData(
foo_loops, foo_data, foo_types, 1, 2, 1, PyUFunc_None, foo_name,
foo_docstring, 0);
if (!obj)
{
Py_DECREF(module);
return NULL;
}
if (PyModule_AddObject(module, foo_name, obj) < 0)
{
Py_DECREF(obj);
Py_DECREF(module);
return NULL;
}

return module;
}
7 changes: 7 additions & 0 deletions tests/ufunc_example/module1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def foo():
'''A doctest...
>>> foo()
1
'''
return 1
1 change: 1 addition & 0 deletions tests/ufunc_example/module2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from _module2 import foo # noqa: F401
7 changes: 7 additions & 0 deletions tests/ufunc_example/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from setuptools import setup, Extension
import numpy as np

ext = Extension('_module2', ['_module2.c'],
extra_compile_args=['-std=c99'],
include_dirs=[np.get_include()])
setup(name='example', py_modules=['module1', 'module2'], ext_modules=[ext])

0 comments on commit 3345ea7

Please sign in to comment.