././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737778683.8487067 pytest_doctestplus-1.4.0/0000755000175100001660000000000014745062774015120 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737778683.8407066 pytest_doctestplus-1.4.0/.github/0000755000175100001660000000000014745062774016460 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/.github/dependabot.yml0000644000175100001660000000117714745062755021315 0ustar00runnerdocker# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: ".github/workflows" # Location of package manifests schedule: interval: "monthly" groups: actions: patterns: - "*" labels: - "no-changelog-entry-needed" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737778683.8417068 pytest_doctestplus-1.4.0/.github/workflows/0000755000175100001660000000000014745062774020515 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/.github/workflows/publish.yml0000644000175100001660000000305614745062755022711 0ustar00runnerdockername: Release on: pull_request: # We also want this workflow triggered if the 'Build wheels' label is added # or present when PR is updated types: - synchronize - labeled push: tags: - '*' jobs: build-n-publish: name: Build and publish Python 🐍 distributions 📦 to PyPI runs-on: ubuntu-latest if: ((github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) || contains(github.event.pull_request.labels.*.name, 'Build wheels')) steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.10' - name: Install python-build and twine run: python -m pip install build "twine>=3.3" - name: Build package run: python -m build --sdist --wheel . - name: List result run: ls -l dist - name: Check long_description run: python -m twine check --strict dist/* - name: Test package run: | cd .. python -m venv testenv testenv/bin/pip install pytest pytest-remotedata $(ls pytest-doctestplus/dist/*.whl)[test] testenv/bin/pytest pytest-doctestplus/tests --doctest-plus --doctest-rst - name: Publish distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 with: user: __token__ password: ${{ secrets.pypi_password }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/.github/workflows/python-tests.yml0000644000175100001660000000540414745062755023723 0ustar00runnerdockername: Run unit tests on: pull_request: push: branches: [ main ] tags: - '*' workflow_dispatch: schedule: # Run every Tuesday at 03:53 UTC - cron: 53 3 * * 2 jobs: tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false # The aim with the matrix below is to walk through a representative but not full combination of OS, Python, and pytest versions. matrix: include: - os: ubuntu-latest python-version: '3.9' toxenv: py39-test-pytestoldest - os: windows-latest python-version: '3.9' toxenv: py39-test-pytest50 - os: macos-13 python-version: '3.9' toxenv: py39-test-pytest51 - os: ubuntu-latest python-version: '3.9' toxenv: py39-test-pytest60 - os: ubuntu-latest python-version: '3.9' toxenv: py39-test-pytest62 - os: ubuntu-latest python-version: '3.10' toxenv: py310-test-pytest70 - os: ubuntu-latest python-version: '3.10' toxenv: py310-test-pytest71 - os: windows-latest python-version: '3.11' toxenv: py311-test-pytest72 - os: ubuntu-latest python-version: '3.11' toxenv: py311-test-pytest73 - os: ubuntu-latest python-version: '3.11' toxenv: py311-test-pytest74 - os: ubuntu-latest python-version: '3.12' toxenv: py312-test-pytest80 - os: windows-latest python-version: '3.12' toxenv: py312-test-pytest81 - os: ubuntu-latest python-version: '3.12' toxenv: py312-test-pytest82 - os: macos-latest python-version: '3.13' toxenv: py313-test-pytest83 - os: windows-latest python-version: '3.13' toxenv: py313-test-pytestdev - os: macos-latest python-version: '3.12' toxenv: py312-test-pytestdev - os: ubuntu-latest python-version: '3.13' toxenv: py313-test-pytestdev-numpydev - os: ubuntu-latest python-version: '3.13' toxenv: py313-test-pytest83-pytestasyncio steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install Tox run: python -m pip install tox - name: Run Tox run: tox ${{ matrix.toxargs }} -v -e ${{ matrix.toxenv }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/.gitignore0000644000175100001660000000123314745062755017106 0ustar00runnerdocker# Compiled files *.py[cod] *.a *.o *.so *.pyd __pycache__ # Ignore .c files by default to avoid including generated code. If you want to # add a non-generated .c extension, use `git add -f filename.c`. *.c # Other generated files MANIFEST # Sphinx _build _generated docs/api docs/generated # Packages/installer info *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg distribute-*.tar.gz # Other .cache .tox .*.swp .*.swo *~ .project .pydevproject .settings .coverage cover htmlcov .pytest_cache # Env .venv venv .env # Mac OSX .DS_Store # PyCharm .idea # VS code .vscode pytest_doctestplus/version.py pip-wheel-metadata/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/.mailmap0000644000175100001660000000222514745062755016541 0ustar00runnerdockerBrigitta Sipőcz Brigitta Sipőcz Dan D'Avella Dan D'Avella E. Madison Bray E. Madison Bray Hans Moritz Günther Kirill Tchernyshyov Leo Singer Matt Davis Matteo Bachetti Matteo Bachetti Michael Seifert Pey Lian Lim <2090236+pllim@users.noreply.github.com> Philipp A. Pratik Patel Sebastian Berg Simon Conseil Simon Conseil Tinuade Adeleke ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/CHANGES.rst0000644000175100001660000001354214745062755016726 0ustar00runnerdocker1.4.0 (2025-01-24) ================== - Fixing compatibility with pytest-asyncio. [#278] - Adding new directive ``doctest-requires-all`` to conditionally skip all doctests in narrative documentations based on availability of dependencies. [#280] - Adding new directive ``doctest-remote-data-all`` to conditionally skip all doctests in narrative documentations based on availability of remote-data. [#281] - Versions of Python <3.9 are no longer supported. [#274] 1.3.0 (2024-11-25) ================== - Fixing output update for multiline code. [#253] - Fixing Python 3.13 compatibility. [#260] - Dropped ``setuptools`` as a runtime dependency. [#258] - Fixing bug of assuming doctestmodules exists on Namespace. [#271] 1.2.1 (2024-03-09) ================== - Compatibility with pytest 8.1.1 [#241, #242] 1.2.0 (2024-03-04) ================== - Compatibility with pytest 8.1. [#236, #238] 1.1.0 (2023-12-13) ================== - Added ``--doctest-plus-generate-diff`` to update documentation based on actual output. [#227] - Fix module level ``__doctest_requires__``. [#228] - Versions of Python <3.8 are no longer supported. [#217] - Fix erroneous attempt to import ``__main__.py`` by skipping it. [#232] - Respect pytest ``--import-mode``. [#233] 1.0.0 (2023-08-11) ================== - Changing GitHub organization. 0.13.0 (2023-06-07) =================== - Compatibility with pytest 7.4 with respect to ``norecursedirs`` handling. [#201] - Respect ``--doctest-continue-on-failure`` flag. [#197] - Report doctests raising skip exceptions as skipped. [#196] 0.12.1 (2022-09-26) =================== - Allow floating point comparison in Python dictionary. [#186] 0.12.0 (2022-02-25) =================== - Run doctests in docstrings of Numpy ufuncs. [#174, #175] 0.11.2 (2021-12-09) =================== - Fix version check for pytest 7.0.0rc1. [#171] - Recognize text beginning with ``\n3", ) testdir.makefile( '.rst', foo_2=".. >>> 1 + 1\n3", ) testdir.makefile( '.tex', foo_3="% >>> 1 + 1\n3", ) testdir.makefile( '.txt', foo_4="# >>> 1 + 1\n3", ) testdir.inline_run( '--doctest-plus', '--doctest-glob', '*.md', '--doctest-glob', '*.rst', '--doctest-glob', '*.tex', '--doctest-glob', '*.txt' ).assertoutcome(passed=0) def test_text_file_comment_chars(testdir): # override default comment chars testdir.makeini( """ [pytest] text_file_extensions = .rst=# .tex=# """ ) testdir.makefile( '.rst', foo_1="# >>> 1 + 1\n3", ) testdir.makefile( '.tex', foo_2="# >>> 1 + 1\n3", ) testdir.inline_run( '--doctest-plus', '--doctest-glob', '*.rst', '--doctest-glob', '*.tex', '--doctest-glob', '*.txt' ).assertoutcome(passed=0) def test_ignore_option(testdir): testdir.makepyfile(foo=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makepyfile(bar=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makefile('.rst', foo='>>> 1+1\n2') testdir.inline_run('--doctest-plus').assertoutcome(passed=2) testdir.inline_run('--doctest-plus', '--doctest-rst').assertoutcome(passed=3) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore', '.' ).assertoutcome(passed=0) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore', 'bar.py' ).assertoutcome(passed=2) def test_ignore_glob_option(testdir): testdir.makepyfile(foo=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makepyfile(bar=""" def f(): ''' >>> 1+1 2 ''' pass """) testdir.makefile('.rst', foo='>>> 1+1\n2') testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore-glob', 'foo*' ).assertoutcome(passed=1) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore-glob', 'bar*' ).assertoutcome(passed=2) testdir.inline_run( '--doctest-plus', '--doctest-rst', '--ignore-glob', '*.rst' ).assertoutcome(passed=2) def test_doctest_only(testdir, makepyfile, maketestfile, makerstfile): # regular python files with doctests makepyfile(p1='>>> 1 + 1\n2') makepyfile(p2='>>> 1 + 1\n3') # regular test files maketestfile(test_1='foo') maketestfile(test_2='bar') # rst files makerstfile(r1='>>> 1 + 1\n2') makerstfile(r3='>>> 1 + 1\n3') makerstfile(r2='>>> 1 + 2\n3') # regular tests testdir.inline_run().assertoutcome(passed=2) # regular + doctests testdir.inline_run("--doctest-plus").assertoutcome(passed=3, failed=1) # regular + doctests + doctest in rst files testdir.inline_run("--doctest-plus", "--doctest-rst").assertoutcome(passed=5, failed=2) # only doctests in python files, implicit usage of doctest-plus testdir.inline_run("--doctest-only").assertoutcome(passed=1, failed=1) # only doctests in python files testdir.inline_run("--doctest-only", "--doctest-rst").assertoutcome(passed=3, failed=2) def test_doctest_float_replacement(tmp_path): test1 = dedent(""" This will demonstrate a doctest that fails due to a few extra decimal places:: >>> 1.0 / 3.0 0.333333333333333311 """) test2 = dedent(""" This is the same test, but it should pass with use of +FLOAT_CMP:: >>> 1.0 / 3.0 # doctest: +FLOAT_CMP 0.333333333333333311 """) test1_rst = tmp_path / "test1.rst" test2_rst = tmp_path / "test2.rst" test1_rst.write_text(test1) test2_rst.write_text(test2) with pytest.raises(doctest.DocTestFailure): doctest.testfile( test1_rst, module_relative=False, raise_on_error=True, verbose=False, encoding="utf-8", ) doctest.testfile( test2_rst, module_relative=False, raise_on_error=True, verbose=False, encoding="utf-8", ) # Note that each entry under doctest_subpackage_requires has different whitespace # around the = to make sure that all cases work properly. SUBPACKAGE_REQUIRES_INI = ( "makeini", """ [pytest] doctest_subpackage_requires = test/a/* = pytest>1 test/b/*= pytest>1;averyfakepackage>99999.9 test/c/*=anotherfakepackage>=22000.1.2 """ ) SUBPACKAGE_REQUIRES_PYPROJECT = ( "makepyprojecttoml", """ [tool.pytest.ini_options] doctest_subpackage_requires = [ "test/a/* = pytest>1", "test/b/*= pytest>1;averyfakepackage>99999.9", "test/c/*=anotherfakepackage>=22000.1.2", ] """ ) @pytest.fixture() def subpackage_requires_testdir(testdir, request): if request.param[0] == 'makepyprojecttoml' and PYTEST_LT_6: return None, None config_file = getattr(testdir, request.param[0])(request.param[1]) test = testdir.mkdir('test') a = test.mkdir('a') b = test.mkdir('b') c = test.mkdir('c') pyfile = dedent(""" def f(): ''' >>> 1 1 ''' pass """) a.join('testcode.py').write(pyfile) b.join('testcode.py').write(pyfile) c.join('testcode.py').write(pyfile) return config_file, testdir @pytest.mark.parametrize('subpackage_requires_testdir', [SUBPACKAGE_REQUIRES_INI, SUBPACKAGE_REQUIRES_PYPROJECT], indirect=True) def test_doctest_subpackage_requires(subpackage_requires_testdir, caplog): config_file, testdir = subpackage_requires_testdir if config_file is None: pytest.skip("pyproject.toml not supported in pytest<6") reprec = testdir.inline_run(str(testdir), f"-c={config_file}", "--doctest-plus") reprec.assertoutcome(passed=1) assert reprec.listoutcomes()[0][0].location[0] == os.path.join('test', 'a', 'testcode.py') assert caplog.text == '' @pytest.mark.parametrize(('import_mode', 'expected'), [ pytest.param('importlib', dict(passed=2), marks=pytest.mark.skipif(PYTEST_LT_6, reason="importlib import mode not supported on Pytest <6"), id="importlib"), pytest.param('append', dict(failed=1), id="append"), pytest.param('prepend', dict(failed=1), id="prepend"), ]) def test_import_mode(testdir, import_mode, expected): """Test that two files with the same name but in different folders work with --import-mode=importlib.""" a = testdir.mkdir('a') b = testdir.mkdir('b') pyfile = dedent(""" def f(): ''' >>> 1 1 ''' """) a.join('testcode.py').write(pyfile) b.join('testcode.py').write(pyfile) reprec = testdir.inline_run(str(testdir), "--doctest-plus", f"--import-mode={import_mode}") reprec.assertoutcome(**expected) def test_doctest_skip(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) p = testdir.makefile( '.rst', """ .. doctest-skip:: >>> import asdf >>> asdf.open('file.asdf') # doctest: +IGNORE_WARNINGS """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_remote_data_all(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) p = testdir.makefile( '.rst', """ This is a narrative docs, which some of the lines requiring remote-data access. The first code block always passes, the second is skipped without remote data and the last section is skipped due to the all option. >>> print("Test") Test .. doctest-remote-data-all:: >>> from contextlib import closing >>> from urllib.request import urlopen >>> with closing(urlopen('https://www.astropy.org')) as remote: ... remote.read() # doctest: +IGNORE_OUTPUT Narrative before a codeblock that should fail normally but with the all option in the directive it is skipped over thus producing a passing status. >>> print(123) """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(failed=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(passed=1) # We repeat all testst including remote data with and without it opted in def test_remote_data_url(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) p = testdir.makefile( '.rst', """ # This test should be skipped when remote data is not requested. .. doctest-remote-data:: >>> from contextlib import closing >>> from urllib.request import urlopen >>> with closing(urlopen('https://www.astropy.org')) as remote: ... remote.read() # doctest: +IGNORE_OUTPUT """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(passed=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_remote_data_float_cmp(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) p = testdir.makefile( '.rst', """ #This test is skipped when remote data is not requested .. doctest-remote-data:: >>> x = 1/3. >>> x # doctest: +FLOAT_CMP 0.333333 """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(passed=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_remote_data_ignore_whitespace(testdir): testdir.makeini( """ [pytest] doctest_optionflags = NORMALIZE_WHITESPACE doctestplus = enabled """) p = testdir.makefile( '.rst', """ #This test should be skipped when remote data is not requested, and should #pass when remote data is requested .. doctest-remote-data:: >>> a = "foo " >>> print(a) foo """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(passed=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_remote_data_ellipsis(testdir): testdir.makeini( """ [pytest] doctest_optionflags = ELLIPSIS doctestplus = enabled """) p = testdir.makefile( '.rst', """ # This test should be skipped when remote data is not requested, and should # pass when remote data is requested .. doctest-remote-data:: >>> a = "freedom at last" >>> print(a) freedom ... """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(passed=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_remote_data_requires(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) p = testdir.makefile( '.rst', """ # This test should be skipped when remote data is not requested. # It should also be skipped instead of failing when remote data is requested because # the module required does not exist .. doctest-remote-data:: .. doctest-requires:: does-not-exist >>> 1 + 1 3 """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(skipped=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_remote_data_ignore_warnings(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """) p = testdir.makefile( '.rst', """ # This test should be skipped if remote data is not requested. .. doctest-remote-data:: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +IGNORE_WARNINGS """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst', '--remote-data').assertoutcome(passed=1) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=1) def test_skiptest(testdir): testdir.makeini( """ [pytest] doctestplus = enabled """ ) p = testdir.makepyfile( """ class MyClass: ''' >>> import pytest >>> pytest.skip("I changed my mind") >>> assert False, "This should not be reached" ''' pass """ ) reprec = testdir.inline_run(p, "--doctest-plus") reprec.assertoutcome(skipped=1, failed=0) @pytest.mark.parametrize('cont_on_fail', [False, True]) def test_fail_two_tests(testdir, cont_on_fail): p = testdir.makepyfile( """ class MyClass: ''' .. doctest:: >>> print(2) 1 .. doctest:: >>> print(3) 1 ''' pass """ ) arg = ("--doctest-continue-on-failure",) if cont_on_fail else () reprec = testdir.inline_run(p, "--doctest-plus", *arg) reprec.assertoutcome(skipped=0, failed=1) _, _, failed = reprec.listoutcomes() report = failed[0] assert "Expected:\n 1\nGot:\n 2" in report.longreprtext assert ("Expected:\n 1\nGot:\n 3" in report.longreprtext) is cont_on_fail @pytest.mark.parametrize('cont_on_fail', [False, True]) def test_fail_data_dependency(testdir, cont_on_fail): p = testdir.makepyfile( """ class MyClass: ''' .. doctest:: >>> import nonexistentmodule as nem >>> a = nem.calculate_something() ''' pass """ ) arg = ("--doctest-continue-on-failure",) if cont_on_fail else () reprec = testdir.inline_run(p, "--doctest-plus", *arg) reprec.assertoutcome(skipped=0, failed=1) _, _, failed = reprec.listoutcomes() # Both lines fail in a single error report = failed[0] assert " as nem\nUNEXPECTED EXCEPTION: ModuleNotFoundError" in report.longreprtext assert ("something()\nUNEXPECTED EXCEPTION: NameError" in report.longreprtext) is cont_on_fail @pytest.mark.xfail( has_pytest_asyncio, reason='pytest_asyncio monkey-patches .collect()') def test_main(testdir): pkg = testdir.mkdir('pkg') code = dedent( ''' def f(): raise RuntimeError("This is a CLI, do not execute module while doctesting") f() ''' ) pkg.join('__init__.py').write_text("", "utf-8") main_path = pkg.join('__main__.py') main_path.write_text(code, "utf-8") testdir.inline_run(pkg).assertoutcome(passed=0) testdir.inline_run(pkg, '--doctest-plus').assertoutcome(passed=0) @pytest.mark.xfail( python_version() in ('3.11.9', '3.11.10', '3.11.11', '3.12.3'), reason='broken by https://github.com/python/cpython/pull/115440') def test_ufunc(testdir): pytest.importorskip('numpy') # Create and build example module testdir.makepyfile(module1=""" def foo(): '''A doctest... >>> foo() 1 ''' return 1 """) testdir.makepyfile(module2=""" import functools from _module2 import foo, bar, bat as _bat def wrap_func(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper bat = wrap_func(_bat) """) testdir.makepyfile(setup=""" 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]) """) testdir.makefile('.c', _module2=r""" #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include #include static double ufunc_inner(double a, double b) { return a + b; } static void ufunc_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]] = ufunc_inner( *(double *) &args[0][i * steps[0]], *(double *) &args[1][i * steps[1]]); } } static PyUFuncGenericFunction ufunc_loops[] = {ufunc_loop}; static char ufunc_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static void *ufunc_data[] = {NULL}; static const char ufunc_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; /* Add a ufunc _with_ a docstring. */ PyObject *foo = PyUFunc_FromFuncAndData( ufunc_loops, ufunc_data, ufunc_types, 1, 2, 1, PyUFunc_None, "foo", ufunc_docstring, 0); if (!foo) { Py_DECREF(module); return NULL; } if (PyModule_AddObject(module, "foo", foo) < 0) { Py_DECREF(foo); Py_DECREF(module); return NULL; } /* Add a ufunc _without_ a docstring. */ PyObject *bar = PyUFunc_FromFuncAndData( ufunc_loops, ufunc_data, ufunc_types, 1, 2, 1, PyUFunc_None, "bar", NULL, 0); if (!bar) { Py_DECREF(module); return NULL; } if (PyModule_AddObject(module, "bar", bar) < 0) { Py_DECREF(bar); Py_DECREF(module); return NULL; } /* Add another ufunc _without_ a docstring. */ PyObject *bat = PyUFunc_FromFuncAndData( ufunc_loops, ufunc_data, ufunc_types, 1, 2, 1, PyUFunc_None, "bat", NULL, 0); if (!bat) { Py_DECREF(module); return NULL; } if (PyModule_AddObject(module, "bat", bat) < 0) { Py_DECREF(bat); Py_DECREF(module); return NULL; } return module; } """) testdir.run(sys.executable, 'setup.py', 'build') build_dir, = glob.glob(str(testdir.tmpdir / 'build/lib.*')) result = testdir.inline_run(build_dir, '--doctest-plus', '--doctest-modules') result.assertoutcome(passed=1, failed=0) result = testdir.inline_run(build_dir, '--doctest-plus', '--doctest-modules', '--doctest-ufunc') result.assertoutcome(passed=2, failed=0) NORCURSEDIRS_INI = ( "makeini", """ [pytest] doctest_norecursedirs = "bad_dir" "*/bad_file.py" """ ) NORCURSEDIRS_PYPROJECT = ( "makepyprojecttoml", """ [tool.pytest.ini_options] doctest_norecursedirs = [ "bad_dir", "*/bad_file.py", ] """ ) @pytest.fixture() def norecursedirs_testdir(testdir, request): if request.param[0] == 'makepyprojecttoml' and PYTEST_LT_6: return None, None config_file = getattr(testdir, request.param[0])(request.param[1]) bad_text = dedent(""" def f(): ''' This should fail doc testing >>> 1 2 ''' pass """) good_text = dedent(""" def g(): ''' This should pass doc testing >>> 1 1 ''' pass """) # Create a bad file that should be by its folder bad_subdir = testdir.mkdir("bad_dir") bad_file = bad_subdir.join("test_foobar.py") bad_file.write_text(bad_text, "utf-8") # Create a bad file that should be skipped by its name okay_subdir1 = testdir.mkdir("okay_foo_dir") bad_file = okay_subdir1.join("bad_file.py") bad_file.write_text(bad_text, "utf-8") # Create a good file in that directory that doctest won't skip good_file1 = okay_subdir1.join("good_file1.py") good_file1.write_text(good_text, "utf-8") # Create another bad file that should be skipped by its name okay_subdir2 = testdir.mkdir("okay_bar_dir") bad_file = okay_subdir2.join("bad_file.py") bad_file.write_text(bad_text, "utf-8") # Create a good file in that directory that doctest won't skip good_file2 = okay_subdir2.join("good_file2.py") good_file2.write_text(good_text, "utf-8") return config_file, testdir @pytest.mark.parametrize('norecursedirs_testdir', [NORCURSEDIRS_INI, NORCURSEDIRS_PYPROJECT], indirect=True) def test_doctest_norecursedirs(norecursedirs_testdir): config_file, testdir = norecursedirs_testdir if config_file is None: pytest.skip("pyproject.toml not supported in pytest<6") reprec = testdir.inline_run(str(testdir), f"-c={config_file}", "--doctest-plus") reprec.assertoutcome(passed=2) def test_norecursedirs(testdir): testdir.makeini( """ [pytest] norecursedirs = \"bad_dir\" doctestplus = enabled """ ) subdir = testdir.mkdir("bad_dir") badfile = subdir.join("test_foobar.py") badfile.write_text(""" def f(): ''' >>> x = 1/3. >>> x 0.333333 ''' fail """, "utf-8") reprec = testdir.inline_run(str(testdir), "--doctest-plus") reprec.assertoutcome(failed=0, passed=0) def test_generate_diff_basic(testdir, capsys): p = testdir.makepyfile(""" def f(): ''' >>> print(2) 4 >>> print(3) 5 ''' pass """) with open(p) as f: original = f.read() testdir.inline_run(p, "--doctest-plus-generate-diff") diff = dedent(""" >>> print(2) - 4 + 2 >>> print(3) - 5 + 3 """) captured = capsys.readouterr() assert diff in captured.out testdir.inline_run(p, "--doctest-plus-generate-diff=overwrite") captured = capsys.readouterr() assert "Applied fix to the following files" in captured.out with open(p) as f: result = f.read() assert result == original.replace("4", "2").replace("5", "3") def test_generate_diff_multiline(testdir, capsys): p = testdir.makepyfile(""" def f(): ''' >>> print(2) 2 >>> for i in range(4): ... print(i) 1 2 ''' pass """) with open(p) as f: original = f.read() testdir.inline_run(p, "--doctest-plus-generate-diff") diff = dedent(""" >>> for i in range(4): ... print(i) + 0 1 2 + 3 """) captured = capsys.readouterr() assert diff in captured.out testdir.inline_run(p, "--doctest-plus-generate-diff=overwrite") captured = capsys.readouterr() assert "Applied fix to the following files" in captured.out with open(p) as f: result = f.read() original_fixed = original.replace("1\n 2", "\n ".join(["0", "1", "2", "3"])) assert result == original_fixed ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/tests/test_utils.py0000644000175100001660000000052414745062755021033 0ustar00runnerdockerfrom pytest_doctestplus.utils import ModuleChecker class TestModuleChecker: def test_simple(self): c = ModuleChecker() assert c.check('sys') assert not c.check('foobar') def test_with_version(self): c = ModuleChecker() assert c.check('pytest>1.0') assert not c.check('foobar>1.0') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737778669.0 pytest_doctestplus-1.4.0/tox.ini0000644000175100001660000000332514745062755016435 0ustar00runnerdocker[tox] envlist = py{39,310,311,312,313}-test codestyle requires = setuptools >= 30.3.0 pip >= 19.3.1 isolated_build = true [testenv] changedir = .tmp/{envname} setenv = numpydev: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple description = run tests deps = pytestoldest: pytest==4.6.0 pytest50: pytest==5.0.* pytest51: pytest==5.1.* pytest52: pytest==5.2.* pytest53: pytest==5.3.* pytest60: pytest==6.0.* pytest61: pytest==6.1.* pytest62: pytest==6.2.* pytest70: pytest==7.0.* pytest71: pytest==7.1.* pytest72: pytest==7.2.* pytest73: pytest==7.3.* pytest74: pytest==7.4.* pytest80: pytest==8.0.* pytest81: pytest==8.1.* pytest82: pytest==8.2.* pytest83: pytest==8.3.* pytestdev: git+https://github.com/pytest-dev/pytest#egg=pytest numpydev: numpy>=0.0.dev0 pytestasyncio: pytest-asyncio extras = test commands = pip freeze # Ignore directly running tests in ``skip_some_remote_data.rst`` with # ``remote-data`` as there are some artificial failures included in there. pytest {toxinidir}/tests --ignore={toxinidir}/tests/docs/skip_some_remote_data.rst --doctest-plus --doctest-rst --remote-data {posargs} pytest {toxinidir}/tests {posargs} pytest {toxinidir}/tests --doctest-plus {posargs} pytest {toxinidir}/tests --doctest-plus --doctest-rst {posargs} pytest {toxinidir}/tests --doctest-plus --doctest-rst --text-file-format=tex {posargs} sphinx-build {toxinidir}/tests {toxinidir}/tests/_build/html -W [testenv:codestyle] changedir = skip_install = true description = check code style, e.g. with flake8 deps = flake8 commands = flake8 pytest_doctestplus --count