diff --git a/.gitignore b/.gitignore index c65864f..6731cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -59,5 +59,8 @@ venv # PyCharm .idea +# VS code +.vscode + pytest_doctestplus/version.py pip-wheel-metadata/ diff --git a/CHANGES.rst b/CHANGES.rst index d52aa83..63f16fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ 0.10.0 (unreleased) =================== +- Added ``..doctest-remote-data::`` directive to control remote data + access for a chunk of code. [#137] + - Drop support for ``python`` 3.6. [#159] - Fixed a bug where the command-line option ``--remote-data=any`` (associated @@ -10,6 +13,7 @@ - Fix wrong behavior with ``IGNORE_WARNINGS`` and ``SHOW_WARNINGS`` that could make a block to pass instead of being skipped. [#148] + 0.9.0 (2021-01-14) ================== diff --git a/README.rst b/README.rst index 0452729..78cb5d6 100644 --- a/README.rst +++ b/README.rst @@ -273,7 +273,17 @@ marked: The ``+REMOTE_DATA`` directive indicates that the marked statement should only be executed if the ``--remote-data`` option is given. By default, all -statements marked with ``--remote-data`` will be skipped. +statements marked with the remote data directive will be skipped. + +Whole code example blocks can also be marked to control access to data from the internet +this way: + +.. code-block:: python + + .. doctest-remote-data:: + + >>> import requests + >>> r = requests.get('https://www.astropy.org') .. _pytest-remotedata: https://github.com/astropy/pytest-remotedata __ pytest-remotedata_ diff --git a/pytest_doctestplus/plugin.py b/pytest_doctestplus/plugin.py index f540556..86e3b13 100644 --- a/pytest_doctestplus/plugin.py +++ b/pytest_doctestplus/plugin.py @@ -90,9 +90,9 @@ def pytest_addoption(parser): parser.addoption("--text-file-format", action="store", help=( - "Text file format for narrative documentation. " - "Options accepted are 'txt', 'tex', and 'rst'. " - "This is no longer recommended, use --doctest-glob instead." + "Text file format for narrative documentation. " + "Options accepted are 'txt', 'tex', and 'rst'. " + "This is no longer recommended, use --doctest-glob instead." )) # Defaults to `atol` parameter from `numpy.allclose`. @@ -140,8 +140,8 @@ def pytest_addoption(parser): default=[]) parser.addini("doctest_subpackage_requires", - "A list of paths to skip if requirements are not satisfied. Each item in the list " - "should have the syntax path=req1;req2", + "A list of paths to skip if requirements are not satisfied." + "Each item in the list should have the syntax path=req1;req2", type='linelist', default=[]) @@ -157,7 +157,8 @@ def get_optionflags(parent): def pytest_configure(config): doctest_plugin = config.pluginmanager.getplugin('doctest') run_regular_doctest = config.option.doctestmodules and not config.option.doctest_plus - use_doctest_plus = config.getini('doctest_plus') or config.option.doctest_plus or config.option.doctest_only + use_doctest_plus = config.getini( + 'doctest_plus') or config.option.doctest_plus or config.option.doctest_only if doctest_plugin is None or run_regular_doctest or not use_doctest_plus: return @@ -324,6 +325,9 @@ class DocTestParserPlus(doctest.DocTestParser): installed. - ``.. doctest-skip-all``: Skip all subsequent doctests. + + - ``.. doctest-remote-data::``: Skip the next doctest chunk if + --remote-data is not passed. """ def parse(self, s, name=None): @@ -355,7 +359,8 @@ def parse(self, s, name=None): required = [] skip_next = False lines = entry.strip().splitlines() - if any([re.match('{} doctest-skip-all'.format(comment_char), x.strip()) for x in lines]): + if any(re.match( + '{} doctest-skip-all'.format(comment_char), x.strip()) for x in lines): skip_all = True continue @@ -382,6 +387,15 @@ def parse(self, s, name=None): skip_next = True continue + if config.getoption('remote_data', 'none') != 'any': + matches = (re.match( + r'{}\s+doctest-remote-data\s*::'.format(comment_char), + last_line) for last_line in last_lines) + + if any(matches): + skip_next = True + continue + matches = [re.match( r'{}\s+doctest-requires\s*::\s+(.*)'.format(comment_char), last_line) for last_line in last_lines] diff --git a/setup.cfg b/setup.cfg index c1df38c..f01003b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,10 @@ install_requires = setuptools>=30.3.0 packaging>=17.0 +[options.extras_require] +test = + pytest-remotedata>=0.3.2 + [options.entry_points] pytest11 = pytest_doctestplus = pytest_doctestplus.plugin diff --git a/tests/docs/skip_some.rst b/tests/docs/skip_some.rst index 63e75e7..b05410f 100644 --- a/tests/docs/skip_some.rst +++ b/tests/docs/skip_some.rst @@ -51,7 +51,7 @@ available: .. doctest-requires:: sys - >>> import sys + >>> import sys Bad Imports =========== diff --git a/tests/docs/skip_some_remote_data.rst b/tests/docs/skip_some_remote_data.rst new file mode 100644 index 0000000..9ecf828 --- /dev/null +++ b/tests/docs/skip_some_remote_data.rst @@ -0,0 +1,78 @@ +Some test cases for remote data +******************************* + +We only run tests on this file when ``remote-data`` is not opted in as most +of the code examples below should fail if not skipped. + + +Remote data block code sandwiched in block codes +================================================ + +This code block should work just fine:: + + >>> 1 + 1 + 2 + +This should be skipped when remote data is not requested +otherwise the test should fail:: + +.. doctest-remote-data:: + + >>> 1 + 3 + 2 + +This code block should work just fine:: + + >>> 1 + 1 + 2 + + +Remote data followed by plain block code +======================================== + +This one should be skipped when remote data is not requested +otherwise the test should fail:: + +.. doctest-remote-data:: + + >>> 1 + 3 + 2 + +This code block should work just fine:: + + >>> 1 + 1 + 2 + + +Several blocks of Remote data +============================= + +The three block codes should be skipped when remote data +is not requested otherwise the tests should fail: + +.. doctest-remote-data:: + + >>> 1 + 3 + 2 + +.. doctest-remote-data:: + + >>> 1 + 4 + 2 + +.. doctest-remote-data:: + + >>> 1 + 5 + 2 + +Composite directive with remote data +==================================== + +This should be skipped otherwise the test should fail:: + +.. doctest-remote-data:: + + >>> 1 + 1 + 3 + >>> import warnings + >>> warnings.warn('A warning occurred', UserWarning) # doctest: +IGNORE_WARNINGS diff --git a/tests/test_doctestplus.py b/tests/test_doctestplus.py index 6c10e97..2cf29e0 100644 --- a/tests/test_doctestplus.py +++ b/tests/test_doctestplus.py @@ -433,7 +433,7 @@ def test_ignore_warnings_rst(testdir): # First check that we get a warning if we don't add the IGNORE_WARNINGS # directive p = testdir.makefile(".rst", - """ + """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) @@ -444,7 +444,7 @@ def test_ignore_warnings_rst(testdir): # Now try with the IGNORE_WARNINGS directive p = testdir.makefile(".rst", - """ + """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +IGNORE_WARNINGS @@ -486,7 +486,7 @@ def myfunc(): def test_show_warnings_rst(testdir): p = testdir.makefile(".rst", - """ + """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS @@ -498,7 +498,7 @@ def test_show_warnings_rst(testdir): # Make sure it fails if warning message is missing p = testdir.makefile(".rst", - """ + """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS @@ -509,7 +509,7 @@ def test_show_warnings_rst(testdir): # Make sure it fails if warning message is missing p = testdir.makefile(".rst", - """ + """ :: >>> import warnings >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS @@ -775,3 +775,142 @@ def test_doctest_skip(testdir): """ ) testdir.inline_run(p, '--doctest-plus', '--doctest-rst').assertoutcome(skipped=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) diff --git a/tox.ini b/tox.ini index 9016f7d..4dc7920 100644 --- a/tox.ini +++ b/tox.ini @@ -21,13 +21,20 @@ deps = pytest62: pytest==6.2.* pytestdev: git+https://github.com/pytest-dev/pytest#egg=pytest +extras = + test + commands = pip freeze + # Ignore directly running tests in ``skip_some_remote_data.rst`` with + # ``remote-data`` as there are some artifical 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} + [testenv:codestyle] changedir = skip_install = true