diff --git a/CHANGES.rst b/CHANGES.rst index c8cc0f0..d35cb10 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ - Declare ``setuptools`` runtime dependency [#93] +- Add ``SHOW_WARNINGS`` flag to show warnings. [#136] + 0.8.0 (2020-07-31) ================== diff --git a/README.rst b/README.rst index 9adf254..e224bff 100644 --- a/README.rst +++ b/README.rst @@ -147,6 +147,23 @@ you can make use of the ``IGNORE_WARNINGS`` flag. For example: >>> np.mean([]) # doctest: +IGNORE_WARNINGS np.nan +Showing warnings +~~~~~~~~~~~~~~~~ + +If code in a doctest emits a warning and you want to make sure that warning is +shown, you can make use of the ``SHOW_WARNINGS`` flag. This is useful when +warnings are turned into errors by pytest, and also because by default warnings +are printed to stderr. This is the opposite from ``IGNORE_WARNINGS`` so +obviously the two flags should not be used together. For example: + +.. code-block:: python + + >>> import numpy as np + >>> np.mean([]) # doctest: +SHOW_WARNINGS + RuntimeWarning: Mean of empty slice. + RuntimeWarning: invalid value encountered in double_scalars + np.nan + Skipping Tests ~~~~~~~~~~~~~~ diff --git a/pytest_doctestplus/output_checker.py b/pytest_doctestplus/output_checker.py index b6f02a8..511503f 100644 --- a/pytest_doctestplus/output_checker.py +++ b/pytest_doctestplus/output_checker.py @@ -20,6 +20,7 @@ IGNORE_OUTPUT = doctest.register_optionflag('IGNORE_OUTPUT') IGNORE_OUTPUT_3 = doctest.register_optionflag('IGNORE_OUTPUT_3') IGNORE_WARNINGS = doctest.register_optionflag('IGNORE_WARNINGS') +SHOW_WARNINGS = doctest.register_optionflag('SHOW_WARNINGS') # These might appear in some doctests and are used in the default pytest # doctest plugin. This plugin doesn't actually implement these flags but this diff --git a/pytest_doctestplus/plugin.py b/pytest_doctestplus/plugin.py index 10109b0..1e483f3 100644 --- a/pytest_doctestplus/plugin.py +++ b/pytest_doctestplus/plugin.py @@ -10,12 +10,13 @@ import sys import warnings -from packaging.version import Version - import pytest +from packaging.version import Version from pytest_doctestplus.utils import ModuleChecker -from .output_checker import FIX, IGNORE_WARNINGS, OutputChecker, REMOTE_DATA + +from .output_checker import (FIX, IGNORE_WARNINGS, REMOTE_DATA, SHOW_WARNINGS, + OutputChecker) try: from textwrap import indent @@ -32,12 +33,12 @@ def indent(text, prefix): } -# For the IGNORE_WARNINGS option, we create a context manager that doesn't -# require us to add any imports to the example list and contains everything -# that is needed to silence warnings. +# For the IGNORE_WARNINGS and SHOW_WARNINGS option, we create a context manager +# that doesn't require us to add any imports to the example list and contains +# everything that is needed to silence or print warnings. IGNORE_WARNINGS_CONTEXT = """ -class _doctestplus_ignore_all_warnings(object): +class _doctestplus_ignore_all_warnings: def __init__(self): import warnings @@ -54,6 +55,26 @@ def __exit__(self, *args, **kwargs): """.lstrip() +SHOW_WARNINGS_CONTEXT = """ +class _doctestplus_show_all_warnings: + + def __init__(self): + import warnings + self._cw = warnings.catch_warnings(record=True) + + def __enter__(self, *args, **kwargs): + self.result = self._cw.__enter__(*args, **kwargs) + import warnings + warnings.simplefilter('always') + return self.result + + def __exit__(self, *args, **kwargs): + self._cw.__exit__(*args, **kwargs) + for warn in self.result: + print(f'{warn._category_name}: {warn.message}') +""".lstrip() + + # these pytest hooks allow us to mark tests and run the marked tests with # specific command line options. def pytest_addoption(parser): @@ -200,6 +221,7 @@ def collect(self): if config.getoption('remote_data', 'none') != 'any': ignore_warnings_context_needed = False + show_warnings_context_needed = False for example in test.examples: @@ -210,13 +232,24 @@ def collect(self): + indent(example.source, ' ')) ignore_warnings_context_needed = True + # Same for SHOW_WARNINGS + if example.options.get(SHOW_WARNINGS, False): + example.source = ("with _doctestplus_show_all_warnings():\n" + + indent(example.source, ' ')) + show_warnings_context_needed = True + if example.options.get(REMOTE_DATA): example.options[doctest.SKIP] = True # We insert the definition of the context manager to ignore # warnings at the start of the file if needed. if ignore_warnings_context_needed: - test.examples.insert(0, doctest.Example(source=IGNORE_WARNINGS_CONTEXT, want='')) + test.examples.insert(0, doctest.Example( + source=IGNORE_WARNINGS_CONTEXT, want='')) + + if show_warnings_context_needed: + test.examples.insert(0, doctest.Example( + source=SHOW_WARNINGS_CONTEXT, want='')) try: yield doctest_plugin.DoctestItem.from_parent( @@ -289,6 +322,7 @@ def parse(self, s, name=None): comment_char = comment_characters[ext] ignore_warnings_context_needed = False + show_warnings_context_needed = False for entry in result: @@ -344,6 +378,12 @@ def parse(self, s, name=None): + indent(entry.source, ' ')) ignore_warnings_context_needed = True + # Same to show warnings + if entry.options.get(SHOW_WARNINGS, False): + entry.source = ("with _doctestplus_show_all_warnings():\n" + + indent(entry.source, ' ')) + show_warnings_context_needed = True + has_required_modules = DocTestFinderPlus.check_required_modules(required) if skip_all or skip_next or not has_required_modules: entry.options[doctest.SKIP] = True @@ -356,6 +396,9 @@ def parse(self, s, name=None): if ignore_warnings_context_needed: result.insert(0, doctest.Example(source=IGNORE_WARNINGS_CONTEXT, want='')) + if show_warnings_context_needed: + result.insert(0, doctest.Example(source=SHOW_WARNINGS_CONTEXT, want='')) + return result config.pluginmanager.register( diff --git a/tests/test_doctestplus.py b/tests/test_doctestplus.py index 5a1e8f7..9bf7cae 100644 --- a/tests/test_doctestplus.py +++ b/tests/test_doctestplus.py @@ -454,6 +454,72 @@ def test_ignore_warnings_rst(testdir): reprec.assertoutcome(failed=0, passed=1) +def test_show_warnings_module(testdir): + + p = testdir.makepyfile( + """ + def myfunc(): + ''' + >>> import warnings + >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS + UserWarning: A warning occurred + ''' + pass + """) + reprec = testdir.inline_run(p, "--doctest-plus", "-W error") + reprec.assertoutcome(failed=0, passed=1) + + # Make sure it fails if warning message is missing + p = testdir.makepyfile( + """ + def myfunc(): + ''' + >>> import warnings + >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS + ''' + pass + """) + reprec = testdir.inline_run(p, "--doctest-plus", "-W error") + reprec.assertoutcome(failed=1, passed=0) + + +def test_show_warnings_rst(testdir): + + p = testdir.makefile(".rst", + """ + :: + >>> import warnings + >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS + UserWarning: A warning occurred + """) + reprec = testdir.inline_run(p, "--doctest-plus", "--doctest-rst", + "--text-file-format=rst", "-W error") + reprec.assertoutcome(failed=0, passed=1) + + # Make sure it fails if warning message is missing + p = testdir.makefile(".rst", + """ + :: + >>> import warnings + >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS + """) + reprec = testdir.inline_run(p, "--doctest-plus", "--doctest-rst", + "--text-file-format=rst", "-W error") + reprec.assertoutcome(failed=1, passed=0) + + # Make sure it fails if warning message is missing + p = testdir.makefile(".rst", + """ + :: + >>> import warnings + >>> warnings.warn('A warning occurred', UserWarning) # doctest: +SHOW_WARNINGS + Warning: Another warning occurred + """) + reprec = testdir.inline_run(p, "--doctest-plus", "--doctest-rst", + "--text-file-format=rst", "-W error") + reprec.assertoutcome(failed=1, passed=0) + + def test_doctest_glob(testdir): testdir.makefile( '.rst',