pax_global_header00006660000000000000000000000064150745721230014520gustar00rootroot0000000000000052 comment=c6d12560783f1373c0143fa440a3f56f28bf60f2 scientific-python-pytest-doctestplus-d87b8cb/000077500000000000000000000000001507457212300215575ustar00rootroot00000000000000scientific-python-pytest-doctestplus-d87b8cb/.github/000077500000000000000000000000001507457212300231175ustar00rootroot00000000000000scientific-python-pytest-doctestplus-d87b8cb/.github/dependabot.yml000066400000000000000000000011771507457212300257550ustar00rootroot00000000000000# 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" scientific-python-pytest-doctestplus-d87b8cb/.github/release.yml000066400000000000000000000001771507457212300252670ustar00rootroot00000000000000changelog: exclude: authors: - dependabot - dependabot[bot] - pre-commit-ci - pre-commit-ci[bot] scientific-python-pytest-doctestplus-d87b8cb/.github/workflows/000077500000000000000000000000001507457212300251545ustar00rootroot00000000000000scientific-python-pytest-doctestplus-d87b8cb/.github/workflows/publish.yml000066400000000000000000000030561507457212300273510ustar00rootroot00000000000000name: 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.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@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: user: __token__ password: ${{ secrets.pypi_password }} scientific-python-pytest-doctestplus-d87b8cb/.github/workflows/python-tests.yml000066400000000000000000000055561507457212300303730ustar00rootroot00000000000000name: 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-pytest84 - os: windows-latest python-version: '3.14' toxenv: py314-test-pytestdev - os: macos-latest python-version: '3.14' toxenv: py314-test-pytestdev - os: ubuntu-latest python-version: '3.14' toxenv: py314-test-pytestdev-numpydev - os: ubuntu-latest python-version: '3.13' toxenv: py313-test-pytest83-pytestasyncio steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.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 }} scientific-python-pytest-doctestplus-d87b8cb/.gitignore000066400000000000000000000012331507457212300235460ustar00rootroot00000000000000# 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/ scientific-python-pytest-doctestplus-d87b8cb/.mailmap000066400000000000000000000022251507457212300232010ustar00rootroot00000000000000Brigitta 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 scientific-python-pytest-doctestplus-d87b8cb/CHANGES.rst000066400000000000000000000140241507457212300233620ustar00rootroot000000000000001.5.0 (2025-10-17) ================== - Adding the usage of the ``doctest_encoding`` ini option when overwriting files with the ``doctest-plus-generate-diff`` option. [#284] 1.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) # We see unclosed file ResourceWarning on windows with python 3.14 @pytest.mark.filterwarnings('ignore:unclosed file:ResourceWarning') 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( main_pytest_asyncio_xfails, 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.11.12', '3.11.13', '3.11.14', '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 scientific-python-pytest-doctestplus-d87b8cb/tests/test_encoding.py000066400000000000000000000106401507457212300261210ustar00rootroot00000000000000import locale import os from pathlib import Path from textwrap import dedent from typing import Callable, Optional import pytest pytest_plugins = ["pytester"] IS_CI = os.getenv("CI", "false") == "true" @pytest.fixture( params=[ ("A", "a", "utf-8"), ("☆", "★", "utf-8"), ("b", "B", "cp1252"), ("☁", "☀", "utf-8"), ], ids=[ "Aa-utf8", "star-utf8", "bB-cp1252", "cloud-utf8", ], ) def charset(request): return request.param @pytest.fixture() def basic_file(tmp_path: Path) -> Callable[[str, str, str], tuple[str, str, str]]: def makebasicfile(a, b, encoding: str) -> tuple[str, str, str]: """alternative implementation without the use of `testdir.makepyfile`.""" content = """ def f(): ''' >>> print('{}') {} ''' pass """ original = dedent(content.format(a, b)) expected_result = dedent(content.format(a, a)) original_file = tmp_path.joinpath("test_basic.py") original_file.write_text(original, encoding=encoding) expected_diff = dedent( f""" >>> print('{a}') - {b} + {a} """ ).strip("\n") return str(original_file), expected_diff, expected_result return makebasicfile @pytest.fixture() def ini_file(testdir) -> Callable[..., Path]: def makeini( encoding: Optional[str] = None, ) -> Path: """Create a pytest.ini file with the specified encoding.""" ini = ["[pytest]"] if encoding is not None: ini.append(f"doctest_encoding = {encoding}") ini.append("") p = testdir.makefile(".ini", pytest="\n".join(ini)) return Path(p) return makeini def test_basic_file_encoding_diff(testdir, capsys, basic_file, charset, ini_file): """ Test the diff from console output is as expected. """ a, b, encoding = charset # create python file to test file, diff, _ = basic_file(a, b, encoding) # create pytest.ini file ini = ini_file(encoding=encoding) assert ini.is_file(), "setup pytest.ini not created/found" testdir.inline_run( file, "--doctest-plus-generate-diff", "-c", str(ini), ) stdout, _ = capsys.readouterr() assert diff in stdout def test_basic_file_encoding_overwrite(testdir, basic_file, charset, ini_file): """ Test that the file is overwritten with the expected content. """ a, b, encoding = charset # create python file to test file, _, expected = basic_file(a, b, encoding) # create pytest.ini file ini = ini_file(encoding=encoding) assert ini.is_file(), "setup pytest.ini not created/found" testdir.inline_run( file, "--doctest-plus-generate-diff", "overwrite", "-c", str(ini), ) assert expected in Path(file).read_text(encoding) @pytest.mark.skipif(IS_CI, reason="skip on CI") def test_legacy_diff(testdir, capsys, basic_file, charset): """ Legacy test are supported to fail on Windows, when no encoding is provided. On Windows this is cp1252, so "utf-8" are expected to fail while writing test files. """ a, b, _ = charset try: file, diff, _ = basic_file(a, b, None) except UnicodeEncodeError: encoding = locale.getpreferredencoding(False) reason = f"could not encode {repr(charset)} with {encoding=}" pytest.xfail(reason=reason) testdir.inline_run( file, "--doctest-plus-generate-diff", ) stdout, _ = capsys.readouterr() assert diff in stdout @pytest.mark.skipif(IS_CI, reason="skip on CI") def test_legacy_overwrite(testdir, basic_file, charset): """ Legacy test are supported to fail on Windows, when no encoding is provided. On Windows this is cp1252, so "utf-8" are expected to fail while writing test files. """ a, b, _encoding = charset try: file, _, expected = basic_file(a, b, None) except UnicodeEncodeError: encoding = locale.getpreferredencoding(False) reason = f"could not encode {repr(charset)} with {encoding=}" pytest.xfail(reason=reason) testdir.inline_run( file, "--doctest-plus-generate-diff", "overwrite", ) assert expected in Path(file).read_text(_encoding) scientific-python-pytest-doctestplus-d87b8cb/tests/test_utils.py000066400000000000000000000005241507457212300254730ustar00rootroot00000000000000from 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') scientific-python-pytest-doctestplus-d87b8cb/tox.ini000066400000000000000000000034361507457212300231000ustar00rootroot00000000000000[tox] envlist = py{39,310,311,312,313,314}-test codestyle requires = setuptools >= 30.3.0 pip >= 19.3.1 isolated_build = true [testenv] changedir = .tmp/{envname} passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CI 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.* pytest84: pytest==8.4.* 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