From afe9fd5ffd4486121d16ff2a434807822bc4b06c Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Thu, 24 Jan 2019 21:53:14 +0000 Subject: [PATCH 1/7] Adds `does_not_raise` context manager Addressing issues #4324 and #1830 --- AUTHORS | 1 + src/_pytest/python_api.py | 30 +++++++++++++++++++++++++++++ src/pytest.py | 2 ++ testing/python/raises.py | 40 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/AUTHORS b/AUTHORS index 5c8cd9c9e..a4a41c942 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,7 @@ Anthony Shaw Anthony Sottile Anton Lodder Antony Lee +Arel Cordero Armin Rigo Aron Coyle Aron Curzon diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9b31d4e68..8babe13bf 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -4,6 +4,7 @@ import math import pprint import sys import warnings +from contextlib import contextmanager from decimal import Decimal from numbers import Number @@ -726,3 +727,32 @@ class RaisesContext(object): if self.match_expr is not None and suppress_exception: self.excinfo.match(self.match_expr) return suppress_exception + + +# builtin pytest.does_not_raise helper + + +@contextmanager +def does_not_raise(): + r""" + This context manager is a complement to ``pytest.raises()`` that does + *not* catch any exceptions raised by the code block. + + + This is essentially a no-op but is useful when + conditionally parameterizing tests that may or may not + raise an error. For example:: + + @pytest.mark.parametrize('example_input,expectation', [ + (3, does_not_raise()), + (2, does_not_raise()), + (1, does_not_raise()), + (0, raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + '''Test how much I know division.''' + with expectation: + assert (6 / example_input) is not None + """ + + yield diff --git a/src/pytest.py b/src/pytest.py index c0010f166..16ce2ad70 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -32,6 +32,7 @@ from _pytest.python import Instance from _pytest.python import Module from _pytest.python import Package from _pytest.python_api import approx +from _pytest.python_api import does_not_raise from _pytest.python_api import raises from _pytest.recwarn import deprecated_call from _pytest.recwarn import warns @@ -50,6 +51,7 @@ __all__ = [ "cmdline", "Collector", "deprecated_call", + "does_not_raise", "exit", "fail", "File", diff --git a/testing/python/raises.py b/testing/python/raises.py index 4ff0b51bc..4ba9c1ccb 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -94,6 +94,46 @@ class TestRaises(object): result = testdir.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) + def test_does_not_raise(self, testdir): + testdir.makepyfile( + """ + import pytest + import _pytest._code + + @pytest.mark.parametrize('example_input,expectation', [ + (3, pytest.does_not_raise()), + (2, pytest.does_not_raise()), + (1, pytest.does_not_raise()), + (0, pytest.raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + '''Test how much I know division.''' + with expectation: + assert (6 / example_input) is not None + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*4 passed*"]) + + def test_does_not_raise_does_raise(self, testdir): + testdir.makepyfile( + """ + import pytest + import _pytest._code + + @pytest.mark.parametrize('example_input,expectation', [ + (0, pytest.does_not_raise()), + (1, pytest.raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + '''Test how much I know division.''' + with expectation: + assert (6 / example_input) is not None + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*2 failed*"]) + def test_noclass(self): with pytest.raises(TypeError): pytest.raises("wrong", lambda: None) From c166b80a8c8d4efe2ca4574f7334602cfd594c75 Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Thu, 24 Jan 2019 22:29:47 +0000 Subject: [PATCH 2/7] Documenting raises/does_not_raise + parametrize --- doc/en/example/parametrize.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 98aaeae3b..6c089fa41 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -559,3 +559,26 @@ As the result: - The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing. - The test ``test_eval[basic_2+4]`` passed. - The test ``test_eval[basic_6*9]`` was expected to fail and did fail. + +Parametrizing conditional raising with ``pytest.raises`` +-------------------------------------------------------------------- + +Use ``pytest.raises`` and ``pytest.does_not_raise`` together with the +``parametrize`` decorator to write parametrized tests in which some +tests raise exceptions and others do not. For example:: + + import pytest + + @pytest.mark.parametrize('example_input,expectation', [ + (3, pytest.does_not_raise()), + (2, pytest.does_not_raise()), + (1, pytest.does_not_raise()), + (0, pytest.raises(ZeroDivisionError)), + ]) + def test_division(example_input, expectation): + """Test how much I know division.""" + with expectation: + assert (6 / example_input) is not None + +In this example, the first three tests should run unexceptionally, +while the fourth test should raise ``ZeroDivisionError``. From c1fe07276cd746bf9143476d89e3bfe7a96043e2 Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Thu, 24 Jan 2019 22:57:39 +0000 Subject: [PATCH 3/7] Adding changelog entries for `does_not_raise` --- changelog/1830.feature.rst | 1 + changelog/4324.doc.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/1830.feature.rst create mode 100644 changelog/4324.doc.rst diff --git a/changelog/1830.feature.rst b/changelog/1830.feature.rst new file mode 100644 index 000000000..7a157abc3 --- /dev/null +++ b/changelog/1830.feature.rst @@ -0,0 +1 @@ +A context manager ``does_not_raise`` is added to complement ``raises`` in parametrized tests with conditional raises. diff --git a/changelog/4324.doc.rst b/changelog/4324.doc.rst new file mode 100644 index 000000000..5e37a91aa --- /dev/null +++ b/changelog/4324.doc.rst @@ -0,0 +1 @@ +Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises. From 977adf135434bea2bdcda4a06a1187adbfd40309 Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Fri, 25 Jan 2019 21:14:15 +0000 Subject: [PATCH 4/7] Improving sphinx docs based on feedback --- doc/en/example/parametrize.rst | 12 +++++++----- doc/en/reference.rst | 6 ++++++ src/_pytest/python_api.py | 25 +++++++++++++++++++------ testing/python/raises.py | 2 -- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 6c089fa41..50d615891 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -560,11 +560,13 @@ As the result: - The test ``test_eval[basic_2+4]`` passed. - The test ``test_eval[basic_6*9]`` was expected to fail and did fail. -Parametrizing conditional raising with ``pytest.raises`` +.. _`parametrizing_conditional_raising`: + +Parametrizing conditional raising -------------------------------------------------------------------- -Use ``pytest.raises`` and ``pytest.does_not_raise`` together with the -``parametrize`` decorator to write parametrized tests in which some +Use :func:`pytest.raises` and :func:`pytest.does_not_raise` together with the +:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some tests raise exceptions and others do not. For example:: import pytest @@ -580,5 +582,5 @@ tests raise exceptions and others do not. For example:: with expectation: assert (6 / example_input) is not None -In this example, the first three tests should run unexceptionally, -while the fourth test should raise ``ZeroDivisionError``. +In this example, the first three test cases should run unexceptionally, +while the fourth should raise ``ZeroDivisionError``. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 92e298a88..f2dedbd97 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -61,6 +61,12 @@ pytest.raises .. autofunction:: pytest.raises(expected_exception: Exception, [match], [message]) :with: excinfo +pytest.does_not_raise +~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: pytest.does_not_raise() + :with: excinfo + pytest.deprecated_call ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 8babe13bf..a3532a541 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -622,6 +622,14 @@ def raises(expected_exception, *args, **kwargs): ... >>> assert exc_info.type is ValueError + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` + it is possible to parametrize tests such that + some runs raise an exception and others do not. + + See :ref:`parametrizing_conditional_raising` for an example. + **Legacy form** It is possible to specify a callable by passing a to-be-called lambda:: @@ -734,13 +742,13 @@ class RaisesContext(object): @contextmanager def does_not_raise(): - r""" + r''' This context manager is a complement to ``pytest.raises()`` that does *not* catch any exceptions raised by the code block. - This is essentially a no-op but is useful when - conditionally parameterizing tests that may or may not + This is essentially a *no-op* but is useful when + conditionally parametrizing tests that may or may not raise an error. For example:: @pytest.mark.parametrize('example_input,expectation', [ @@ -750,9 +758,14 @@ def does_not_raise(): (0, raises(ZeroDivisionError)), ]) def test_division(example_input, expectation): - '''Test how much I know division.''' - with expectation: + """Test how much I know division.""" + with expectation as excinfo: assert (6 / example_input) is not None - """ + + Note that `excinfo` will be *None* when using + ``does_not_raise``. In the example above, `execinfo` + will be `None` for the first three runs and + an :class:`ExceptionInfo` instance on last run. + ''' yield diff --git a/testing/python/raises.py b/testing/python/raises.py index 4ba9c1ccb..8135c2c34 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -98,7 +98,6 @@ class TestRaises(object): testdir.makepyfile( """ import pytest - import _pytest._code @pytest.mark.parametrize('example_input,expectation', [ (3, pytest.does_not_raise()), @@ -119,7 +118,6 @@ class TestRaises(object): testdir.makepyfile( """ import pytest - import _pytest._code @pytest.mark.parametrize('example_input,expectation', [ (0, pytest.does_not_raise()), From fd4289dae0b5d7644219b17749c00f83cb7ee973 Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Sun, 27 Jan 2019 16:10:11 +0000 Subject: [PATCH 5/7] Adding `does_not_raise` to documentation only --- changelog/1830.feature.rst | 1 - doc/en/example/parametrize.rst | 21 ++++++++++++++------ doc/en/reference.rst | 6 ------ src/_pytest/python_api.py | 35 ---------------------------------- src/pytest.py | 2 -- testing/python/raises.py | 18 +++++++++++++---- 6 files changed, 29 insertions(+), 54 deletions(-) delete mode 100644 changelog/1830.feature.rst diff --git a/changelog/1830.feature.rst b/changelog/1830.feature.rst deleted file mode 100644 index 7a157abc3..000000000 --- a/changelog/1830.feature.rst +++ /dev/null @@ -1 +0,0 @@ -A context manager ``does_not_raise`` is added to complement ``raises`` in parametrized tests with conditional raises. diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 50d615891..b7be543ea 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -565,16 +565,25 @@ As the result: Parametrizing conditional raising -------------------------------------------------------------------- -Use :func:`pytest.raises` and :func:`pytest.does_not_raise` together with the -:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some -tests raise exceptions and others do not. For example:: +Use :func:`pytest.raises` with the +:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests +in which some tests raise exceptions and others do not. +It is helpful to define a function such as ``does_not_raise`` to serve +as a complement to ``raises``. For example:: + + from contextlib import contextmanager import pytest + @contextmanager + def does_not_raise(): + yield + + @pytest.mark.parametrize('example_input,expectation', [ - (3, pytest.does_not_raise()), - (2, pytest.does_not_raise()), - (1, pytest.does_not_raise()), + (3, does_not_raise()), + (2, does_not_raise()), + (1, does_not_raise()), (0, pytest.raises(ZeroDivisionError)), ]) def test_division(example_input, expectation): diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f2dedbd97..92e298a88 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -61,12 +61,6 @@ pytest.raises .. autofunction:: pytest.raises(expected_exception: Exception, [match], [message]) :with: excinfo -pytest.does_not_raise -~~~~~~~~~~~~~~~~~~~~~ - -.. autofunction:: pytest.does_not_raise() - :with: excinfo - pytest.deprecated_call ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index a3532a541..1b643d430 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -4,7 +4,6 @@ import math import pprint import sys import warnings -from contextlib import contextmanager from decimal import Decimal from numbers import Number @@ -735,37 +734,3 @@ class RaisesContext(object): if self.match_expr is not None and suppress_exception: self.excinfo.match(self.match_expr) return suppress_exception - - -# builtin pytest.does_not_raise helper - - -@contextmanager -def does_not_raise(): - r''' - This context manager is a complement to ``pytest.raises()`` that does - *not* catch any exceptions raised by the code block. - - - This is essentially a *no-op* but is useful when - conditionally parametrizing tests that may or may not - raise an error. For example:: - - @pytest.mark.parametrize('example_input,expectation', [ - (3, does_not_raise()), - (2, does_not_raise()), - (1, does_not_raise()), - (0, raises(ZeroDivisionError)), - ]) - def test_division(example_input, expectation): - """Test how much I know division.""" - with expectation as excinfo: - assert (6 / example_input) is not None - - Note that `excinfo` will be *None* when using - ``does_not_raise``. In the example above, `execinfo` - will be `None` for the first three runs and - an :class:`ExceptionInfo` instance on last run. - ''' - - yield diff --git a/src/pytest.py b/src/pytest.py index 16ce2ad70..c0010f166 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -32,7 +32,6 @@ from _pytest.python import Instance from _pytest.python import Module from _pytest.python import Package from _pytest.python_api import approx -from _pytest.python_api import does_not_raise from _pytest.python_api import raises from _pytest.recwarn import deprecated_call from _pytest.recwarn import warns @@ -51,7 +50,6 @@ __all__ = [ "cmdline", "Collector", "deprecated_call", - "does_not_raise", "exit", "fail", "File", diff --git a/testing/python/raises.py b/testing/python/raises.py index 8135c2c34..f5827e9b0 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -97,12 +97,17 @@ class TestRaises(object): def test_does_not_raise(self, testdir): testdir.makepyfile( """ + from contextlib import contextmanager import pytest + @contextmanager + def does_not_raise(): + yield + @pytest.mark.parametrize('example_input,expectation', [ - (3, pytest.does_not_raise()), - (2, pytest.does_not_raise()), - (1, pytest.does_not_raise()), + (3, does_not_raise()), + (2, does_not_raise()), + (1, does_not_raise()), (0, pytest.raises(ZeroDivisionError)), ]) def test_division(example_input, expectation): @@ -117,10 +122,15 @@ class TestRaises(object): def test_does_not_raise_does_raise(self, testdir): testdir.makepyfile( """ + from contextlib import contextmanager import pytest + @contextmanager + def does_not_raise(): + yield + @pytest.mark.parametrize('example_input,expectation', [ - (0, pytest.does_not_raise()), + (0, does_not_raise()), (1, pytest.raises(ZeroDivisionError)), ]) def test_division(example_input, expectation): From 8a1afe421357439dd15d9a16448ecbdd69fdab85 Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Mon, 28 Jan 2019 13:31:08 +0000 Subject: [PATCH 6/7] Including note on using nullcontext in Python 3.7+ --- doc/en/example/parametrize.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index b7be543ea..0a16621d3 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -569,7 +569,7 @@ Use :func:`pytest.raises` with the :ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some tests raise exceptions and others do not. -It is helpful to define a function such as ``does_not_raise`` to serve +It is helpful to define a context manager ``does_not_raise`` to serve as a complement to ``raises``. For example:: from contextlib import contextmanager @@ -591,5 +591,10 @@ as a complement to ``raises``. For example:: with expectation: assert (6 / example_input) is not None -In this example, the first three test cases should run unexceptionally, +In the example above, the first three test cases should run unexceptionally, while the fourth should raise ``ZeroDivisionError``. + +In Python 3.7+, you can simply use ``nullcontext`` to define +``does_not_raise``:: + + from contextlib import nullcontext as does_not_raise From 7ec1a1407aeb52b40514b657d63e5dd926b8eb8b Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Sat, 2 Feb 2019 01:57:17 +0000 Subject: [PATCH 7/7] Incorporating feedback from asottile --- doc/en/example/parametrize.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 0a16621d3..5c26e1b70 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -569,7 +569,7 @@ Use :func:`pytest.raises` with the :ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some tests raise exceptions and others do not. -It is helpful to define a context manager ``does_not_raise`` to serve +It is helpful to define a no-op context manager ``does_not_raise`` to serve as a complement to ``raises``. For example:: from contextlib import contextmanager @@ -594,7 +594,15 @@ as a complement to ``raises``. For example:: In the example above, the first three test cases should run unexceptionally, while the fourth should raise ``ZeroDivisionError``. -In Python 3.7+, you can simply use ``nullcontext`` to define -``does_not_raise``:: +If you're only supporting Python 3.7+, you can simply use ``nullcontext`` +to define ``does_not_raise``:: from contextlib import nullcontext as does_not_raise + +Or, if you're supporting Python 3.3+ you can use:: + + from contextlib import ExitStack as does_not_raise + +Or, if desired, you can ``pip install contextlib2`` and use:: + + from contextlib2 import ExitStack as does_not_raise