From afe9fd5ffd4486121d16ff2a434807822bc4b06c Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Thu, 24 Jan 2019 21:53:14 +0000 Subject: [PATCH 01/25] 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 02/25] 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 03/25] 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 04/25] 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 05/25] 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 06/25] 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 c3d734054241f69220070ce6d4cb00db4acc50c6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 31 Jan 2019 20:24:11 -0200 Subject: [PATCH 07/25] Fix setUpClass being called in subclasses that were skipped Fix #4700 --- changelog/4700.bugfix.rst | 2 ++ src/_pytest/unittest.py | 3 +++ .../example_scripts/unittest/test_setup_skip.py | 13 +++++++++++++ .../unittest/test_setup_skip_class.py | 14 ++++++++++++++ .../unittest/test_setup_skip_module.py | 12 ++++++++++++ testing/test_unittest.py | 15 +++++++++++++++ 6 files changed, 59 insertions(+) create mode 100644 changelog/4700.bugfix.rst create mode 100644 testing/example_scripts/unittest/test_setup_skip.py create mode 100644 testing/example_scripts/unittest/test_setup_skip_class.py create mode 100644 testing/example_scripts/unittest/test_setup_skip_module.py diff --git a/changelog/4700.bugfix.rst b/changelog/4700.bugfix.rst new file mode 100644 index 000000000..3f8acb876 --- /dev/null +++ b/changelog/4700.bugfix.rst @@ -0,0 +1,2 @@ +Fix regression where ``setUpClass`` would always be called in subclasses even if all tests +were skipped by a ``unittest.skip()`` decorator applied in the subclass. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index e00636d46..58d79845b 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -87,6 +87,9 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self): @pytest.fixture(scope=scope, autouse=True) def fixture(self, request): + if getattr(self, "__unittest_skip__", None): + reason = self.__unittest_skip_why__ + pytest.skip(reason) if setup is not None: if pass_self: setup(self, request.function) diff --git a/testing/example_scripts/unittest/test_setup_skip.py b/testing/example_scripts/unittest/test_setup_skip.py new file mode 100644 index 000000000..93f79bb3b --- /dev/null +++ b/testing/example_scripts/unittest/test_setup_skip.py @@ -0,0 +1,13 @@ +"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class.""" +import unittest + + +class Base(unittest.TestCase): + def setUp(self): + assert 0 + + +@unittest.skip("skip all tests") +class Test(Base): + def test_foo(self): + assert 0 diff --git a/testing/example_scripts/unittest/test_setup_skip_class.py b/testing/example_scripts/unittest/test_setup_skip_class.py new file mode 100644 index 000000000..4f251dcba --- /dev/null +++ b/testing/example_scripts/unittest/test_setup_skip_class.py @@ -0,0 +1,14 @@ +"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class.""" +import unittest + + +class Base(unittest.TestCase): + @classmethod + def setUpClass(cls): + assert 0 + + +@unittest.skip("skip all tests") +class Test(Base): + def test_foo(self): + assert 0 diff --git a/testing/example_scripts/unittest/test_setup_skip_module.py b/testing/example_scripts/unittest/test_setup_skip_module.py new file mode 100644 index 000000000..98befbe51 --- /dev/null +++ b/testing/example_scripts/unittest/test_setup_skip_module.py @@ -0,0 +1,12 @@ +"""setUpModule is always called, even if all tests in the module are skipped""" +import unittest + + +def setUpModule(): + assert 0 + + +@unittest.skip("skip all tests") +class Base(unittest.TestCase): + def test(self): + assert 0 diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 2c60cd271..fe33855fa 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1026,3 +1026,18 @@ def test_error_message_with_parametrized_fixtures(testdir): "*Function type: TestCaseFunction", ] ) + + +@pytest.mark.parametrize( + "test_name, expected_outcome", + [ + ("test_setup_skip.py", "1 skipped"), + ("test_setup_skip_class.py", "1 skipped"), + ("test_setup_skip_module.py", "1 error"), + ], +) +def test_setup_inheritance_skipping(testdir, test_name, expected_outcome): + """Issue #4700""" + testdir.copy_example("unittest/{}".format(test_name)) + result = testdir.runpytest() + result.stdout.fnmatch_lines("* {} in *".format(expected_outcome)) From c2c9b27771f2c859d71d864384b96373ddda9e96 Mon Sep 17 00:00:00 2001 From: Raphael Pierzina Date: Fri, 1 Feb 2019 13:15:18 +0100 Subject: [PATCH 08/25] Update changelog to reflect spelling change of xfail in teststatus report Resolve #4705 --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76c947473..9afd301bb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -96,6 +96,9 @@ Trivial/Internal Changes - `#4657 `_: Copy saferepr from pylib +- `#4668 `_: The verbose word for expected failures in the teststatus report changes from ``xfail`` to ``XFAIL`` to be consistent with other test outcomes. + + pytest 4.1.1 (2019-01-12) ========================= From 7ec1a1407aeb52b40514b657d63e5dd926b8eb8b Mon Sep 17 00:00:00 2001 From: Arel Cordero Date: Sat, 2 Feb 2019 01:57:17 +0000 Subject: [PATCH 09/25] 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 From f0ecb25acd2faefa31c460c43b580b071393bdf9 Mon Sep 17 00:00:00 2001 From: Nick Murphy Date: Fri, 1 Feb 2019 21:48:29 -0500 Subject: [PATCH 10/25] Document custom failure messages for missing warnings --- doc/en/warnings.rst | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 8f0244aea..cfa966488 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -233,7 +233,7 @@ You can also use it as a contextmanager:: .. _warns: Asserting warnings with the warns function ------------------------------------------------ +------------------------------------------ .. versionadded:: 2.8 @@ -291,7 +291,7 @@ Alternatively, you can examine raised warnings in detail using the .. _recwarn: Recording warnings ------------------------- +------------------ You can record raised warnings either using ``pytest.warns`` or with the ``recwarn`` fixture. @@ -329,6 +329,26 @@ warnings, or index into it to get a particular recorded warning. Full API: :class:`WarningsRecorder`. +.. _custom_failure_messages: + +Custom failure messages +----------------------- + +Recording warnings provides an opportunity to produce custom test +failure messages for when no warnings are issued or other conditions +are met. + +.. code-block:: python + + def test(): + with pytest.warns(Warning) as record: + f() + if not record: + pytest.fail('Expected a warning!') + +If no warnings are issued when calling ``f``, then ``not record`` will +evaluate to ``False``. You can then call ``pytest.fail`` with a +custom error message. .. _internal-warnings: From 8003d8d2798cd23cf7b295f5ec7b5a8689893783 Mon Sep 17 00:00:00 2001 From: Nick Murphy Date: Fri, 1 Feb 2019 21:55:01 -0500 Subject: [PATCH 11/25] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 35e07dcb4..e8b0166ff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -173,6 +173,7 @@ Nathaniel Waisbrot Ned Batchelder Neven Mundar Nicholas Devenish +Nicholas Murphy Niclas Olofsson Nicolas Delaby Oleg Pidsadnyi From 4e93dc2c97382bf897df3e2d5e8e002cc48a15b3 Mon Sep 17 00:00:00 2001 From: Nick Murphy Date: Fri, 1 Feb 2019 22:11:41 -0500 Subject: [PATCH 12/25] Update changelog for pytest.warns doc update --- changelog/4709.doc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/4709.doc.rst diff --git a/changelog/4709.doc.rst b/changelog/4709.doc.rst new file mode 100644 index 000000000..5f21728f6 --- /dev/null +++ b/changelog/4709.doc.rst @@ -0,0 +1,2 @@ +Document how to customize test failure messages when using +``pytest.warns``. From 726e1659320d2c5872cd6185008625dcdc425e12 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 4 Feb 2019 08:38:48 -0200 Subject: [PATCH 13/25] Fix typo in CHANGELOG --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9afd301bb..ac168dfd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,7 +24,7 @@ pytest 4.2.0 (2019-01-30) Features -------- -- `#3094 `_: `Class xunit-style `__ functions and methods +- `#3094 `_: `Classic xunit-style `__ functions and methods now obey the scope of *autouse* fixtures. This fixes a number of surprising issues like ``setup_method`` being called before session-scoped From 315374008b5868f635881d2de5c53dbe1369f193 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Feb 2019 12:48:18 -0800 Subject: [PATCH 14/25] Remove workaround for docstrings for py38+ --- testing/test_assertrewrite.py | 49 ++++++++++------------------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 840fda2ca..a852277cc 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -68,38 +68,16 @@ def getmsg(f, extra_ns=None, must_pass=False): pytest.fail("function didn't raise at all") -def adjust_body_for_new_docstring_in_module_node(m): - """Module docstrings in 3.8 are part of Module node. - This was briefly in 3.7 as well but got reverted in beta 5. - - It's not in the body so we remove it so the following body items have - the same indexes on all Python versions: - - TODO: - - We have a complicated sys.version_info if in here to ease testing on - various Python 3.7 versions, but we should remove the 3.7 check after - 3.7 is released as stable to make this check more straightforward. - """ - if sys.version_info < (3, 8) and not ( - (3, 7) <= sys.version_info <= (3, 7, 0, "beta", 4) - ): - assert len(m.body) > 1 - assert isinstance(m.body[0], ast.Expr) - assert isinstance(m.body[0].value, ast.Str) - del m.body[0] - - class TestAssertionRewrite(object): def test_place_initial_imports(self): s = """'Doc string'\nother = stuff""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - for imp in m.body[0:2]: + assert isinstance(m.body[0], ast.Expr) + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[2], ast.Assign) + assert isinstance(m.body[3], ast.Assign) s = """from __future__ import division\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) @@ -110,24 +88,24 @@ class TestAssertionRewrite(object): assert isinstance(m.body[3], ast.Expr) s = """'doc string'\nfrom __future__ import division""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:3]: + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[1], ast.ImportFrom) + for imp in m.body[2:4]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 s = """'doc string'\nfrom __future__ import division\nother""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - assert isinstance(m.body[0], ast.ImportFrom) - for imp in m.body[1:3]: + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[1], ast.ImportFrom) + for imp in m.body[2:4]: assert isinstance(imp, ast.Import) assert imp.lineno == 3 assert imp.col_offset == 0 - assert isinstance(m.body[3], ast.Expr) + assert isinstance(m.body[4], ast.Expr) s = """from . import relative\nother_stuff""" m = rewrite(s) - for imp in m.body[0:2]: + for imp in m.body[:2]: assert isinstance(imp, ast.Import) assert imp.lineno == 1 assert imp.col_offset == 0 @@ -136,9 +114,8 @@ class TestAssertionRewrite(object): def test_dont_rewrite(self): s = """'PYTEST_DONT_REWRITE'\nassert 14""" m = rewrite(s) - adjust_body_for_new_docstring_in_module_node(m) - assert len(m.body) == 1 - assert m.body[0].msg is None + assert len(m.body) == 2 + assert m.body[1].msg is None def test_dont_rewrite_plugin(self, testdir): contents = { From 584c052da41c83692f437b75ae3eacb8faba8b05 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Feb 2019 19:04:26 -0200 Subject: [PATCH 15/25] Fix linting and change False to True as requested in review --- doc/en/warnings.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index cfa966488..11f73f43e 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -344,10 +344,10 @@ are met. with pytest.warns(Warning) as record: f() if not record: - pytest.fail('Expected a warning!') + pytest.fail("Expected a warning!") If no warnings are issued when calling ``f``, then ``not record`` will -evaluate to ``False``. You can then call ``pytest.fail`` with a +evaluate to ``True``. You can then call ``pytest.fail`` with a custom error message. .. _internal-warnings: From 0ce8b910cae3e6321cb2f8404c3be162f7ed2f12 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Mon, 4 Feb 2019 20:19:07 +0100 Subject: [PATCH 16/25] Only call _setup_cli_logging in __init__ Supersedes #4719 --- src/_pytest/logging.py | 98 ++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 0c47b8b51..c9be85350 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -370,6 +370,8 @@ def get_actual_log_level(config, *setting_names): ) +# run after terminalreporter/capturemanager are configured +@pytest.hookimpl(trylast=True) def pytest_configure(config): config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") @@ -388,8 +390,6 @@ class LoggingPlugin(object): # enable verbose output automatically if live logging is enabled if self._log_cli_enabled() and not config.getoption("verbose"): - # sanity check: terminal reporter should not have been loaded at this point - assert self._config.pluginmanager.get_plugin("terminalreporter") is None config.option.verbose = 1 self.print_logs = get_option_ini(config, "log_print") @@ -420,6 +420,55 @@ class LoggingPlugin(object): self.log_cli_handler = None + self._setup_cli_logging() + + def _setup_cli_logging(self): + terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") + + if self._log_cli_enabled() and terminal_reporter is not None: + # FIXME don't set verbosity level and derived attributes of + # terminalwriter directly + terminal_reporter.verbosity = self._config.option.verbose + terminal_reporter.showheader = terminal_reporter.verbosity >= 0 + terminal_reporter.showfspath = terminal_reporter.verbosity >= 0 + terminal_reporter.showlongtestinfo = terminal_reporter.verbosity > 0 + + capture_manager = self._config.pluginmanager.get_plugin("capturemanager") + log_cli_handler = _LiveLoggingStreamHandler( + terminal_reporter, capture_manager + ) + log_cli_format = get_option_ini( + self._config, "log_cli_format", "log_format" + ) + log_cli_date_format = get_option_ini( + self._config, "log_cli_date_format", "log_date_format" + ) + if ( + self._config.option.color != "no" + and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) + ): + log_cli_formatter = ColoredLevelFormatter( + create_terminal_writer(self._config), + log_cli_format, + datefmt=log_cli_date_format, + ) + else: + log_cli_formatter = logging.Formatter( + log_cli_format, datefmt=log_cli_date_format + ) + log_cli_level = get_actual_log_level( + self._config, "log_cli_level", "log_level" + ) + self.log_cli_handler = log_cli_handler + self.live_logs_context = lambda: catching_logs( + log_cli_handler, formatter=log_cli_formatter, level=log_cli_level + ) + else: + self.live_logs_context = lambda: dummy_context_manager() + # Note that the lambda for the live_logs_context is needed because + # live_logs_context can otherwise not be entered multiple times due + # to limitations of contextlib.contextmanager + def _log_cli_enabled(self): """Return True if log_cli should be considered enabled, either explicitly or because --log-cli-level was given in the command-line. @@ -430,10 +479,6 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self): - # This has to be called before the first log message is logged, - # so we can access the terminal reporter plugin. - self._setup_cli_logging() - with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("collection") @@ -513,7 +558,6 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self): - self._setup_cli_logging() with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("sessionstart") @@ -533,46 +577,6 @@ class LoggingPlugin(object): else: yield # run all the tests - def _setup_cli_logging(self): - """Sets up the handler and logger for the Live Logs feature, if enabled.""" - terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") - if self._log_cli_enabled() and terminal_reporter is not None: - capture_manager = self._config.pluginmanager.get_plugin("capturemanager") - log_cli_handler = _LiveLoggingStreamHandler( - terminal_reporter, capture_manager - ) - log_cli_format = get_option_ini( - self._config, "log_cli_format", "log_format" - ) - log_cli_date_format = get_option_ini( - self._config, "log_cli_date_format", "log_date_format" - ) - if ( - self._config.option.color != "no" - and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) - ): - log_cli_formatter = ColoredLevelFormatter( - create_terminal_writer(self._config), - log_cli_format, - datefmt=log_cli_date_format, - ) - else: - log_cli_formatter = logging.Formatter( - log_cli_format, datefmt=log_cli_date_format - ) - log_cli_level = get_actual_log_level( - self._config, "log_cli_level", "log_level" - ) - self.log_cli_handler = log_cli_handler - self.live_logs_context = lambda: catching_logs( - log_cli_handler, formatter=log_cli_formatter, level=log_cli_level - ) - else: - self.live_logs_context = lambda: dummy_context_manager() - # Note that the lambda for the live_logs_context is needed because - # live_logs_context can otherwise not be entered multiple times due - # to limitations of contextlib.contextmanager - class _LiveLoggingStreamHandler(logging.StreamHandler): """ From 19c93d16d1190106ba45d5fded5af74430e7a2e7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Feb 2019 00:02:39 -0200 Subject: [PATCH 17/25] Do not raise UsageError when "pytest_plugins" is a module Fix #3899 --- changelog/3899.bugfix.rst | 1 + src/_pytest/config/__init__.py | 4 ++-- testing/acceptance_test.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 changelog/3899.bugfix.rst diff --git a/changelog/3899.bugfix.rst b/changelog/3899.bugfix.rst new file mode 100644 index 000000000..8f117779e --- /dev/null +++ b/changelog/3899.bugfix.rst @@ -0,0 +1 @@ +Do not raise ``UsageError`` when an imported package has a ``pytest_plugins.py`` child module. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 23e09ff40..26999e125 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -559,8 +559,8 @@ def _get_plugin_specs_as_list(specs): which case it is returned as a list. Specs can also be `None` in which case an empty list is returned. """ - if specs is not None: - if isinstance(specs, str): + if specs is not None and not isinstance(specs, types.ModuleType): + if isinstance(specs, six.string_types): specs = specs.split(",") if specs else [] if not isinstance(specs, (list, tuple)): raise UsageError( diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c8a391b78..95c419599 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -969,6 +969,20 @@ def test_import_plugin_unicode_name(testdir): assert r.ret == 0 +def test_pytest_plugins_as_module(testdir): + """Do not raise an error if pytest_plugins attribute is a module (#3899)""" + testdir.makepyfile( + **{ + "__init__.py": "", + "pytest_plugins.py": "", + "conftest.py": "from . import pytest_plugins", + "test_foo.py": "def test(): pass", + } + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines("* 1 passed in *") + + def test_deferred_hook_checking(testdir): """ Check hooks as late as possible (#1821). From 54af0f4c65f3d63210e661193f3cef2f727f246d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Feb 2019 21:29:30 -0200 Subject: [PATCH 18/25] Call pytest_report_collectionfinish hook when --collect-only is passed Fix #2895 --- changelog/2895.bugfix.rst | 1 + src/_pytest/terminal.py | 15 ++++++++------- testing/test_terminal.py | 7 +++++-- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 changelog/2895.bugfix.rst diff --git a/changelog/2895.bugfix.rst b/changelog/2895.bugfix.rst new file mode 100644 index 000000000..8e01e193c --- /dev/null +++ b/changelog/2895.bugfix.rst @@ -0,0 +1 @@ +The ``pytest_report_collectionfinish`` hook now is also called with ``--collect-only``. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 03b0761f2..eb35577f1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -574,19 +574,20 @@ class TerminalReporter(object): return lines def pytest_collection_finish(self, session): - if self.config.option.collectonly: + if self.config.getoption("collectonly"): self._printcollecteditems(session.items) - if self.stats.get("failed"): - self._tw.sep("!", "collection failures") - for rep in self.stats.get("failed"): - rep.toterminal(self._tw) - return 1 - return 0 + lines = self.config.hook.pytest_report_collectionfinish( config=self.config, startdir=self.startdir, items=session.items ) self._write_report_lines_from_hooks(lines) + if self.config.getoption("collectonly"): + if self.stats.get("failed"): + self._tw.sep("!", "collection failures") + for rep in self.stats.get("failed"): + rep.toterminal(self._tw) + def _printcollecteditems(self, items): # to print out items and their parent collectors # we take care to leave out Instances aka () diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 71e49fb42..798e8c16a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -649,7 +649,10 @@ class TestTerminalFunctional(object): assert "===" not in s assert "passed" not in s - def test_report_collectionfinish_hook(self, testdir): + @pytest.mark.parametrize( + "params", [(), ("--collect-only",)], ids=["no-params", "collect-only"] + ) + def test_report_collectionfinish_hook(self, testdir, params): testdir.makeconftest( """ def pytest_report_collectionfinish(config, startdir, items): @@ -664,7 +667,7 @@ class TestTerminalFunctional(object): pass """ ) - result = testdir.runpytest() + result = testdir.runpytest(*params) result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"]) From 0c5e717f43151852efb08ac72b0f0691443ab352 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Feb 2019 13:11:00 -0200 Subject: [PATCH 19/25] Add py38-dev job to Travis --- .travis.yml | 7 +++++++ tox.ini | 1 + 2 files changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1c055e663..6489a1647 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,11 @@ env: - TOXENV=py37-pluggymaster PYTEST_NO_COVERAGE=1 - TOXENV=py37-freeze PYTEST_NO_COVERAGE=1 +matrix: + allow_failures: + - python: '3.8-dev' + env: TOXENV=py38 + jobs: include: # Coverage tracking is slow with pypy, skip it. @@ -35,6 +40,8 @@ jobs: python: '3.5' - env: TOXENV=py36 python: '3.6' + - env: TOXENV=py38 + python: '3.8-dev' - env: TOXENV=py37 - &test-macos language: generic diff --git a/tox.ini b/tox.ini index 7cb430223..f67d06f18 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py35 py36 py37 + py38 pypy {py27,py37}-{pexpect,xdist,trial,numpy,pluggymaster} py27-nobyte From 7445b5345f22c2cf0072326f89f09a1e8bfec54f Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Wed, 6 Feb 2019 21:51:05 +0100 Subject: [PATCH 20/25] Mention that `pytest_plugins` should not be used as module name --- changelog/3899.doc.rst | 1 + doc/en/plugins.rst | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelog/3899.doc.rst diff --git a/changelog/3899.doc.rst b/changelog/3899.doc.rst new file mode 100644 index 000000000..675684a01 --- /dev/null +++ b/changelog/3899.doc.rst @@ -0,0 +1 @@ +Add note to ``plugins.rst`` that ``pytest_plugins`` should not be used as a name for a user module containing plugins. diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index 3d1226d34..c4961d0a4 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -86,6 +86,11 @@ which will import the specified module as a ``pytest`` plugin. :ref:`full explanation ` in the Writing plugins section. +.. note:: + The name ``pytest_plugins`` is reserved and should not be used as a + name for a custom plugin module. + + .. _`findpluginname`: Finding out which plugins are active From 7b8fd0cc12cac29430c49fe4ca4b8aa12ed855ef Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 6 Feb 2019 21:22:04 +0100 Subject: [PATCH 21/25] Refactor _setup_cli_logging code Change the indentation in _setup_cli_logging by moving the self._log_cli_enabled check outside of the _setup_cli_logging method. --- src/_pytest/logging.py | 91 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index c9be85350..ba0acd269 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -420,54 +420,53 @@ class LoggingPlugin(object): self.log_cli_handler = None - self._setup_cli_logging() - - def _setup_cli_logging(self): - terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") - - if self._log_cli_enabled() and terminal_reporter is not None: - # FIXME don't set verbosity level and derived attributes of - # terminalwriter directly - terminal_reporter.verbosity = self._config.option.verbose - terminal_reporter.showheader = terminal_reporter.verbosity >= 0 - terminal_reporter.showfspath = terminal_reporter.verbosity >= 0 - terminal_reporter.showlongtestinfo = terminal_reporter.verbosity > 0 - - capture_manager = self._config.pluginmanager.get_plugin("capturemanager") - log_cli_handler = _LiveLoggingStreamHandler( - terminal_reporter, capture_manager - ) - log_cli_format = get_option_ini( - self._config, "log_cli_format", "log_format" - ) - log_cli_date_format = get_option_ini( - self._config, "log_cli_date_format", "log_date_format" - ) - if ( - self._config.option.color != "no" - and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) - ): - log_cli_formatter = ColoredLevelFormatter( - create_terminal_writer(self._config), - log_cli_format, - datefmt=log_cli_date_format, - ) - else: - log_cli_formatter = logging.Formatter( - log_cli_format, datefmt=log_cli_date_format - ) - log_cli_level = get_actual_log_level( - self._config, "log_cli_level", "log_level" - ) - self.log_cli_handler = log_cli_handler - self.live_logs_context = lambda: catching_logs( - log_cli_handler, formatter=log_cli_formatter, level=log_cli_level - ) - else: - self.live_logs_context = lambda: dummy_context_manager() + self.live_logs_context = lambda: dummy_context_manager() # Note that the lambda for the live_logs_context is needed because # live_logs_context can otherwise not be entered multiple times due - # to limitations of contextlib.contextmanager + # to limitations of contextlib.contextmanager. + + if self._log_cli_enabled(): + self._setup_cli_logging() + + def _setup_cli_logging(self): + config = self._config + terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + if terminal_reporter is None: + # terminal reporter is disabled e.g. by pytest-xdist. + return + + # FIXME don't set verbosity level and derived attributes of + # terminalwriter directly + terminal_reporter.verbosity = config.option.verbose + terminal_reporter.showheader = terminal_reporter.verbosity >= 0 + terminal_reporter.showfspath = terminal_reporter.verbosity >= 0 + terminal_reporter.showlongtestinfo = terminal_reporter.verbosity > 0 + + capture_manager = config.pluginmanager.get_plugin("capturemanager") + # if capturemanager plugin is disabled, live logging still works. + log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) + log_cli_format = get_option_ini(config, "log_cli_format", "log_format") + log_cli_date_format = get_option_ini( + config, "log_cli_date_format", "log_date_format" + ) + if ( + config.option.color != "no" + and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) + ): + log_cli_formatter = ColoredLevelFormatter( + create_terminal_writer(config), + log_cli_format, + datefmt=log_cli_date_format, + ) + else: + log_cli_formatter = logging.Formatter( + log_cli_format, datefmt=log_cli_date_format + ) + log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level") + self.log_cli_handler = log_cli_handler + self.live_logs_context = lambda: catching_logs( + log_cli_handler, formatter=log_cli_formatter, level=log_cli_level + ) def _log_cli_enabled(self): """Return True if log_cli should be considered enabled, either explicitly From 4c7ddb8d9b951a730f840e3d426de2e25d01a55e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 Feb 2019 11:07:20 -0800 Subject: [PATCH 22/25] Fix `parametrize(... ids=)` when the function returns non-strings. --- changelog/4739.bugfix.rst | 1 + src/_pytest/python.py | 10 ++++------ testing/python/metafunc.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 changelog/4739.bugfix.rst diff --git a/changelog/4739.bugfix.rst b/changelog/4739.bugfix.rst new file mode 100644 index 000000000..dcd44d3fa --- /dev/null +++ b/changelog/4739.bugfix.rst @@ -0,0 +1 @@ +Fix ``parametrize(... ids=)`` when the function returns non-strings. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 85373f47c..aaa49f7dd 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1144,9 +1144,10 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): def _idval(val, argname, idx, idfn, item, config): if idfn: - s = None try: - s = idfn(val) + generated_id = idfn(val) + if generated_id is not None: + val = generated_id except Exception as e: # See issue https://github.com/pytest-dev/pytest/issues/2169 msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" @@ -1154,10 +1155,7 @@ def _idval(val, argname, idx, idfn, item, config): # we only append the exception type and message because on Python 2 reraise does nothing msg += " {}: {}\n".format(type(e).__name__, e) six.raise_from(ValueError(msg), e) - if s: - return ascii_escaped(s) - - if config: + elif config: hook_id = config.hook.pytest_make_parametrize_id( config=config, val=val, argname=argname ) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5c352efd1..fa22966d8 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -418,6 +418,21 @@ class TestMetafunc(object): ] ) + def test_parametrize_ids_returns_non_string(self, testdir): + testdir.makepyfile( + """\ + import pytest + + def ids(d): + return d + + @pytest.mark.parametrize("arg", ({1: 2}, {3, 4}), ids=ids) + def test(arg): + assert arg + """ + ) + assert testdir.runpytest().ret == 0 + def test_idmaker_with_ids(self): from _pytest.python import idmaker From 913a2da6e52b6f2395b19c3163200726070b449d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 8 Feb 2019 18:03:45 +0100 Subject: [PATCH 23/25] Fix handling of collect_ignore from parent conftest `_collectfile` should be called on files only. Fixes https://github.com/pytest-dev/pytest/issues/4592. --- changelog/4592.bugfix.rst | 1 + src/_pytest/main.py | 1 + src/_pytest/python.py | 12 +++++++----- testing/test_collection.py | 13 +++++++++++++ 4 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 changelog/4592.bugfix.rst diff --git a/changelog/4592.bugfix.rst b/changelog/4592.bugfix.rst new file mode 100644 index 000000000..f1eaae7eb --- /dev/null +++ b/changelog/4592.bugfix.rst @@ -0,0 +1 @@ +Fix handling of ``collect_ignore`` via parent ``conftest.py``. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d0d826bb6..68d5bac40 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -607,6 +607,7 @@ class Session(nodes.FSCollector): yield y def _collectfile(self, path, handle_dupes=True): + assert path.isfile() ihook = self.gethookproxy(path) if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index aaa49f7dd..48962d137 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -599,6 +599,7 @@ class Package(Module): return proxy def _collectfile(self, path, handle_dupes=True): + assert path.isfile() ihook = self.gethookproxy(path) if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): @@ -642,11 +643,12 @@ class Package(Module): ): continue - if path.isdir() and path.join("__init__.py").check(file=1): - pkg_prefixes.add(path) - - for x in self._collectfile(path): - yield x + if path.isdir(): + if path.join("__init__.py").check(file=1): + pkg_prefixes.add(path) + else: + for x in self._collectfile(path): + yield x def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): diff --git a/testing/test_collection.py b/testing/test_collection.py index 36e8a69ce..329182b0f 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1144,3 +1144,16 @@ def test_collect_symlink_out_of_tree(testdir): ] ) assert result.ret == 0 + + +def test_collectignore_via_conftest(testdir, monkeypatch): + """collect_ignore in parent conftest skips importing child (issue #4592).""" + tests = testdir.mkpydir("tests") + tests.ensure("conftest.py").write("collect_ignore = ['ignore_me']") + + ignore_me = tests.mkdir("ignore_me") + ignore_me.ensure("__init__.py") + ignore_me.ensure("conftest.py").write("assert 0, 'should_not_be_called'") + + result = testdir.runpytest() + assert result.ret == EXIT_NOTESTSCOLLECTED From 9be069f89969bc9287d83d8782bfed0353b2c3fd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 Feb 2019 15:50:33 -0200 Subject: [PATCH 24/25] Use isolated_build option in tox.ini As per the excellent article by gaborbernat: https://www.bernat.tech/pep-517-518/ --- pyproject.toml | 1 + tox.ini | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d17b936c1..2a4cd65c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ requires = [ "setuptools-scm", "wheel", ] +build-backend = "setuptools.build_meta" [tool.towncrier] package = "pytest" diff --git a/tox.ini b/tox.ini index f67d06f18..347faf15c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -minversion = 2.0 +isolated_build = True +minversion = 3.3 distshare = {homedir}/.tox/distshare # make sure to update environment list in travis.yml and appveyor.yml envlist = From e191a65ebb493c65cce4d3ef9eb4d52908bf9a35 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 8 Feb 2019 20:51:39 +0100 Subject: [PATCH 25/25] tox: py37-freeze: use --no-use-pep517 for PyInstaller Fixes https://github.com/pytest-dev/pytest/issues/4750. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index f67d06f18..75b3a8ed7 100644 --- a/tox.ini +++ b/tox.ini @@ -166,7 +166,9 @@ commands = [testenv:py37-freeze] changedir = testing/freeze +# Disable PEP 517 with pip, which does not work with PyInstaller currently. deps = + --no-use-pep517 pyinstaller commands = {envpython} create_executable.py