From dff7b203f7ac83192611c361ebf89e861ba94e70 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Jun 2018 18:46:51 +0200 Subject: [PATCH 01/41] tox: clean up docs target --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index adc4e974661..03fa0af8bff 100644 --- a/tox.ini +++ b/tox.ini @@ -115,8 +115,6 @@ skipsdist = True usedevelop = True changedir = doc/en deps = - attrs - more-itertools PyYAML sphinx sphinxcontrib-trio From dcafb8c48ca42afd940db76ee48b72982f646bc0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 2 Aug 2018 15:18:36 -0300 Subject: [PATCH 02/41] Add example for package recursion bug --- .../collect/package_infinite_recursion/conftest.py | 2 ++ .../collect/package_infinite_recursion/tests/__init__.py | 0 .../collect/package_infinite_recursion/tests/test_basic.py | 2 ++ testing/python/collect.py | 6 ++++++ 4 files changed, 10 insertions(+) create mode 100644 testing/example_scripts/collect/package_infinite_recursion/conftest.py create mode 100644 testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py create mode 100644 testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py diff --git a/testing/example_scripts/collect/package_infinite_recursion/conftest.py b/testing/example_scripts/collect/package_infinite_recursion/conftest.py new file mode 100644 index 00000000000..9629fa646af --- /dev/null +++ b/testing/example_scripts/collect/package_infinite_recursion/conftest.py @@ -0,0 +1,2 @@ +def pytest_ignore_collect(path): + return False diff --git a/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py b/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py b/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py new file mode 100644 index 00000000000..f174823854e --- /dev/null +++ b/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py @@ -0,0 +1,2 @@ +def test(): + pass diff --git a/testing/python/collect.py b/testing/python/collect.py index a76cecada07..907b368ebfd 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1577,3 +1577,9 @@ def test_real(): ) result = testdir.runpytest("--keep-duplicates", a.strpath, a.strpath) result.stdout.fnmatch_lines(["*collected 2 item*"]) + + +def test_package_collection_infinite_recursion(testdir): + testdir.copy_example("collect/package_infinite_recursion") + result = testdir.runpytest() + result.stdout.fnmatch_lines("*1 passed*") From fe0a76e1a61fab631ba21ca29a4cd13c71a3f807 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Aug 2018 15:39:36 -0300 Subject: [PATCH 03/41] Fix recursion bug if a pytest_ignore_collect returns False instead of None --- changelog/3771.bugfix.rst | 1 + src/_pytest/python.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog/3771.bugfix.rst diff --git a/changelog/3771.bugfix.rst b/changelog/3771.bugfix.rst new file mode 100644 index 00000000000..09c953aa22a --- /dev/null +++ b/changelog/3771.bugfix.rst @@ -0,0 +1 @@ +Fix infinite recursion during collection if a ``pytest_ignore_collect`` returns ``False`` instead of ``None``. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 5b8305e7763..2657bff638a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -561,7 +561,7 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): def _recurse(self, path): ihook = self.gethookproxy(path.dirpath()) if ihook.pytest_ignore_collect(path=path, config=self.config): - return + return False for pat in self._norecursepatterns: if path.check(fnmatch=pat): return False @@ -594,9 +594,12 @@ def isinitpath(self, path): return path in self.session._initialpaths def collect(self): - path = self.fspath.dirpath() + this_path = self.fspath.dirpath() pkg_prefix = None - for path in path.visit(fil=lambda x: 1, rec=self._recurse, bf=True, sort=True): + for path in this_path.visit(rec=self._recurse, bf=True, sort=True): + # we will visit our own __init__.py file, in which case we skip it + if path.basename == "__init__.py" and path.dirpath() == this_path: + continue if pkg_prefix and pkg_prefix in path.parts(): continue for x in self._collectfile(path): From 0a1c2a7ca1d37070b0ca819c7e465bd35b4d79ff Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 4 Aug 2018 13:15:20 -0300 Subject: [PATCH 04/41] Add a changelog blurb and title, similar to tox --- CHANGELOG.rst | 10 ++++++++++ doc/en/changelog.rst | 3 --- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 90e8cc1acf1..e837807cb29 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,13 @@ +================= +Changelog history +================= + +Versions follow `Semantic Versioning `_ (``..``). + +Backward incompatible (breaking) changes will only be introduced in major versions +with advance notice in the **Deprecations** section of releases. + + .. You should *NOT* be adding new change log entries to this file, this file is managed by towncrier. You *may* edit previous change logs to diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index a59b3c7e250..f9a219a92ae 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -1,7 +1,4 @@ .. _changelog: -Changelog history -================================= - .. include:: ../../CHANGELOG.rst From ef8ec01e3979aff992d63540a0e36a957e133c76 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 4 Aug 2018 15:14:00 -0300 Subject: [PATCH 05/41] Fix issue where fixtures would lose the decorated functionality Fix #3774 --- src/_pytest/compat.py | 7 ++++ src/_pytest/fixtures.py | 7 ++-- testing/acceptance_test.py | 7 ++++ .../acceptance/fixture_mock_integration.py | 17 ++++++++++ testing/test_compat.py | 32 +++++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 testing/example_scripts/acceptance/fixture_mock_integration.py diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 52051ff2326..57ad4fdd84d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -234,6 +234,13 @@ def get_real_func(obj): """ start_obj = obj for i in range(100): + # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function + # to trigger a warning if it gets called directly instead of by pytest: we don't + # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) + new_obj = getattr(obj, "__pytest_wrapped__", None) + if new_obj is not None: + obj = new_obj + break new_obj = getattr(obj, "__wrapped__", None) if new_obj is None: break diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 818c5b81f74..0d63b151fd7 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -954,9 +954,6 @@ def _ensure_immutable_ids(ids): def wrap_function_to_warning_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can issue warnings about it being called directly, instead of used as an argument in a test function. - - The warning is emitted only in Python 3, because I didn't find a reliable way to make the wrapper function - keep the original signature, and we probably will drop Python 2 in Pytest 4 anyway. """ is_yield_function = is_generator(function) msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) @@ -982,6 +979,10 @@ def result(*args, **kwargs): if six.PY2: result.__wrapped__ = function + # keep reference to the original function in our own custom attribute so we don't unwrap + # further than this point and lose useful wrappings like @mock.patch (#3774) + result.__pytest_wrapped__ = function + return result diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 6f12cc3351a..bc4e3bed85f 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1044,3 +1044,10 @@ def test2(): ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) + + +def test_fixture_mock_integration(testdir): + """Test that decorators applied to fixture are left working (#3774)""" + p = testdir.copy_example("acceptance/fixture_mock_integration.py") + result = testdir.runpytest(p) + result.stdout.fnmatch_lines("*1 passed*") diff --git a/testing/example_scripts/acceptance/fixture_mock_integration.py b/testing/example_scripts/acceptance/fixture_mock_integration.py new file mode 100644 index 00000000000..51f46f82cae --- /dev/null +++ b/testing/example_scripts/acceptance/fixture_mock_integration.py @@ -0,0 +1,17 @@ +"""Reproduces issue #3774""" + +import mock + +import pytest + +config = {"mykey": "ORIGINAL"} + + +@pytest.fixture(scope="function") +@mock.patch.dict(config, {"mykey": "MOCKED"}) +def my_fixture(): + return config["mykey"] + + +def test_foobar(my_fixture): + assert my_fixture == "MOCKED" diff --git a/testing/test_compat.py b/testing/test_compat.py index 399b0d342b5..0663fa1496d 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,5 +1,8 @@ from __future__ import absolute_import, division, print_function import sys +from functools import wraps + +import six import pytest from _pytest.compat import is_generator, get_real_func, safe_getattr @@ -26,6 +29,8 @@ def __repr__(self): return "".format(left=self.left) def __getattr__(self, attr): + if attr == "__pytest_wrapped__": + raise AttributeError if not self.left: raise RuntimeError("its over") self.left -= 1 @@ -38,6 +43,33 @@ def __getattr__(self, attr): print(res) +def test_get_real_func(): + """Check that get_real_func correctly unwraps decorators until reaching the real function""" + + def decorator(f): + @wraps(f) + def inner(): + pass + + if six.PY2: + inner.__wrapped__ = f + return inner + + def func(): + pass + + wrapped_func = decorator(decorator(func)) + assert get_real_func(wrapped_func) is func + + wrapped_func2 = decorator(decorator(wrapped_func)) + assert get_real_func(wrapped_func2) is func + + # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point + # a function was wrapped by pytest itself + wrapped_func2.__pytest_wrapped__ = wrapped_func + assert get_real_func(wrapped_func2) is wrapped_func + + @pytest.mark.skipif( sys.version_info < (3, 4), reason="asyncio available in Python 3.4+" ) From 2c0d2eef40bf6393643f045ec1766e1b60ccc999 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 4 Aug 2018 16:35:24 -0300 Subject: [PATCH 06/41] Only consider actual functions when considering hooks Fix #3775 --- changelog/3775.bugfix.rst | 1 + src/_pytest/config/__init__.py | 5 +++++ .../config/collect_pytest_prefix/conftest.py | 2 ++ .../config/collect_pytest_prefix/test_foo.py | 2 ++ testing/test_config.py | 18 ++++++++++++++++++ 5 files changed, 28 insertions(+) create mode 100644 changelog/3775.bugfix.rst create mode 100644 testing/example_scripts/config/collect_pytest_prefix/conftest.py create mode 100644 testing/example_scripts/config/collect_pytest_prefix/test_foo.py diff --git a/changelog/3775.bugfix.rst b/changelog/3775.bugfix.rst new file mode 100644 index 00000000000..dd5263f743a --- /dev/null +++ b/changelog/3775.bugfix.rst @@ -0,0 +1 @@ +Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 421d124e9a9..921b3ef2026 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,6 +1,7 @@ """ command line options, ini-file and conftest.py processing. """ from __future__ import absolute_import, division, print_function import argparse +import inspect import shlex import traceback import types @@ -252,6 +253,10 @@ def parse_hookimpl_opts(self, plugin, name): method = getattr(plugin, name) opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name) + # consider only actual functions for hooks (#3775) + if not inspect.isroutine(method): + return + # collect unmarked hooks as long as they have the `pytest_' prefix if opts is None and name.startswith("pytest_"): opts = {} diff --git a/testing/example_scripts/config/collect_pytest_prefix/conftest.py b/testing/example_scripts/config/collect_pytest_prefix/conftest.py new file mode 100644 index 00000000000..56a4c71d358 --- /dev/null +++ b/testing/example_scripts/config/collect_pytest_prefix/conftest.py @@ -0,0 +1,2 @@ +class pytest_something(object): + pass diff --git a/testing/example_scripts/config/collect_pytest_prefix/test_foo.py b/testing/example_scripts/config/collect_pytest_prefix/test_foo.py new file mode 100644 index 00000000000..8f2d73cfa4f --- /dev/null +++ b/testing/example_scripts/config/collect_pytest_prefix/test_foo.py @@ -0,0 +1,2 @@ +def test_foo(): + pass diff --git a/testing/test_config.py b/testing/test_config.py index b507bb8e823..ef9dacd9c5b 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -745,6 +745,24 @@ def test_get_plugin_specs_as_list(): assert _get_plugin_specs_as_list(("foo", "bar")) == ["foo", "bar"] +def test_collect_pytest_prefix_bug_integration(testdir): + """Integration test for issue #3775""" + p = testdir.copy_example("config/collect_pytest_prefix") + result = testdir.runpytest(p) + result.stdout.fnmatch_lines("* 1 passed *") + + +def test_collect_pytest_prefix_bug(pytestconfig): + """Ensure we collect only actual functions from conftest files (#3775)""" + + class Dummy(object): + class pytest_something(object): + pass + + pm = pytestconfig.pluginmanager + assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None + + class TestWarning(object): def test_warn_config(self, testdir): testdir.makeconftest( From aa358433b0d63655981e2bac112ab5a490927716 Mon Sep 17 00:00:00 2001 From: Wes Thomas Date: Wed, 8 Aug 2018 18:13:21 -0500 Subject: [PATCH 07/41] Fix AttributeError bug in TestCaseFunction.teardown by creating TestCaseFunction._testcase as attribute of class with a None default. --- changelog/3788.bugfix.rst | 1 + src/_pytest/unittest.py | 1 + testing/test_unittest.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changelog/3788.bugfix.rst diff --git a/changelog/3788.bugfix.rst b/changelog/3788.bugfix.rst new file mode 100644 index 00000000000..d1bf68ba596 --- /dev/null +++ b/changelog/3788.bugfix.rst @@ -0,0 +1 @@ +Fix AttributeError bug in TestCaseFunction.teardown by creating TestCaseFunction._testcase as attribute of class with a None default. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index d6e7cb27247..a135dbd53f3 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -69,6 +69,7 @@ def collect(self): class TestCaseFunction(Function): nofuncargs = True _excinfo = None + _testcase = None def setup(self): self._testcase = self.parent.obj(self.name) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 482e8928007..444725ab9e9 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -989,3 +989,24 @@ def test_two(self): result = testdir.runpytest("-s") result.assert_outcomes(passed=2) + + +def test_testcase_handles_init_exceptions(testdir): + """ + Regression test to make sure exceptions in the __init__ method are bubbled up correctly. + See https://github.com/pytest-dev/pytest/issues/3788 + """ + testdir.makepyfile( + """ + from unittest import TestCase + import pytest + class MyTestCase(TestCase): + def __init__(self, *args, **kwargs): + raise Exception("should raise this exception") + def test_hello(self): + pass + """ + ) + result = testdir.runpytest() + assert "should raise this exception" in result.stdout.str() + assert "ERROR at teardown of MyTestCase.test_hello" not in result.stdout.str() From 051db6a33d75e0aff4664be12eb85206ec9d8234 Mon Sep 17 00:00:00 2001 From: Wes Thomas Date: Wed, 8 Aug 2018 18:18:18 -0500 Subject: [PATCH 08/41] Trimming Trailing Whitespace --- testing/test_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 444725ab9e9..56fdebf4877 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1002,7 +1002,7 @@ def test_testcase_handles_init_exceptions(testdir): import pytest class MyTestCase(TestCase): def __init__(self, *args, **kwargs): - raise Exception("should raise this exception") + raise Exception("should raise this exception") def test_hello(self): pass """ From 74d9f56d0f254f63468a3e53f1691e591d2e4c75 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 Aug 2018 21:24:14 -0300 Subject: [PATCH 09/41] Improve CHANGELOG a bit --- changelog/3788.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3788.bugfix.rst b/changelog/3788.bugfix.rst index d1bf68ba596..aa391e28b11 100644 --- a/changelog/3788.bugfix.rst +++ b/changelog/3788.bugfix.rst @@ -1 +1 @@ -Fix AttributeError bug in TestCaseFunction.teardown by creating TestCaseFunction._testcase as attribute of class with a None default. +Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``. From 67106f056b0633b35dd4a080ef120fa61b55cf37 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Aug 2018 09:20:37 -0300 Subject: [PATCH 10/41] Use a custom holder class so we can be sure __pytest_wrapper__ was set by us --- src/_pytest/compat.py | 16 ++++++++++++++-- src/_pytest/fixtures.py | 3 ++- testing/test_compat.py | 6 ++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 57ad4fdd84d..c3ecaf9121d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -228,6 +228,18 @@ def ascii_escaped(val): return val.encode("unicode-escape") +class _PytestWrapper(object): + """Dummy wrapper around a function object for internal use only. + + Used to correctly unwrap the underlying function object + when we are creating fixtures, because we wrap the function object ourselves with a decorator + to issue warnings when the fixture function is called directly. + """ + + def __init__(self, obj): + self.obj = obj + + def get_real_func(obj): """ gets the real function object of the (possibly) wrapped object by functools.wraps or functools.partial. @@ -238,8 +250,8 @@ def get_real_func(obj): # to trigger a warning if it gets called directly instead of by pytest: we don't # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) new_obj = getattr(obj, "__pytest_wrapped__", None) - if new_obj is not None: - obj = new_obj + if isinstance(new_obj, _PytestWrapper): + obj = new_obj.obj break new_obj = getattr(obj, "__wrapped__", None) if new_obj is None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0d63b151fd7..a6634cd1162 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -31,6 +31,7 @@ safe_getattr, FuncargnamesCompatAttr, get_real_method, + _PytestWrapper, ) from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning from _pytest.outcomes import fail, TEST_OUTCOME @@ -981,7 +982,7 @@ def result(*args, **kwargs): # keep reference to the original function in our own custom attribute so we don't unwrap # further than this point and lose useful wrappings like @mock.patch (#3774) - result.__pytest_wrapped__ = function + result.__pytest_wrapped__ = _PytestWrapper(function) return result diff --git a/testing/test_compat.py b/testing/test_compat.py index 0663fa1496d..a6249d14b2e 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -5,7 +5,7 @@ import six import pytest -from _pytest.compat import is_generator, get_real_func, safe_getattr +from _pytest.compat import is_generator, get_real_func, safe_getattr, _PytestWrapper from _pytest.outcomes import OutcomeException @@ -29,8 +29,6 @@ def __repr__(self): return "".format(left=self.left) def __getattr__(self, attr): - if attr == "__pytest_wrapped__": - raise AttributeError if not self.left: raise RuntimeError("its over") self.left -= 1 @@ -66,7 +64,7 @@ def func(): # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point # a function was wrapped by pytest itself - wrapped_func2.__pytest_wrapped__ = wrapped_func + wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func) assert get_real_func(wrapped_func2) is wrapped_func From 220288ac773c630bcf768174fd9db142b7249a6b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Aug 2018 12:33:02 -0300 Subject: [PATCH 11/41] Add CHANGELOG for issue #3774, missing from PR #3780 --- changelog/3774.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3774.bugfix.rst diff --git a/changelog/3774.bugfix.rst b/changelog/3774.bugfix.rst new file mode 100644 index 00000000000..89be8edb67e --- /dev/null +++ b/changelog/3774.bugfix.rst @@ -0,0 +1 @@ +Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``). From 266f05c4c4ce981bfa7a1d2266380380aaf4bb72 Mon Sep 17 00:00:00 2001 From: turturica Date: Thu, 9 Aug 2018 18:28:22 -0700 Subject: [PATCH 12/41] Fix #3751 --- src/_pytest/main.py | 5 +++-- testing/test_collection.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 105891e4686..eae0bb25548 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -505,8 +505,9 @@ def _collect(self, arg): root = self._node_cache[pkginit] else: col = root._collectfile(pkginit) - if col and isinstance(col, Package): - root = col[0] + if col: + if isinstance(col[0], Package): + root = col[0] self._node_cache[root.fspath] = root # If it's a directory argument, recurse and look for any Subpackages. diff --git a/testing/test_collection.py b/testing/test_collection.py index 23d82cb141b..6480cc85d42 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -647,7 +647,7 @@ def test_pkgfile(self, testdir): col = testdir.getnode(config, x) assert isinstance(col, pytest.Module) assert col.name == "x.py" - assert col.parent.parent is None + assert col.parent.parent.parent is None for col in col.listchain(): assert col.config is config From be11d3e19526148716b49705369cf0d2eeaf31a8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Aug 2018 12:49:06 -0300 Subject: [PATCH 13/41] Improve warning messages when addoption is called with string as `type` Encountered the warning myself and to me the message was not clear about what should be done to fix the warning --- src/_pytest/config/argparsing.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 21d99b0ceec..5a4e35b8813 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -174,23 +174,23 @@ def __init__(self, *names, **attrs): if isinstance(typ, six.string_types): if typ == "choice": warnings.warn( - "type argument to addoption() is a string %r." - " For parsearg this is optional and when supplied" - " should be a type." + "`type` argument to addoption() is the string %r." + " For choices this is optional and can be omitted, " + " but when supplied should be a type (for example `str` or `int`)." " (options: %s)" % (typ, names), DeprecationWarning, - stacklevel=3, + stacklevel=4, ) # argparse expects a type here take it from # the type of the first element attrs["type"] = type(attrs["choices"][0]) else: warnings.warn( - "type argument to addoption() is a string %r." - " For parsearg this should be a type." + "`type` argument to addoption() is the string %r, " + " but when supplied should be a type (for example `str` or `int`)." " (options: %s)" % (typ, names), DeprecationWarning, - stacklevel=3, + stacklevel=4, ) attrs["type"] = Argument._typ_map[typ] # used in test_parseopt -> test_parse_defaultgetter From bfd0addaeb29d25e88cbc56fb529b2181d401308 Mon Sep 17 00:00:00 2001 From: turturica Date: Fri, 10 Aug 2018 12:56:08 -0700 Subject: [PATCH 14/41] Fix test collection from packages mixed with directories. #3768 and #3789 --- src/_pytest/python.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2657bff638a..6282c13cf40 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -216,18 +216,6 @@ def pytest_pycollect_makemodule(path, parent): return Module(path, parent) -def pytest_ignore_collect(path, config): - # Skip duplicate packages. - keepduplicates = config.getoption("keepduplicates") - if keepduplicates: - duplicate_paths = config.pluginmanager._duplicatepaths - if path.basename == "__init__.py": - if path in duplicate_paths: - return True - else: - duplicate_paths.add(path) - - @hookimpl(hookwrapper=True) def pytest_pycollect_makeitem(collector, name, obj): outcome = yield @@ -554,9 +542,7 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): self.name = fspath.dirname self.trace = session.trace self._norecursepatterns = session._norecursepatterns - for path in list(session.config.pluginmanager._duplicatepaths): - if path.dirname == fspath.dirname and path != fspath: - session.config.pluginmanager._duplicatepaths.remove(path) + self.fspath = fspath def _recurse(self, path): ihook = self.gethookproxy(path.dirpath()) @@ -594,6 +580,15 @@ def isinitpath(self, path): return path in self.session._initialpaths def collect(self): + # XXX: HACK! + # Before starting to collect any files from this package we need + # to cleanup the duplicate paths added by the session's collect(). + # Proper fix is to not track these as duplicates in the first place. + for path in list(self.session.config.pluginmanager._duplicatepaths): + # if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts(): + if path.dirname.startswith(self.name): + self.session.config.pluginmanager._duplicatepaths.remove(path) + this_path = self.fspath.dirpath() pkg_prefix = None for path in this_path.visit(rec=self._recurse, bf=True, sort=True): From 50db718a6a0ea09be107f6bf9e9aa1a7002a91de Mon Sep 17 00:00:00 2001 From: turturica Date: Fri, 10 Aug 2018 13:57:29 -0700 Subject: [PATCH 15/41] Add a test description. --- testing/test_collection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/test_collection.py b/testing/test_collection.py index 6480cc85d42..3b9c5df6cc4 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -638,6 +638,10 @@ def test_global_file(self, testdir, tmpdir): assert col.config is config def test_pkgfile(self, testdir): + """Verify nesting when a module is within a package. + The parent chain should match: Module -> Package -> Session. + Session's parent should always be None. + """ tmpdir = testdir.tmpdir subdir = tmpdir.join("subdir") x = subdir.ensure("x.py") From 27b5435a40384a7e5b2ba85a9916774d171c4d66 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Aug 2018 18:18:07 -0300 Subject: [PATCH 16/41] Fix docs formatting and improve test a bit --- testing/test_collection.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 3b9c5df6cc4..5b494ba31af 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -639,9 +639,9 @@ def test_global_file(self, testdir, tmpdir): def test_pkgfile(self, testdir): """Verify nesting when a module is within a package. - The parent chain should match: Module -> Package -> Session. - Session's parent should always be None. - """ + The parent chain should match: Module -> Package -> Session. + Session's parent should always be None. + """ tmpdir = testdir.tmpdir subdir = tmpdir.join("subdir") x = subdir.ensure("x.py") @@ -649,8 +649,11 @@ def test_pkgfile(self, testdir): with subdir.as_cwd(): config = testdir.parseconfigure(x) col = testdir.getnode(config, x) - assert isinstance(col, pytest.Module) assert col.name == "x.py" + assert isinstance(col, pytest.Module) + assert isinstance(col.parent, pytest.Package) + assert isinstance(col.parent.parent, pytest.Session) + # session is batman (has no parents) assert col.parent.parent.parent is None for col in col.listchain(): assert col.config is config From e92893ed2443840058a1ed3321e471b319534f62 Mon Sep 17 00:00:00 2001 From: turturica Date: Fri, 10 Aug 2018 17:29:30 -0700 Subject: [PATCH 17/41] Add test for packages mixed with modules. --- testing/python/collect.py | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/testing/python/collect.py b/testing/python/collect.py index 907b368ebfd..c040cc09e68 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1583,3 +1583,43 @@ def test_package_collection_infinite_recursion(testdir): testdir.copy_example("collect/package_infinite_recursion") result = testdir.runpytest() result.stdout.fnmatch_lines("*1 passed*") + + +def test_package_with_modules(testdir): + """ + . + └── root + ├── __init__.py + ├── sub1 + │ ├── __init__.py + │ └── sub1_1 + │ ├── __init__.py + │ └── test_in_sub1.py + └── sub2 + └── test + └── test_in_sub2.py + + """ + root = testdir.mkpydir("root") + sub1 = root.mkdir("sub1") + sub1.ensure("__init__.py") + sub1_test = sub1.mkdir("sub1_1") + sub1_test.ensure("__init__.py") + sub2 = root.mkdir("sub2") + sub2_test = sub2.mkdir("sub2") + + sub1_test.join("test_in_sub1.py").write("def test_1(): pass") + sub2_test.join("test_in_sub2.py").write("def test_2(): pass") + + # Execute from . + result = testdir.runpytest("-v", "-s") + result.assert_outcomes(passed=2) + + # Execute from . with one argument "root" + result = testdir.runpytest("-v", "-s", "root") + result.assert_outcomes(passed=2) + + # Chdir into package's root and execute with no args + root.chdir() + result = testdir.runpytest("-v", "-s") + result.assert_outcomes(passed=2) From abae60c8d047aae92f5bb5215f42ab15771673a6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Aug 2018 22:04:42 -0300 Subject: [PATCH 18/41] Add CHANGELOG entries --- changelog/3768.bugfix.rst | 1 + changelog/3789.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/3768.bugfix.rst create mode 100644 changelog/3789.bugfix.rst diff --git a/changelog/3768.bugfix.rst b/changelog/3768.bugfix.rst new file mode 100644 index 00000000000..b824853c0c8 --- /dev/null +++ b/changelog/3768.bugfix.rst @@ -0,0 +1 @@ +Fix test collection from packages mixed with normal directories. diff --git a/changelog/3789.bugfix.rst b/changelog/3789.bugfix.rst new file mode 100644 index 00000000000..b824853c0c8 --- /dev/null +++ b/changelog/3789.bugfix.rst @@ -0,0 +1 @@ +Fix test collection from packages mixed with normal directories. From abbd7c30a43b56106fb323892311b9d4ba7a3105 Mon Sep 17 00:00:00 2001 From: Josh Holland Date: Sat, 11 Aug 2018 20:48:55 +0100 Subject: [PATCH 19/41] Unhide documentation for metafunc.config Fixes #3746. --- changelog/3746.doc.rst | 1 + src/_pytest/python.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog/3746.doc.rst diff --git a/changelog/3746.doc.rst b/changelog/3746.doc.rst new file mode 100644 index 00000000000..4adecbec0c7 --- /dev/null +++ b/changelog/3746.doc.rst @@ -0,0 +1 @@ +Add documentation for ``metafunc.config`` that had been mistakenly hidden. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2657bff638a..1561bddde07 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -883,12 +883,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): """ def __init__(self, definition, fixtureinfo, config, cls=None, module=None): - #: access to the :class:`_pytest.config.Config` object for the test session assert ( isinstance(definition, FunctionDefinition) or type(definition).__name__ == "DefinitionMock" ) self.definition = definition + + #: access to the :class:`_pytest.config.Config` object for the test session self.config = config #: the module object where the test function is defined in. From 6367f0f5f19185ae031e465fca904298b124b880 Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Tue, 14 Aug 2018 16:07:29 -0500 Subject: [PATCH 20/41] fix `filterwarnings` mark not registered --- src/_pytest/warnings.py | 8 ++++++++ testing/test_warnings.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index abd04801bde..f2f23a6e2be 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -49,6 +49,14 @@ def pytest_addoption(parser): ) +def pytest_configure(config): + config.addinivalue_line( + "markers", + "filterwarnings(warning): add a warning filter to the given test. " + "see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings ", + ) + + @contextmanager def catch_warnings_for_item(item): """ diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 15ec36600d6..99a5aff47ac 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -287,3 +287,18 @@ def test(): ) result = testdir.runpytest("-W", "always") result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"]) + + +def test_filterwarnings_mark_registration(testdir): + """Ensure filterwarnings mark is registered""" + testdir.makepyfile( + """ + import pytest + + @pytest.mark.filterwarnings('error') + def test_error(): + assert True + """ + ) + result = testdir.runpytest("--strict") + assert result.ret == 0 From cb77e65c97896b6fca8ce5f3d33541200a3d8b65 Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Tue, 14 Aug 2018 16:16:25 -0500 Subject: [PATCH 21/41] updated AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 49440194e5c..babe7fee013 100644 --- a/AUTHORS +++ b/AUTHORS @@ -217,3 +217,4 @@ Xuecong Liao Zoltán Máté Roland Puntaier Allan Feldman +Sankt Petersbug From e06a077ac25771591ed9cd21f26390e7e0d28e57 Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Tue, 14 Aug 2018 16:16:37 -0500 Subject: [PATCH 22/41] added changelog --- changelog/3671.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3671.bugfix.rst diff --git a/changelog/3671.bugfix.rst b/changelog/3671.bugfix.rst new file mode 100644 index 00000000000..c9562b39020 --- /dev/null +++ b/changelog/3671.bugfix.rst @@ -0,0 +1 @@ +Fix ``filterwarnings`` mark not registered \ No newline at end of file From c1c08852f99e647335fb8e5b1040450f7cc09e00 Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Tue, 14 Aug 2018 19:54:51 -0500 Subject: [PATCH 23/41] lint checks --- changelog/3671.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3671.bugfix.rst b/changelog/3671.bugfix.rst index c9562b39020..9c61f84631c 100644 --- a/changelog/3671.bugfix.rst +++ b/changelog/3671.bugfix.rst @@ -1 +1 @@ -Fix ``filterwarnings`` mark not registered \ No newline at end of file +Fix ``filterwarnings`` mark not registered From 212ee450b7836a4f2ab5e8626c521d5febcf94fe Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Tue, 14 Aug 2018 20:29:42 -0500 Subject: [PATCH 24/41] simplified test function --- testing/test_warnings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 99a5aff47ac..a26fb459776 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -296,8 +296,8 @@ def test_filterwarnings_mark_registration(testdir): import pytest @pytest.mark.filterwarnings('error') - def test_error(): - assert True + def test_func(): + pass """ ) result = testdir.runpytest("--strict") From 78ef531420f2327a0daaa1c28a19ceefb9ce7b7f Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Tue, 14 Aug 2018 20:33:55 -0500 Subject: [PATCH 25/41] corrected the position of myname --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index babe7fee013..4a322b0a548 100644 --- a/AUTHORS +++ b/AUTHORS @@ -182,6 +182,7 @@ Russel Winder Ryan Wooden Samuel Dion-Girardeau Samuele Pedroni +Sankt Petersbug Segev Finer Serhii Mozghovyi Simon Gomizelj @@ -217,4 +218,3 @@ Xuecong Liao Zoltán Máté Roland Puntaier Allan Feldman -Sankt Petersbug From 17644ff285a30aa43ec4c02c167fbdf4ad47a291 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 11 Aug 2018 08:54:48 -0700 Subject: [PATCH 26/41] Fix traceback reporting for exceptions with `__cause__` cycles. --- changelog/3804.bugfix.rst | 1 + src/_pytest/_code/code.py | 4 +++- testing/code/test_excinfo.py | 45 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 changelog/3804.bugfix.rst diff --git a/changelog/3804.bugfix.rst b/changelog/3804.bugfix.rst new file mode 100644 index 00000000000..d03afe9b2a4 --- /dev/null +++ b/changelog/3804.bugfix.rst @@ -0,0 +1 @@ +Fix traceback reporting for exceptions with ``__cause__`` cycles. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 78644db8a20..d6c5cd90edb 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -719,7 +719,9 @@ def repr_excinfo(self, excinfo): repr_chain = [] e = excinfo.value descr = None - while e is not None: + seen = set() + while e is not None and id(e) not in seen: + seen.add(id(e)) if excinfo: reprtraceback = self.repr_traceback(excinfo) reprcrash = excinfo._getreprcrash() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 403063ad6e7..fbdaeacf753 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -4,6 +4,7 @@ import operator import os import sys +import textwrap import _pytest import py import pytest @@ -1265,6 +1266,50 @@ def g(): ] ) + @pytest.mark.skipif("sys.version_info[0] < 3") + def test_exc_chain_repr_cycle(self, importasmod): + mod = importasmod( + """ + class Err(Exception): + pass + def fail(): + return 0 / 0 + def reraise(): + try: + fail() + except ZeroDivisionError as e: + raise Err() from e + def unreraise(): + try: + reraise() + except Err as e: + raise e.__cause__ + """ + ) + excinfo = pytest.raises(ZeroDivisionError, mod.unreraise) + r = excinfo.getrepr(style="short") + tw = TWMock() + r.toterminal(tw) + out = "\n".join(line for line in tw.lines if isinstance(line, str)) + expected_out = textwrap.dedent( + """\ + :13: in unreraise + reraise() + :10: in reraise + raise Err() from e + E test_exc_chain_repr_cycle0.mod.Err + + During handling of the above exception, another exception occurred: + :15: in unreraise + raise e.__cause__ + :8: in reraise + fail() + :5: in fail + return 0 / 0 + E ZeroDivisionError: division by zero""" + ) + assert out == expected_out + @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) From da9d814da43a32f9eb3924e5b5618a80d7786731 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 17 Aug 2018 00:20:51 +0200 Subject: [PATCH 27/41] Added test. --- testing/test_capture.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/testing/test_capture.py b/testing/test_capture.py index 5f5e1b98da1..9140d2fbfdb 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1385,3 +1385,31 @@ def test_pickling_and_unpickling_encoded_file(): ef = capture.EncodedFile(None, None) ef_as_str = pickle.dumps(ef) pickle.loads(ef_as_str) + + +def test_capsys_with_cli_logging(testdir): + # Issue 3819 + # capsys should work with real-time cli logging + testdir.makepyfile( + """ + import logging + import sys + + logger = logging.getLogger(__name__) + + def test_myoutput(capsys): # or use "capfd" for fd-level + print("hello") + sys.stderr.write("world\\n") + captured = capsys.readouterr() + assert captured.out == "hello\\n" + assert captured.err == "world\\n" + + logging.info("something") + + print("next") + captured = capsys.readouterr() + assert captured.out == "next\\n" + """ + ) + result = testdir.runpytest_subprocess("--log-cli-level=INFO") + assert result.ret == 0 From 2b71cb9c381effaea13fa00c096a8f4c76a663b5 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 17 Aug 2018 00:26:12 +0200 Subject: [PATCH 28/41] Added activation/deactivation of capture fixture in logging emit. --- src/_pytest/capture.py | 16 ++++++++++++++-- src/_pytest/logging.py | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index faa767a86c5..34d42821f35 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -85,6 +85,7 @@ class CaptureManager(object): def __init__(self, method): self._method = method self._global_capturing = None + self._current_item = None def _getcapture(self, method): if method == "fd": @@ -121,15 +122,23 @@ def suspend_global_capture(self, item=None, in_=False): cap.suspend_capturing(in_=in_) return outerr - def activate_fixture(self, item): + def activate_fixture(self, item=None): """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over the global capture. """ + if item is None: + if self._current_item is None: + return + item = self._current_item fixture = getattr(item, "_capture_fixture", None) if fixture is not None: fixture._start() - def deactivate_fixture(self, item): + def deactivate_fixture(self, item=None): + if item is None: + if self._current_item is None: + return + item = self._current_item """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" fixture = getattr(item, "_capture_fixture", None) if fixture is not None: @@ -151,6 +160,7 @@ def pytest_make_collect_report(self, collector): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): + self._current_item = item self.resume_global_capture() # no need to activate a capture fixture because they activate themselves during creation; this # only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will @@ -160,6 +170,7 @@ def pytest_runtest_setup(self, item): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): + self._current_item = item self.resume_global_capture() # it is important to activate this fixture during the call phase so it overwrites the "global" # capture @@ -169,6 +180,7 @@ def pytest_runtest_call(self, item): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): + self._current_item = item self.resume_global_capture() self.activate_fixture(item) yield diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 1472b0dbdfd..65ac2d24bf7 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -573,6 +573,7 @@ def set_when(self, when): def emit(self, record): if self.capture_manager is not None: + self.capture_manager.deactivate_fixture() self.capture_manager.suspend_global_capture() try: if not self._first_record_emitted: @@ -589,3 +590,4 @@ def emit(self, record): finally: if self.capture_manager is not None: self.capture_manager.resume_global_capture() + self.capture_manager.activate_fixture() From e5a3c870b4fa6c3a1abd7f464b419c95190ced4a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Aug 2018 22:29:00 +0000 Subject: [PATCH 29/41] Preparing release version 3.7.2 --- CHANGELOG.rst | 34 +++++++++++++++++++++++++++++++ changelog/3671.bugfix.rst | 1 - changelog/3746.doc.rst | 1 - changelog/3768.bugfix.rst | 1 - changelog/3771.bugfix.rst | 1 - changelog/3774.bugfix.rst | 1 - changelog/3775.bugfix.rst | 1 - changelog/3788.bugfix.rst | 1 - changelog/3789.bugfix.rst | 1 - changelog/3804.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-3.7.2.rst | 25 +++++++++++++++++++++++ doc/en/example/markers.rst | 4 ++++ doc/en/example/nonpython.rst | 7 ++++--- doc/en/example/parametrize.rst | 7 +++---- 15 files changed, 71 insertions(+), 16 deletions(-) delete mode 100644 changelog/3671.bugfix.rst delete mode 100644 changelog/3746.doc.rst delete mode 100644 changelog/3768.bugfix.rst delete mode 100644 changelog/3771.bugfix.rst delete mode 100644 changelog/3774.bugfix.rst delete mode 100644 changelog/3775.bugfix.rst delete mode 100644 changelog/3788.bugfix.rst delete mode 100644 changelog/3789.bugfix.rst delete mode 100644 changelog/3804.bugfix.rst create mode 100644 doc/en/announce/release-3.7.2.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e837807cb29..d27891b686b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,40 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.7.2 (2018-08-16) +========================= + +Bug Fixes +--------- + +- `#3671 `_: Fix ``filterwarnings`` mark not registered + + +- `#3768 `_, `#3789 `_: Fix test collection from packages mixed with normal directories. + + +- `#3771 `_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` returns ``False`` instead of ``None``. + + +- `#3774 `_: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``). + + +- `#3775 `_: Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``. + + +- `#3788 `_: Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``. + + +- `#3804 `_: Fix traceback reporting for exceptions with ``__cause__`` cycles. + + + +Improved Documentation +---------------------- + +- `#3746 `_: Add documentation for ``metafunc.config`` that had been mistakenly hidden. + + pytest 3.7.1 (2018-08-02) ========================= diff --git a/changelog/3671.bugfix.rst b/changelog/3671.bugfix.rst deleted file mode 100644 index 9c61f84631c..00000000000 --- a/changelog/3671.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``filterwarnings`` mark not registered diff --git a/changelog/3746.doc.rst b/changelog/3746.doc.rst deleted file mode 100644 index 4adecbec0c7..00000000000 --- a/changelog/3746.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add documentation for ``metafunc.config`` that had been mistakenly hidden. diff --git a/changelog/3768.bugfix.rst b/changelog/3768.bugfix.rst deleted file mode 100644 index b824853c0c8..00000000000 --- a/changelog/3768.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix test collection from packages mixed with normal directories. diff --git a/changelog/3771.bugfix.rst b/changelog/3771.bugfix.rst deleted file mode 100644 index 09c953aa22a..00000000000 --- a/changelog/3771.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix infinite recursion during collection if a ``pytest_ignore_collect`` returns ``False`` instead of ``None``. diff --git a/changelog/3774.bugfix.rst b/changelog/3774.bugfix.rst deleted file mode 100644 index 89be8edb67e..00000000000 --- a/changelog/3774.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``). diff --git a/changelog/3775.bugfix.rst b/changelog/3775.bugfix.rst deleted file mode 100644 index dd5263f743a..00000000000 --- a/changelog/3775.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``. diff --git a/changelog/3788.bugfix.rst b/changelog/3788.bugfix.rst deleted file mode 100644 index aa391e28b11..00000000000 --- a/changelog/3788.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``. diff --git a/changelog/3789.bugfix.rst b/changelog/3789.bugfix.rst deleted file mode 100644 index b824853c0c8..00000000000 --- a/changelog/3789.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix test collection from packages mixed with normal directories. diff --git a/changelog/3804.bugfix.rst b/changelog/3804.bugfix.rst deleted file mode 100644 index d03afe9b2a4..00000000000 --- a/changelog/3804.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix traceback reporting for exceptions with ``__cause__`` cycles. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index d0f79a50084..1e7f2ce0f74 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.7.2 release-3.7.1 release-3.7.0 release-3.6.4 diff --git a/doc/en/announce/release-3.7.2.rst b/doc/en/announce/release-3.7.2.rst new file mode 100644 index 00000000000..4f7e0744d50 --- /dev/null +++ b/doc/en/announce/release-3.7.2.rst @@ -0,0 +1,25 @@ +pytest-3.7.2 +======================================= + +pytest 3.7.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Josh Holland +* Ronny Pfannschmidt +* Sankt Petersbug +* Wes Thomas +* turturica + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 93dc371970c..1ae99436d84 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -200,6 +200,8 @@ You can ask which markers exist for your test suite - the list includes our just $ pytest --markers @pytest.mark.webtest: mark a test as a webtest. + @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings + @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html @@ -374,6 +376,8 @@ The ``--markers`` option always gives you a list of available markers:: $ pytest --markers @pytest.mark.env(name): mark test to run only on named environment + @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings + @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 2bc70c4cc3a..bda15065ae7 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -84,8 +84,9 @@ interesting to just look at the collection tree:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items - - - + + + + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 43f4f598f66..fdc8025545b 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -411,11 +411,10 @@ is to be run with different sets of arguments for its three arguments: Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize):: . $ pytest -rs -q multipython.py - ...ssssssssssssssssssssssss [100%] + ...sss...sssssssss...sss... [100%] ========================= short test summary info ========================== - SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found - SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.5' not found - 3 passed, 24 skipped in 0.12 seconds + SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found + 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- From e0b088b52ea2c9ad9748acbed62dccfd20eccf42 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 16 Aug 2018 19:32:41 -0300 Subject: [PATCH 30/41] Changelog tweaks --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d27891b686b..6a864e8a3f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,13 +24,13 @@ pytest 3.7.2 (2018-08-16) Bug Fixes --------- -- `#3671 `_: Fix ``filterwarnings`` mark not registered +- `#3671 `_: Fix ``filterwarnings`` not being registered as a builtin mark. - `#3768 `_, `#3789 `_: Fix test collection from packages mixed with normal directories. -- `#3771 `_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` returns ``False`` instead of ``None``. +- `#3771 `_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` hook returns ``False`` instead of ``None``. - `#3774 `_: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``). From f66764e1c00624b91c3ab9554c7872f9611ebc42 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 17 Aug 2018 00:33:56 +0200 Subject: [PATCH 31/41] Added changelog and updated AUTHORS. --- AUTHORS | 1 + changelog/3819.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/3819.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 4a322b0a548..9c3cb6a1266 100644 --- a/AUTHORS +++ b/AUTHORS @@ -206,6 +206,7 @@ Trevor Bekolay Tyler Goodlet Tzu-ping Chung Vasily Kuznetsov +Victor Maryama Victor Uriarte Vidar T. Fauske Vitaly Lashmanov diff --git a/changelog/3819.bugfix.rst b/changelog/3819.bugfix.rst new file mode 100644 index 00000000000..02b33f9b1b2 --- /dev/null +++ b/changelog/3819.bugfix.rst @@ -0,0 +1 @@ +Fix ``stdout/stderr`` not getting captured when real-time cli logging is active. From e391c47ed8e6b4e68ec6e97b2ea3195a198e218f Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 17 Aug 2018 00:44:15 +0200 Subject: [PATCH 32/41] Update capture suspend test for logging. --- testing/logging/test_reporting.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 07c092191c5..a85a0aba036 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -889,6 +889,12 @@ def suspend_global_capture(self): def resume_global_capture(self): self.calls.append("resume_global_capture") + def activate_fixture(self, item=None): + self.calls.append("activate_fixture") + + def deactivate_fixture(self, item=None): + self.calls.append("deactivate_fixture") + # sanity check assert CaptureManager.suspend_capture_item assert CaptureManager.resume_global_capture @@ -909,8 +915,10 @@ def section(self, *args, **kwargs): logger.critical("some message") if has_capture_manager: assert MockCaptureManager.calls == [ + "deactivate_fixture", "suspend_global_capture", "resume_global_capture", + "activate_fixture", ] else: assert MockCaptureManager.calls == [] From 3059bfb1b3a45ab517da945acf74fe20abcad5a4 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 17 Aug 2018 13:00:27 +0200 Subject: [PATCH 33/41] Update test with another problem. --- testing/test_capture.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/test_capture.py b/testing/test_capture.py index 9140d2fbfdb..782971af04a 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1407,6 +1407,9 @@ def test_myoutput(capsys): # or use "capfd" for fd-level logging.info("something") print("next") + + logging.info("something") + captured = capsys.readouterr() assert captured.out == "next\\n" """ From 090f67a980adb7460d8600b6e49f7719c7c6e870 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 17 Aug 2018 13:41:26 +0200 Subject: [PATCH 34/41] Refactored implementation and updated tests. --- src/_pytest/capture.py | 31 +++++++++++++++++++++---------- src/_pytest/logging.py | 12 +++++------- testing/logging/test_reporting.py | 24 +++++++----------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 34d42821f35..e6392cb0e89 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -122,23 +122,34 @@ def suspend_global_capture(self, item=None, in_=False): cap.suspend_capturing(in_=in_) return outerr - def activate_fixture(self, item=None): + @contextlib.contextmanager + def disabled(self): + """Temporarily disables capture while inside the 'with' block.""" + if self._current_item is None: + yield + else: + item = self._current_item + fixture = getattr(item, "_capture_fixture", None) + if fixture is None: + yield + else: + fixture._capture.suspend_capturing() + self.suspend_global_capture(item=None, in_=False) + try: + yield + finally: + self.resume_global_capture() + fixture._capture.resume_capturing() + + def activate_fixture(self, item): """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over the global capture. """ - if item is None: - if self._current_item is None: - return - item = self._current_item fixture = getattr(item, "_capture_fixture", None) if fixture is not None: fixture._start() - def deactivate_fixture(self, item=None): - if item is None: - if self._current_item is None: - return - item = self._current_item + def deactivate_fixture(self, item): """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" fixture = getattr(item, "_capture_fixture", None) if fixture is not None: diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 65ac2d24bf7..ad049f1c56d 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -573,9 +573,11 @@ def set_when(self, when): def emit(self, record): if self.capture_manager is not None: - self.capture_manager.deactivate_fixture() - self.capture_manager.suspend_global_capture() - try: + ctx_manager = self.capture_manager.disabled() + else: + ctx_manager = _dummy_context_manager() + + with ctx_manager: if not self._first_record_emitted: self.stream.write("\n") self._first_record_emitted = True @@ -587,7 +589,3 @@ def emit(self, record): self.stream.section("live log " + self._when, sep="-", bold=True) self._section_name_shown = True logging.StreamHandler.emit(self, record) - finally: - if self.capture_manager is not None: - self.capture_manager.resume_global_capture() - self.capture_manager.activate_fixture() diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index a85a0aba036..ed89f5b7aa8 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -876,6 +876,7 @@ def test_live_logging_suspends_capture(has_capture_manager, request): is installed. """ import logging + import contextlib from functools import partial from _pytest.capture import CaptureManager from _pytest.logging import _LiveLoggingStreamHandler @@ -883,17 +884,11 @@ def test_live_logging_suspends_capture(has_capture_manager, request): class MockCaptureManager: calls = [] - def suspend_global_capture(self): - self.calls.append("suspend_global_capture") - - def resume_global_capture(self): - self.calls.append("resume_global_capture") - - def activate_fixture(self, item=None): - self.calls.append("activate_fixture") - - def deactivate_fixture(self, item=None): - self.calls.append("deactivate_fixture") + @contextlib.contextmanager + def disabled(self): + self.calls.append("enter disabled") + yield + self.calls.append("exit disabled") # sanity check assert CaptureManager.suspend_capture_item @@ -914,12 +909,7 @@ def section(self, *args, **kwargs): logger.critical("some message") if has_capture_manager: - assert MockCaptureManager.calls == [ - "deactivate_fixture", - "suspend_global_capture", - "resume_global_capture", - "activate_fixture", - ] + assert MockCaptureManager.calls == ["enter disabled", "exit disabled"] else: assert MockCaptureManager.calls == [] assert out_file.getvalue() == "\nsome message\n" From c3e494f6cf2fe7de97090193d0a96d4f499083c7 Mon Sep 17 00:00:00 2001 From: Vlad Shcherbina Date: Sat, 18 Aug 2018 01:05:30 +0300 Subject: [PATCH 35/41] Replace broken type annotations with type comments Fixes #3826. --- changelog/3826.trivial.rst | 1 + src/_pytest/fixtures.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/3826.trivial.rst diff --git a/changelog/3826.trivial.rst b/changelog/3826.trivial.rst new file mode 100644 index 00000000000..5354d0df9c0 --- /dev/null +++ b/changelog/3826.trivial.rst @@ -0,0 +1 @@ +Replace broken type annotations with type comments. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index a6634cd1162..cc8921e6599 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -307,8 +307,8 @@ class FuncFixtureInfo(object): # fixture names specified via usefixtures and via autouse=True in fixture # definitions. initialnames = attr.ib(type=tuple) - names_closure = attr.ib(type="List[str]") - name2fixturedefs = attr.ib(type="List[str, List[FixtureDef]]") + names_closure = attr.ib() # type: List[str] + name2fixturedefs = attr.ib() # type: List[str, List[FixtureDef]] def prune_dependency_tree(self): """Recompute names_closure from initialnames and name2fixturedefs From 14db2f91ba5e94ebf55c55000b0da1e3b755cc26 Mon Sep 17 00:00:00 2001 From: victor Date: Sat, 18 Aug 2018 12:16:47 +0200 Subject: [PATCH 36/41] Fixed global not called if no capsys fixture. Using now capsys context manager as well. --- src/_pytest/capture.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index e6392cb0e89..9e017a73306 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -121,25 +121,28 @@ def suspend_global_capture(self, item=None, in_=False): finally: cap.suspend_capturing(in_=in_) return outerr + + @contextlib.contextmanager + def _dummy_context_manager(self): + yield @contextlib.contextmanager def disabled(self): - """Temporarily disables capture while inside the 'with' block.""" - if self._current_item is None: - yield + """Context manager to temporarily disables capture.""" + + # Need to undo local capsys-et-al if exists before disabling global capture + fixture = getattr(self._current_item, "_capture_fixture", None) + if fixture: + ctx_manager = fixture.disabled() else: - item = self._current_item - fixture = getattr(item, "_capture_fixture", None) - if fixture is None: + ctx_manager = self._dummy_context_manager() + + with ctx_manager: + self.suspend_global_capture(item=None, in_=False) + try: yield - else: - fixture._capture.suspend_capturing() - self.suspend_global_capture(item=None, in_=False) - try: - yield - finally: - self.resume_global_capture() - fixture._capture.resume_capturing() + finally: + self.resume_global_capture() def activate_fixture(self, item): """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over @@ -340,12 +343,9 @@ def readouterr(self): def disabled(self): """Temporarily disables capture while inside the 'with' block.""" self._capture.suspend_capturing() - capmanager = self.request.config.pluginmanager.getplugin("capturemanager") - capmanager.suspend_global_capture(item=None, in_=False) try: yield finally: - capmanager.resume_global_capture() self._capture.resume_capturing() From 9fa7745795afd20e116cdd8ae93211a32054697b Mon Sep 17 00:00:00 2001 From: victor Date: Sat, 18 Aug 2018 13:40:08 +0200 Subject: [PATCH 37/41] Refactor, tests passing. --- src/_pytest/capture.py | 15 ++++++++------- src/_pytest/logging.py | 6 +----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 9e017a73306..323c9174395 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -129,14 +129,9 @@ def _dummy_context_manager(self): @contextlib.contextmanager def disabled(self): """Context manager to temporarily disables capture.""" - # Need to undo local capsys-et-al if exists before disabling global capture fixture = getattr(self._current_item, "_capture_fixture", None) - if fixture: - ctx_manager = fixture.disabled() - else: - ctx_manager = self._dummy_context_manager() - + ctx_manager = fixture.suspend() if fixture else self._dummy_context_manager() with ctx_manager: self.suspend_global_capture(item=None, in_=False) try: @@ -340,7 +335,7 @@ def readouterr(self): return self._outerr @contextlib.contextmanager - def disabled(self): + def suspend(self): """Temporarily disables capture while inside the 'with' block.""" self._capture.suspend_capturing() try: @@ -348,6 +343,12 @@ def disabled(self): finally: self._capture.resume_capturing() + @contextlib.contextmanager + def disabled(self): + capmanager = self.request.config.pluginmanager.getplugin("capturemanager") + with capmanager.disabled(): + yield + def safe_text_dupfile(f, mode, default_encoding="UTF8"): """ return an open text file object that's a duplicate of f on the diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index ad049f1c56d..fc40fc8b439 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -572,11 +572,7 @@ def set_when(self, when): self._test_outcome_written = False def emit(self, record): - if self.capture_manager is not None: - ctx_manager = self.capture_manager.disabled() - else: - ctx_manager = _dummy_context_manager() - + ctx_manager = self.capture_manager.disabled() if self.capture_manager else _dummy_context_manager() with ctx_manager: if not self._first_record_emitted: self.stream.write("\n") From eb2d0745301d597b7ef03450bea29c509237d60a Mon Sep 17 00:00:00 2001 From: victor Date: Sat, 18 Aug 2018 14:27:09 +0200 Subject: [PATCH 38/41] Black changes. --- src/_pytest/capture.py | 2 +- src/_pytest/logging.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 323c9174395..606f7bdd929 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -121,7 +121,7 @@ def suspend_global_capture(self, item=None, in_=False): finally: cap.suspend_capturing(in_=in_) return outerr - + @contextlib.contextmanager def _dummy_context_manager(self): yield diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index fc40fc8b439..395dc19e9be 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -572,7 +572,11 @@ def set_when(self, when): self._test_outcome_written = False def emit(self, record): - ctx_manager = self.capture_manager.disabled() if self.capture_manager else _dummy_context_manager() + ctx_manager = ( + self.capture_manager.disabled() + if self.capture_manager + else _dummy_context_manager() + ) with ctx_manager: if not self._first_record_emitted: self.stream.write("\n") From 9f7345d6639f803330835febee2694ceb925e08a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Aug 2018 11:08:03 -0300 Subject: [PATCH 39/41] Avoid leaving a reference to the last item on CaptureManager --- src/_pytest/capture.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 606f7bdd929..bd0fb87f00b 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -176,6 +176,7 @@ def pytest_runtest_setup(self, item): # be activated during pytest_runtest_call yield self.suspend_capture_item(item, "setup") + self._current_item = None @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): @@ -186,6 +187,7 @@ def pytest_runtest_call(self, item): self.activate_fixture(item) yield self.suspend_capture_item(item, "call") + self._current_item = None @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): @@ -194,6 +196,7 @@ def pytest_runtest_teardown(self, item): self.activate_fixture(item) yield self.suspend_capture_item(item, "teardown") + self._current_item = None @pytest.hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self, excinfo): From f674217c43c21f17b3693ce8b7b0e5bd06de2197 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Aug 2018 11:15:58 -0300 Subject: [PATCH 40/41] Moved dummy_context_manager to compat module --- src/_pytest/capture.py | 9 ++------- src/_pytest/compat.py | 8 ++++++++ src/_pytest/logging.py | 10 +++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index bd0fb87f00b..4bf979efc0f 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -14,8 +14,7 @@ import six import pytest -from _pytest.compat import CaptureIO - +from _pytest.compat import CaptureIO, dummy_context_manager patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} @@ -122,16 +121,12 @@ def suspend_global_capture(self, item=None, in_=False): cap.suspend_capturing(in_=in_) return outerr - @contextlib.contextmanager - def _dummy_context_manager(self): - yield - @contextlib.contextmanager def disabled(self): """Context manager to temporarily disables capture.""" # Need to undo local capsys-et-al if exists before disabling global capture fixture = getattr(self._current_item, "_capture_fixture", None) - ctx_manager = fixture.suspend() if fixture else self._dummy_context_manager() + ctx_manager = fixture.suspend() if fixture else dummy_context_manager() with ctx_manager: self.suspend_global_capture(item=None, in_=False) try: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index c3ecaf9121d..ea369ccf2af 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -8,6 +8,7 @@ import inspect import re import sys +from contextlib import contextmanager import py @@ -151,6 +152,13 @@ def getfuncargnames(function, is_method=False, cls=None): return arg_names +@contextmanager +def dummy_context_manager(): + """Context manager that does nothing, useful in situations where you might need an actual context manager or not + depending on some condition. Using this allow to keep the same code""" + yield + + def get_default_arg_names(function): # Note: this code intentionally mirrors the code at the beginning of getfuncargnames, # to get the arguments which were excluded from its result because they had default values diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 395dc19e9be..5b0fcd69386 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -6,6 +6,7 @@ import re import six +from _pytest.compat import dummy_context_manager from _pytest.config import create_terminal_writer import pytest import py @@ -369,11 +370,6 @@ def pytest_configure(config): config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") -@contextmanager -def _dummy_context_manager(): - yield - - class LoggingPlugin(object): """Attaches to the logging module and captures log messages for each test. """ @@ -537,7 +533,7 @@ def _setup_cli_logging(self): log_cli_handler, formatter=log_cli_formatter, level=log_cli_level ) else: - self.live_logs_context = _dummy_context_manager() + self.live_logs_context = dummy_context_manager() class _LiveLoggingStreamHandler(logging.StreamHandler): @@ -575,7 +571,7 @@ def emit(self, record): ctx_manager = ( self.capture_manager.disabled() if self.capture_manager - else _dummy_context_manager() + else dummy_context_manager() ) with ctx_manager: if not self._first_record_emitted: From 5cf7d1dba21b51d4d20dc61b5a2f50bf5fc4fbf0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Aug 2018 11:36:24 -0300 Subject: [PATCH 41/41] "suspend" method of capture fixture private Also change the context-manager to global_and_fixture_disabled to better convey its meaning --- src/_pytest/capture.py | 13 +++++++------ src/_pytest/logging.py | 2 +- testing/logging/test_reporting.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 4bf979efc0f..c84ba825eee 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -122,11 +122,11 @@ def suspend_global_capture(self, item=None, in_=False): return outerr @contextlib.contextmanager - def disabled(self): - """Context manager to temporarily disables capture.""" + def global_and_fixture_disabled(self): + """Context manager to temporarily disables global and current fixture capturing.""" # Need to undo local capsys-et-al if exists before disabling global capture fixture = getattr(self._current_item, "_capture_fixture", None) - ctx_manager = fixture.suspend() if fixture else dummy_context_manager() + ctx_manager = fixture._suspend() if fixture else dummy_context_manager() with ctx_manager: self.suspend_global_capture(item=None, in_=False) try: @@ -333,8 +333,8 @@ def readouterr(self): return self._outerr @contextlib.contextmanager - def suspend(self): - """Temporarily disables capture while inside the 'with' block.""" + def _suspend(self): + """Suspends this fixture's own capturing temporarily.""" self._capture.suspend_capturing() try: yield @@ -343,8 +343,9 @@ def suspend(self): @contextlib.contextmanager def disabled(self): + """Temporarily disables capture while inside the 'with' block.""" capmanager = self.request.config.pluginmanager.getplugin("capturemanager") - with capmanager.disabled(): + with capmanager.global_and_fixture_disabled(): yield diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 5b0fcd69386..c9c65c4c18d 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -569,7 +569,7 @@ def set_when(self, when): def emit(self, record): ctx_manager = ( - self.capture_manager.disabled() + self.capture_manager.global_and_fixture_disabled() if self.capture_manager else dummy_context_manager() ) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index ed89f5b7aa8..820295886f4 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -885,7 +885,7 @@ class MockCaptureManager: calls = [] @contextlib.contextmanager - def disabled(self): + def global_and_fixture_disabled(self): self.calls.append("enter disabled") yield self.calls.append("exit disabled")