From dff7b203f7ac83192611c361ebf89e861ba94e70 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Jun 2018 18:46:51 +0200 Subject: [PATCH 01/54] tox: clean up docs target --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index adc4e9746..03fa0af8b 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 220288ac773c630bcf768174fd9db142b7249a6b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Aug 2018 12:33:02 -0300 Subject: [PATCH 02/54] 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 000000000..89be8edb6 --- /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 03/54] 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 105891e46..eae0bb255 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -505,8 +505,9 @@ class Session(nodes.FSCollector): 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 23d82cb14..6480cc85d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -647,7 +647,7 @@ class Test_getinitialnodes(object): 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 04/54] 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 21d99b0ce..5a4e35b88 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -174,23 +174,23 @@ class Argument(object): 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 05/54] 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 2657bff63..6282c13cf 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 @@ class Package(Module): 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 @@ class Package(Module): 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 06/54] 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 6480cc85d..3b9c5df6c 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -638,6 +638,10 @@ class Test_getinitialnodes(object): 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 07/54] 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 3b9c5df6c..5b494ba31 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -639,9 +639,9 @@ class Test_getinitialnodes(object): 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 @@ class Test_getinitialnodes(object): 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 08/54] 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 907b368eb..c040cc09e 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 09/54] 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 000000000..b824853c0 --- /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 000000000..b824853c0 --- /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 10/54] 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 000000000..4adecbec0 --- /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 2657bff63..1561bddde 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 11/54] 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 abd04801b..f2f23a6e2 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 15ec36600..99a5aff47 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -287,3 +287,18 @@ def test_non_string_warning_argument(testdir): ) 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 12/54] updated AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 49440194e..babe7fee0 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 13/54] 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 000000000..c9562b390 --- /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 14/54] 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 c9562b390..9c61f8463 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 15/54] 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 99a5aff47..a26fb4597 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 16/54] corrected the position of myname --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index babe7fee0..4a322b0a5 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 17/54] 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 000000000..d03afe9b2 --- /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 78644db8a..d6c5cd90e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -719,7 +719,9 @@ class FormattedExcinfo(object): 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 403063ad6..fbdaeacf7 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function import operator import os import sys +import textwrap import _pytest import py import pytest @@ -1265,6 +1266,50 @@ raise ValueError() ] ) + @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 18/54] 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 5f5e1b98d..9140d2fbf 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 19/54] 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 faa767a86..34d42821f 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 @@ class CaptureManager(object): 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 @@ class CaptureManager(object): @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 @@ class CaptureManager(object): @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 @@ class CaptureManager(object): @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 1472b0dbd..65ac2d24b 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -573,6 +573,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 20/54] 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 e837807cb..d27891b68 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 9c61f8463..000000000 --- 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 4adecbec0..000000000 --- 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 b824853c0..000000000 --- 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 09c953aa2..000000000 --- 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 89be8edb6..000000000 --- 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 dd5263f74..000000000 --- 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 aa391e28b..000000000 --- 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 b824853c0..000000000 --- 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 d03afe9b2..000000000 --- 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 d0f79a500..1e7f2ce0f 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 000000000..4f7e0744d --- /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 93dc37197..1ae99436d 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 2bc70c4cc..bda15065a 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 43f4f598f..fdc802554 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 21/54] Changelog tweaks --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d27891b68..6a864e8a3 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 22/54] 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 4a322b0a5..9c3cb6a12 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 000000000..02b33f9b1 --- /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 23/54] 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 07c092191..a85a0aba0 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -889,6 +889,12 @@ def test_live_logging_suspends_capture(has_capture_manager, request): 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 test_live_logging_suspends_capture(has_capture_manager, request): 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 24/54] 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 9140d2fbf..782971af0 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1407,6 +1407,9 @@ def test_capsys_with_cli_logging(testdir): 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 25/54] 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 34d42821f..e6392cb0e 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -122,23 +122,34 @@ class CaptureManager(object): 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 65ac2d24b..ad049f1c5 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -573,9 +573,11 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 a85a0aba0..ed89f5b7a 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 test_live_logging_suspends_capture(has_capture_manager, request): 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 26/54] 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 000000000..5354d0df9 --- /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 a6634cd11..cc8921e65 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 27/54] 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 e6392cb0e..9e017a733 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -121,25 +121,28 @@ class CaptureManager(object): 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 @@ class CaptureFixture(object): 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 28/54] 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 9e017a733..323c91743 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -129,14 +129,9 @@ class CaptureManager(object): @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 @@ class CaptureFixture(object): 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 @@ class CaptureFixture(object): 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 ad049f1c5..fc40fc8b4 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -572,11 +572,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 29/54] 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 323c91743..606f7bdd9 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -121,7 +121,7 @@ class CaptureManager(object): 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 fc40fc8b4..395dc19e9 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -572,7 +572,11 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 30/54] 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 606f7bdd9..bd0fb87f0 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -176,6 +176,7 @@ class CaptureManager(object): # 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 @@ class CaptureManager(object): 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 @@ class CaptureManager(object): 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 31/54] 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 bd0fb87f0..4bf979efc 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -14,8 +14,7 @@ from tempfile import TemporaryFile 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 @@ class CaptureManager(object): 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 c3ecaf912..ea369ccf2 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -8,6 +8,7 @@ import functools 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 395dc19e9..5b0fcd693 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -6,6 +6,7 @@ from contextlib import closing, contextmanager 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 @@ class LoggingPlugin(object): 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 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 32/54] "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 4bf979efc..c84ba825e 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -122,11 +122,11 @@ class CaptureManager(object): 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 @@ class CaptureFixture(object): 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 @@ class CaptureFixture(object): @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 5b0fcd693..c9c65c4c1 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -569,7 +569,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): 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 ed89f5b7a..820295886 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -885,7 +885,7 @@ def test_live_logging_suspends_capture(has_capture_manager, request): calls = [] @contextlib.contextmanager - def disabled(self): + def global_and_fixture_disabled(self): self.calls.append("enter disabled") yield self.calls.append("exit disabled") From 273670b2a204f950df93d87baff26b04a47c0400 Mon Sep 17 00:00:00 2001 From: Tyler Richard Date: Wed, 20 Dec 2017 10:28:50 -0800 Subject: [PATCH 33/54] Fixes capfd so data is available after teardown. --- src/_pytest/capture.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c84ba825e..b2ab4e57a 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -188,7 +188,6 @@ class CaptureManager(object): def pytest_runtest_teardown(self, item): self._current_item = item self.resume_global_capture() - self.activate_fixture(item) yield self.suspend_capture_item(item, "teardown") self._current_item = None From c24c7e75e26516e032c750d49cec2b840fb128cf Mon Sep 17 00:00:00 2001 From: Tyler Richard Date: Tue, 26 Dec 2017 10:25:25 -0800 Subject: [PATCH 34/54] Added regression test for capfd in a fixture --- testing/test_capture.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/testing/test_capture.py b/testing/test_capture.py index 782971af0..da0fb5369 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1318,6 +1318,26 @@ def test_error_attribute_issue555(testdir): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) +def test_capfd_after_test(testdir): + testdir.makepyfile(""" + import sys + import pytest + import os + + @pytest.fixture() + def fix(capfd): + yield + out, err = capfd.readouterr() + assert out == 'lolcatz' + os.linesep + assert err == 'err' + + def test_a(fix): + print("lolcatz") + sys.stderr.write("err") + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + @pytest.mark.skipif( not sys.platform.startswith("win") and sys.version_info[:2] >= (3, 6), From f4c5994d27287e1ac2440a4ae17032f272208cb3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 18 Aug 2018 14:32:10 -0300 Subject: [PATCH 35/54] Fixtures during teardown can use capsys and capfd to get output from tests Fix #3033 --- changelog/3033.bugfix.rst | 1 + src/_pytest/capture.py | 36 ++++++++++++++++++++++------- testing/test_capture.py | 48 +++++++++++++++++++++++---------------- 3 files changed, 57 insertions(+), 28 deletions(-) create mode 100644 changelog/3033.bugfix.rst diff --git a/changelog/3033.bugfix.rst b/changelog/3033.bugfix.rst new file mode 100644 index 000000000..3fcd9dd11 --- /dev/null +++ b/changelog/3033.bugfix.rst @@ -0,0 +1 @@ +Fixtures during teardown can again use ``capsys`` and ``cafd`` to inspect output captured during tests. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index b2ab4e57a..deabcac8d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -188,6 +188,7 @@ class CaptureManager(object): def pytest_runtest_teardown(self, item): self._current_item = item self.resume_global_capture() + self.activate_fixture(item) yield self.suspend_capture_item(item, "teardown") self._current_item = None @@ -308,6 +309,9 @@ class CaptureFixture(object): def __init__(self, captureclass, request): self.captureclass = captureclass self.request = request + self._capture = None + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER def _start(self): self._capture = MultiCapture( @@ -316,20 +320,26 @@ class CaptureFixture(object): self._capture.start_capturing() def close(self): - cap = self.__dict__.pop("_capture", None) - if cap is not None: - self._outerr = cap.pop_outerr_to_orig() - cap.stop_capturing() + if self._capture is not None: + out, err = self._capture.pop_outerr_to_orig() + self._captured_out += out + self._captured_err += err + self._capture.stop_capturing() + self._capture = None def readouterr(self): """Read and return the captured output so far, resetting the internal buffer. :return: captured content as a namedtuple with ``out`` and ``err`` string attributes """ - try: - return self._capture.readouterr() - except AttributeError: - return self._outerr + captured_out, captured_err = self._captured_out, self._captured_err + if self._capture is not None: + out, err = self._capture.readouterr() + captured_out += out + captured_err += err + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER + return CaptureResult(captured_out, captured_err) @contextlib.contextmanager def _suspend(self): @@ -462,6 +472,7 @@ class MultiCapture(object): class NoCapture(object): + EMPTY_BUFFER = None __init__ = start = done = suspend = resume = lambda *args: None @@ -471,6 +482,8 @@ class FDCaptureBinary(object): snap() produces `bytes` """ + EMPTY_BUFFER = bytes() + def __init__(self, targetfd, tmpfile=None): self.targetfd = targetfd try: @@ -544,6 +557,8 @@ class FDCapture(FDCaptureBinary): snap() produces text """ + EMPTY_BUFFER = str() + def snap(self): res = FDCaptureBinary.snap(self) enc = getattr(self.tmpfile, "encoding", None) @@ -553,6 +568,9 @@ class FDCapture(FDCaptureBinary): class SysCapture(object): + + EMPTY_BUFFER = str() + def __init__(self, fd, tmpfile=None): name = patchsysdict[fd] self._old = getattr(sys, name) @@ -590,6 +608,8 @@ class SysCapture(object): class SysCaptureBinary(SysCapture): + EMPTY_BUFFER = bytes() + def snap(self): res = self.tmpfile.buffer.getvalue() self.tmpfile.seek(0) diff --git a/testing/test_capture.py b/testing/test_capture.py index da0fb5369..be3385dfb 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -647,6 +647,34 @@ class TestCaptureFixture(object): assert "stdout contents begin" not in result.stdout.str() assert "stderr contents begin" not in result.stdout.str() + @pytest.mark.parametrize("cap", ["capsys", "capfd"]) + def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap): + """Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)""" + testdir.makepyfile( + """ + import sys + import pytest + import os + + @pytest.fixture() + def fix({cap}): + print("setup out") + sys.stderr.write("setup err\\n") + yield + out, err = {cap}.readouterr() + assert out == 'setup out\\ncall out\\n' + assert err == 'setup err\\ncall err\\n' + + def test_a(fix): + print("call out") + sys.stderr.write("call err\\n") + """.format( + cap=cap + ) + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + def test_setup_failure_does_not_kill_capturing(testdir): sub1 = testdir.mkpydir("sub1") @@ -1318,26 +1346,6 @@ def test_error_attribute_issue555(testdir): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) -def test_capfd_after_test(testdir): - testdir.makepyfile(""" - import sys - import pytest - import os - - @pytest.fixture() - def fix(capfd): - yield - out, err = capfd.readouterr() - assert out == 'lolcatz' + os.linesep - assert err == 'err' - - def test_a(fix): - print("lolcatz") - sys.stderr.write("err") - """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1) - @pytest.mark.skipif( not sys.platform.startswith("win") and sys.version_info[:2] >= (3, 6), From 7d9b198f734f7d1968c88476545da29fb90c1040 Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 19 Aug 2018 02:32:36 +0200 Subject: [PATCH 36/54] Refactoring: Separated suspend from snapping (stopped always snapping when suspending - solves bug but still missing tests), reorganized functions and context managers. --- src/_pytest/capture.py | 135 +++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c84ba825e..f10a13a1f 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -62,8 +62,9 @@ def pytest_load_initial_conftests(early_config, parser, args): # finally trigger conftest loading but while capturing (issue93) capman.start_global_capturing() outcome = yield - out, err = capman.suspend_global_capture() + capman.suspend_global_capture() if outcome.excinfo is not None: + out, err = capman.snap_global_capture() sys.stdout.write(out) sys.stderr.write(err) @@ -96,6 +97,8 @@ class CaptureManager(object): else: raise ValueError("unknown capturing method: %r" % method) + # Global capturing control + def start_global_capturing(self): assert self._global_capturing is None self._global_capturing = self._getcapture(self._method) @@ -110,29 +113,15 @@ class CaptureManager(object): def resume_global_capture(self): self._global_capturing.resume_capturing() - def suspend_global_capture(self, item=None, in_=False): - if item is not None: - self.deactivate_fixture(item) + def suspend_global_capture(self, in_=False): cap = getattr(self, "_global_capturing", None) if cap is not None: - try: - outerr = cap.readouterr() - finally: - cap.suspend_capturing(in_=in_) - return outerr + cap.suspend_capturing(in_=in_) - @contextlib.contextmanager - 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() - with ctx_manager: - self.suspend_global_capture(item=None, in_=False) - try: - yield - finally: - self.resume_global_capture() + def snap_global_capture(self): + return self._global_capturing.readouterr() + + # Fixture Control (its just forwarding, think about removing this later) def activate_fixture(self, item): """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over @@ -148,12 +137,53 @@ class CaptureManager(object): if fixture is not None: fixture.close() + def suspend_fixture(self, item): + fixture = getattr(item, "_capture_fixture", None) + if fixture is not None: + fixture._suspend() + + def resume_fixture(self, item): + fixture = getattr(item, "_capture_fixture", None) + if fixture is not None: + fixture._resume() + + # Helper context managers + + @contextlib.contextmanager + 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 + self.suspend_fixture(self._current_item) + self.suspend_global_capture(in_=False) + try: + yield + finally: + self.resume_global_capture() + self.resume_fixture(self._current_item) + + @contextlib.contextmanager + def item_capture(self, when, item): + self.resume_global_capture() + self.activate_fixture(item) + try: + yield + finally: + self.deactivate_fixture(item) + self.suspend_global_capture(in_=False) + + out, err = self.snap_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) + + # Hooks + @pytest.hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector): if isinstance(collector, pytest.File): self.resume_global_capture() outcome = yield - out, err = self.suspend_global_capture() + self.suspend_global_capture() + out, err = self.snap_global_capture() rep = outcome.get_result() if out: rep.sections.append(("Captured stdout", out)) @@ -163,35 +193,27 @@ class CaptureManager(object): yield @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_setup(self, item): + def pytest_runtest_logstart(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 - # be activated during pytest_runtest_call - yield - self.suspend_capture_item(item, "setup") - self._current_item = None + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_logfinish(self, item): + self._current_item = item + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_setup(self, item): + with self.item_capture("setup", item): + yield @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 - self.activate_fixture(item) - yield - self.suspend_capture_item(item, "call") - self._current_item = None + with self.item_capture("call", item): + yield @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): - self._current_item = item - self.resume_global_capture() - self.activate_fixture(item) - yield - self.suspend_capture_item(item, "teardown") - self._current_item = None + with self.item_capture("teardown", item): + yield @pytest.hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self, excinfo): @@ -201,11 +223,6 @@ class CaptureManager(object): def pytest_internalerror(self, excinfo): self.stop_global_capturing() - def suspend_capture_item(self, item, when, in_=False): - out, err = self.suspend_global_capture(item, in_=in_) - item.add_report_section(when, "stdout", out) - item.add_report_section(when, "stderr", err) - capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"} @@ -311,10 +328,12 @@ class CaptureFixture(object): self.request = request def _start(self): - self._capture = MultiCapture( - out=True, err=True, in_=False, Capture=self.captureclass - ) - self._capture.start_capturing() + # Start if not started yet + if getattr(self, "_capture", None) is not None: + self._capture = MultiCapture( + out=True, err=True, in_=False, Capture=self.captureclass + ) + self._capture.start_capturing() def close(self): cap = self.__dict__.pop("_capture", None) @@ -332,14 +351,13 @@ class CaptureFixture(object): except AttributeError: return self._outerr - @contextlib.contextmanager def _suspend(self): """Suspends this fixture's own capturing temporarily.""" self._capture.suspend_capturing() - try: - yield - finally: - self._capture.resume_capturing() + + def _resume(self): + """Resumes this fixture's own capturing temporarily.""" + self._capture.resume_capturing() @contextlib.contextmanager def disabled(self): @@ -743,3 +761,4 @@ def _attempt_to_close_capture_file(f): pass else: f.close() + From 2255892d65810cc4db2b82e2621ed313a0c47f7f Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 19 Aug 2018 13:44:12 +0200 Subject: [PATCH 37/54] Improved test to cover more cases. --- testing/test_capture.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 782971af0..e47689c9c 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1387,17 +1387,46 @@ def test_pickling_and_unpickling_encoded_file(): pickle.loads(ef_as_str) -def test_capsys_with_cli_logging(testdir): +def test_capture_with_live_logging(testdir): # Issue 3819 - # capsys should work with real-time cli logging + # capture should work with live cli logging + + # Teardown report seems to have the capture for the whole process (setup, capture, teardown) + testdir.makeconftest(""" + def pytest_runtest_logreport(report): + if "test_global" in report.nodeid: + if report.when == "teardown": + assert "fix setup" in report.caplog + assert "something in test" in report.caplog + assert "fix teardown" in report.caplog + + assert "fix setup" in report.capstdout + assert "begin test" in report.capstdout + assert "end test" in report.capstdout + assert "fix teardown" in report.capstdout + """) + testdir.makepyfile( """ import logging import sys logger = logging.getLogger(__name__) + + @pytest.fixture + def fix1(): + print("fix setup") + logging("fix setup") + yield + logging("fix teardown") + print("fix teardown") + + def test_global(): + print("begin test") + logging.info("something in test") + print("end test") - def test_myoutput(capsys): # or use "capfd" for fd-level + def test_capsys(capsys): # or use "capfd" for fd-level print("hello") sys.stderr.write("world\\n") captured = capsys.readouterr() From 9e382e8d29f0563925eedb3dc5139aaee72a1e1c Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 19 Aug 2018 14:29:57 +0200 Subject: [PATCH 38/54] Fixed test. --- testing/test_capture.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index e47689c9c..b2185822a 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1396,32 +1396,29 @@ def test_capture_with_live_logging(testdir): def pytest_runtest_logreport(report): if "test_global" in report.nodeid: if report.when == "teardown": - assert "fix setup" in report.caplog - assert "something in test" in report.caplog - assert "fix teardown" in report.caplog - - assert "fix setup" in report.capstdout - assert "begin test" in report.capstdout - assert "end test" in report.capstdout - assert "fix teardown" in report.capstdout + with open("caplog", "w") as f: + f.write(report.caplog) + with open("capstdout", "w") as f: + f.write(report.capstdout) """) testdir.makepyfile( """ import logging import sys + import pytest logger = logging.getLogger(__name__) @pytest.fixture def fix1(): print("fix setup") - logging("fix setup") + logging.info("fix setup") yield - logging("fix teardown") + logging.info("fix teardown") print("fix teardown") - - def test_global(): + + def test_global(fix1): print("begin test") logging.info("something in test") print("end test") @@ -1434,9 +1431,7 @@ def test_capture_with_live_logging(testdir): assert captured.err == "world\\n" logging.info("something") - print("next") - logging.info("something") captured = capsys.readouterr() @@ -1445,3 +1440,18 @@ def test_capture_with_live_logging(testdir): ) result = testdir.runpytest_subprocess("--log-cli-level=INFO") assert result.ret == 0 + + with open("caplog", "r") as f: + caplog = f.read() + + assert "fix setup" in caplog + assert "something in test" in caplog + assert "fix teardown" in caplog + + with open("capstdout", "r") as f: + capstdout = f.read() + + assert "fix setup" in capstdout + assert "begin test" in capstdout + assert "end test" in capstdout + assert "fix teardown" in capstdout From 8b2c91836b507727141b12b1f47c8d29ef744d1e Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 19 Aug 2018 14:30:50 +0200 Subject: [PATCH 39/54] Fixed activation and used just runtest_protocol hook --- src/_pytest/capture.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f10a13a1f..658632e00 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -193,12 +193,10 @@ class CaptureManager(object): yield @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_logstart(self, item): - self._current_item = item - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_logfinish(self, item): + def pytest_runtest_protocol(self, item): self._current_item = item + yield + self._current_item = None @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): @@ -329,7 +327,7 @@ class CaptureFixture(object): def _start(self): # Start if not started yet - if getattr(self, "_capture", None) is not None: + if getattr(self, "_capture", None) is None: self._capture = MultiCapture( out=True, err=True, in_=False, Capture=self.captureclass ) From 0564b52c0e1ecdce87b98f902091f3e06a01cc13 Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 19 Aug 2018 15:26:57 +0200 Subject: [PATCH 40/54] Fixed integration with other modules/tests --- src/_pytest/debugging.py | 3 ++- src/_pytest/setuponly.py | 3 ++- testing/logging/test_reporting.py | 4 ---- testing/test_capture.py | 12 ++++++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 9991307d0..a0594e3e8 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -102,7 +102,8 @@ class PdbInvoke(object): def pytest_exception_interact(self, node, call, report): capman = node.config.pluginmanager.getplugin("capturemanager") if capman: - out, err = capman.suspend_global_capture(in_=True) + capman.suspend_global_capture(in_=True) + out, err = capman.snap_global_capture() sys.stdout.write(out) sys.stdout.write(err) _enter_pdb(node, call.excinfo, report) diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 81240d9d0..721b0a942 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -51,7 +51,8 @@ def _show_fixture_action(fixturedef, msg): config = fixturedef._fixturemanager.config capman = config.pluginmanager.getplugin("capturemanager") if capman: - out, err = capman.suspend_global_capture() + capman.suspend_global_capture() + out, err = capman.snap_global_capture() tw = config.get_terminal_writer() tw.line() diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 820295886..b8fc371d4 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -890,10 +890,6 @@ def test_live_logging_suspends_capture(has_capture_manager, request): yield self.calls.append("exit disabled") - # sanity check - assert CaptureManager.suspend_capture_item - assert CaptureManager.resume_global_capture - class DummyTerminal(six.StringIO): def section(self, *args, **kwargs): pass diff --git a/testing/test_capture.py b/testing/test_capture.py index b2185822a..626c4414a 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -70,19 +70,23 @@ class TestCaptureManager(object): try: capman = CaptureManager(method) capman.start_global_capturing() - outerr = capman.suspend_global_capture() + capman.suspend_global_capture() + outerr = capman.snap_global_capture() assert outerr == ("", "") - outerr = capman.suspend_global_capture() + capman.suspend_global_capture() + outerr = capman.snap_global_capture() assert outerr == ("", "") print("hello") - out, err = capman.suspend_global_capture() + capman.suspend_global_capture() + out, err = capman.snap_global_capture() if method == "no": assert old == (sys.stdout, sys.stderr, sys.stdin) else: assert not out capman.resume_global_capture() print("hello") - out, err = capman.suspend_global_capture() + capman.suspend_global_capture() + out, err = capman.snap_global_capture() if method != "no": assert out == "hello\n" capman.stop_global_capturing() From 7ea4992f169edf84eec10735be688523759d4543 Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 19 Aug 2018 15:46:02 +0200 Subject: [PATCH 41/54] Fixed linting. --- src/_pytest/capture.py | 3 +-- testing/logging/test_reporting.py | 1 - testing/test_capture.py | 8 +++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 658632e00..3147d9728 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -14,7 +14,7 @@ from tempfile import TemporaryFile import six import pytest -from _pytest.compat import CaptureIO, dummy_context_manager +from _pytest.compat import CaptureIO patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} @@ -759,4 +759,3 @@ def _attempt_to_close_capture_file(f): pass else: f.close() - diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index b8fc371d4..363982cf9 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -878,7 +878,6 @@ def test_live_logging_suspends_capture(has_capture_manager, request): import logging import contextlib from functools import partial - from _pytest.capture import CaptureManager from _pytest.logging import _LiveLoggingStreamHandler class MockCaptureManager: diff --git a/testing/test_capture.py b/testing/test_capture.py index 626c4414a..ec8c682e2 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1396,7 +1396,8 @@ def test_capture_with_live_logging(testdir): # capture should work with live cli logging # Teardown report seems to have the capture for the whole process (setup, capture, teardown) - testdir.makeconftest(""" + testdir.makeconftest( + """ def pytest_runtest_logreport(report): if "test_global" in report.nodeid: if report.when == "teardown": @@ -1404,7 +1405,8 @@ def test_capture_with_live_logging(testdir): f.write(report.caplog) with open("capstdout", "w") as f: f.write(report.capstdout) - """) + """ + ) testdir.makepyfile( """ @@ -1413,7 +1415,7 @@ def test_capture_with_live_logging(testdir): import pytest logger = logging.getLogger(__name__) - + @pytest.fixture def fix1(): print("fix setup") From e620798d33346187005b44ab0ff96decd5b5e95a Mon Sep 17 00:00:00 2001 From: wim glenn Date: Sun, 19 Aug 2018 23:11:31 -0500 Subject: [PATCH 42/54] more autodocs for pytester --- changelog/3833.doc.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/pytester.py | 20 ++++++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 changelog/3833.doc.rst diff --git a/changelog/3833.doc.rst b/changelog/3833.doc.rst new file mode 100644 index 000000000..d74ee10b2 --- /dev/null +++ b/changelog/3833.doc.rst @@ -0,0 +1 @@ +Added missing docs for ``pytester.Testdir`` \ No newline at end of file diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 86d92cf07..484c755da 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -460,7 +460,7 @@ To use it, include in your top-most ``conftest.py`` file:: .. autoclass:: Testdir() - :members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile + :members: .. autoclass:: RunResult() :members: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 5b42b81ee..b40a9e267 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -550,18 +550,22 @@ class Testdir(object): return ret def makefile(self, ext, *args, **kwargs): - """Create a new file in the testdir. + r"""Create new file(s) in the testdir. - ext: The extension the file should use, including the dot, e.g. `.py`. - - args: All args will be treated as strings and joined using newlines. + :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. + :param list[str] args: All args will be treated as strings and joined using newlines. The result will be written as contents to the file. The name of the file will be based on the test function requesting this fixture. - E.g. "testdir.makefile('.txt', 'line1', 'line2')" - - kwargs: Each keyword is the name of a file, while the value of it will + :param kwargs: Each keyword is the name of a file, while the value of it will be written as contents of the file. - E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')" + + Examples: + + .. code-block:: python + + testdir.makefile(".txt", "line1", "line2") + + testdir.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") """ return self._makefile(ext, args, kwargs) From e4bea9068bf800f1610b228457232cbdfd949343 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Sun, 19 Aug 2018 23:39:10 -0500 Subject: [PATCH 43/54] end of line for this file, perhaps? --- changelog/3833.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3833.doc.rst b/changelog/3833.doc.rst index d74ee10b2..254e2e4b6 100644 --- a/changelog/3833.doc.rst +++ b/changelog/3833.doc.rst @@ -1 +1 @@ -Added missing docs for ``pytester.Testdir`` \ No newline at end of file +Added missing docs for ``pytester.Testdir`` From d611b035891f481570721d105d392db642412368 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 20 Aug 2018 12:23:59 +0200 Subject: [PATCH 44/54] Parametrized tests for capfd as well. Separated global capture test. --- testing/test_capture.py | 52 ++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index ec8c682e2..c029a21f9 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1391,7 +1391,7 @@ def test_pickling_and_unpickling_encoded_file(): pickle.loads(ef_as_str) -def test_capture_with_live_logging(testdir): +def test_global_capture_with_live_logging(testdir): # Issue 3819 # capture should work with live cli logging @@ -1405,7 +1405,7 @@ def test_capture_with_live_logging(testdir): f.write(report.caplog) with open("capstdout", "w") as f: f.write(report.capstdout) - """ + """ ) testdir.makepyfile( @@ -1428,20 +1428,6 @@ def test_capture_with_live_logging(testdir): print("begin test") logging.info("something in test") print("end test") - - def test_capsys(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") - logging.info("something") - - captured = capsys.readouterr() - assert captured.out == "next\\n" """ ) result = testdir.runpytest_subprocess("--log-cli-level=INFO") @@ -1461,3 +1447,37 @@ def test_capture_with_live_logging(testdir): assert "begin test" in capstdout assert "end test" in capstdout assert "fix teardown" in capstdout + + +@pytest.mark.parametrize("capture_fixture", ["capsys", "capfd"]) +def test_capture_with_live_logging(testdir, capture_fixture): + # Issue 3819 + # capture should work with live cli logging + + testdir.makepyfile( + """ + import logging + import sys + + logger = logging.getLogger(__name__) + + def test_capture({0}): + print("hello") + sys.stderr.write("world\\n") + captured = {0}.readouterr() + assert captured.out == "hello\\n" + assert captured.err == "world\\n" + + logging.info("something") + print("next") + logging.info("something") + + captured = {0}.readouterr() + assert captured.out == "next\\n" + """.format( + capture_fixture + ) + ) + + result = testdir.runpytest_subprocess("--log-cli-level=INFO") + assert result.ret == 0 From 4de247cfa0e5067d7270ae2de6fe1f2dac9bd592 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Aug 2018 06:27:35 -0700 Subject: [PATCH 45/54] Use more flexible `language_version: python3` --- .pre-commit-config.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bac8bb6e2..faae82372 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,13 +5,13 @@ repos: hooks: - id: black args: [--safe, --quiet] - language_version: python3.6 + language_version: python3 - repo: https://github.com/asottile/blacken-docs rev: v0.2.0 hooks: - id: blacken-docs additional_dependencies: [black==18.6b4] - language_version: python3.6 + language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.3.0 hooks: @@ -37,7 +37,6 @@ repos: files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ language: python additional_dependencies: [pygments, restructuredtext_lint] - python_version: python3.6 - id: changelogs-rst name: changelog files must end in .rst entry: ./scripts/fail From 70ebab3537f52163989df6ca6832d6dbb77f41bf Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 20 Aug 2018 17:48:14 +0200 Subject: [PATCH 46/54] Renamed snap_global_capture to read_global_capture. --- src/_pytest/capture.py | 8 ++++---- src/_pytest/debugging.py | 2 +- src/_pytest/setuponly.py | 2 +- testing/test_capture.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3147d9728..8e9715cd8 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -64,7 +64,7 @@ def pytest_load_initial_conftests(early_config, parser, args): outcome = yield capman.suspend_global_capture() if outcome.excinfo is not None: - out, err = capman.snap_global_capture() + out, err = capman.read_global_capture() sys.stdout.write(out) sys.stderr.write(err) @@ -118,7 +118,7 @@ class CaptureManager(object): if cap is not None: cap.suspend_capturing(in_=in_) - def snap_global_capture(self): + def read_global_capture(self): return self._global_capturing.readouterr() # Fixture Control (its just forwarding, think about removing this later) @@ -171,7 +171,7 @@ class CaptureManager(object): self.deactivate_fixture(item) self.suspend_global_capture(in_=False) - out, err = self.snap_global_capture() + out, err = self.read_global_capture() item.add_report_section(when, "stdout", out) item.add_report_section(when, "stderr", err) @@ -183,7 +183,7 @@ class CaptureManager(object): self.resume_global_capture() outcome = yield self.suspend_global_capture() - out, err = self.snap_global_capture() + out, err = self.read_global_capture() rep = outcome.get_result() if out: rep.sections.append(("Captured stdout", out)) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index a0594e3e8..f51dff373 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -103,7 +103,7 @@ class PdbInvoke(object): capman = node.config.pluginmanager.getplugin("capturemanager") if capman: capman.suspend_global_capture(in_=True) - out, err = capman.snap_global_capture() + out, err = capman.read_global_capture() sys.stdout.write(out) sys.stdout.write(err) _enter_pdb(node, call.excinfo, report) diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 721b0a942..c3edc5f81 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -52,7 +52,7 @@ def _show_fixture_action(fixturedef, msg): capman = config.pluginmanager.getplugin("capturemanager") if capman: capman.suspend_global_capture() - out, err = capman.snap_global_capture() + out, err = capman.read_global_capture() tw = config.get_terminal_writer() tw.line() diff --git a/testing/test_capture.py b/testing/test_capture.py index c029a21f9..fb0e14b97 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -71,14 +71,14 @@ class TestCaptureManager(object): capman = CaptureManager(method) capman.start_global_capturing() capman.suspend_global_capture() - outerr = capman.snap_global_capture() + outerr = capman.read_global_capture() assert outerr == ("", "") capman.suspend_global_capture() - outerr = capman.snap_global_capture() + outerr = capman.read_global_capture() assert outerr == ("", "") print("hello") capman.suspend_global_capture() - out, err = capman.snap_global_capture() + out, err = capman.read_global_capture() if method == "no": assert old == (sys.stdout, sys.stderr, sys.stdin) else: @@ -86,7 +86,7 @@ class TestCaptureManager(object): capman.resume_global_capture() print("hello") capman.suspend_global_capture() - out, err = capman.snap_global_capture() + out, err = capman.read_global_capture() if method != "no": assert out == "hello\n" capman.stop_global_capturing() From 223eef6261ef31393452694fcec257f8a389fd94 Mon Sep 17 00:00:00 2001 From: Sankt Petersbug Date: Mon, 20 Aug 2018 14:25:01 -0500 Subject: [PATCH 47/54] Fix '--show-capture=no' capture teardown logs Add a check before printing teardown logs. 'print_teardown_sections' method does not check '--show-capture' option value, and teardown logs are always printed. Resolves: #3816 --- changelog/3816.bugfix.rst | 1 + src/_pytest/terminal.py | 5 +++++ testing/test_terminal.py | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 changelog/3816.bugfix.rst diff --git a/changelog/3816.bugfix.rst b/changelog/3816.bugfix.rst new file mode 100644 index 000000000..6a399d598 --- /dev/null +++ b/changelog/3816.bugfix.rst @@ -0,0 +1 @@ +Fix ``--show-capture=no`` option still capture teardown logs. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 7dd2edd6f..f79624989 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -706,7 +706,12 @@ class TerminalReporter(object): self._outrep_summary(rep) def print_teardown_sections(self, rep): + showcapture = self.config.option.showcapture + if showcapture == "no": + return for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue if "teardown" in secname: self._tw.sep("-", secname) if content[-1:] == "\n": diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a9da27980..88e5287e8 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -948,6 +948,46 @@ def pytest_report_header(config, startdir): assert "!This is stderr!" not in stdout assert "!This is a warning log msg!" not in stdout + def test_show_capture_with_teardown_logs(self, testdir): + """Ensure that the capturing of teardown logs honor --show-capture setting""" + testdir.makepyfile( + """ + import logging + import sys + import pytest + + @pytest.fixture(scope="function", autouse="True") + def hook_each_test(request): + yield + sys.stdout.write("!stdout!") + sys.stderr.write("!stderr!") + logging.warning("!log!") + + def test_func(): + assert False + """ + ) + + result = testdir.runpytest("--show-capture=stdout", "--tb=short").stdout.str() + assert "!stdout!" in result + assert "!stderr!" not in result + assert "!log!" not in result + + result = testdir.runpytest("--show-capture=stderr", "--tb=short").stdout.str() + assert "!stdout!" not in result + assert "!stderr!" in result + assert "!log!" not in result + + result = testdir.runpytest("--show-capture=log", "--tb=short").stdout.str() + assert "!stdout!" not in result + assert "!stderr!" not in result + assert "!log!" in result + + result = testdir.runpytest("--show-capture=no", "--tb=short").stdout.str() + assert "!stdout!" not in result + assert "!stderr!" not in result + assert "!log!" not in result + @pytest.mark.xfail("not hasattr(os, 'dup')") def test_fdopen_kept_alive_issue124(testdir): From 672f4bb5aa9fdb7303a9144507b2aea19c99b534 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Aug 2018 20:19:48 -0300 Subject: [PATCH 48/54] Improve CHANGELOG --- changelog/3816.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3816.bugfix.rst b/changelog/3816.bugfix.rst index 6a399d598..a50c8f729 100644 --- a/changelog/3816.bugfix.rst +++ b/changelog/3816.bugfix.rst @@ -1 +1 @@ -Fix ``--show-capture=no`` option still capture teardown logs. +Fix bug where ``--show-capture=no`` option would still show logs printed during fixture teardown. From 717775a1c69e21fff65c73d2a33e4bdc6a15c50e Mon Sep 17 00:00:00 2001 From: Natan Lao Date: Tue, 21 Aug 2018 16:57:33 -0700 Subject: [PATCH 49/54] Remove warning about #568 from documentation The documentation (https://docs.pytest.org/en/latest/skipping.html) references issue #568, which has since been fixed. --- doc/en/skipping.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index cda67554d..efdf008fb 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -136,12 +136,6 @@ You can use the ``skipif`` marker (as any other marker) on classes:: If the condition is ``True``, this marker will produce a skip result for each of the test methods of that class. -.. warning:: - - The use of ``skipif`` on classes that use inheritance is strongly - discouraged. `A Known bug `_ - in pytest's markers may cause unexpected behavior in super classes. - If you want to skip all test functions of a module, you may use the ``pytestmark`` name on the global level: From 07a560ff246a28e4ab8567404d7f55afb40a3c36 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Aug 2018 20:55:45 -0300 Subject: [PATCH 50/54] Fix collection error when tests is specified with --doctest-modules The problem was that _matchnodes would receive two items: [DoctestModule, Module]. It would then collect the first one, *cache it*, and fail to match against the name in the command line. Next, it would reuse the cached item (DoctestModule) instead of collecting the Module which would eventually find the "test" name on it. Added the type of the node to the cache key to avoid this problem, although I'm not a big fan of caches that have different key types. Fix #3843 --- changelog/3843.bugfix.rst | 1 + src/_pytest/main.py | 7 ++++--- testing/acceptance_test.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 changelog/3843.bugfix.rst diff --git a/changelog/3843.bugfix.rst b/changelog/3843.bugfix.rst new file mode 100644 index 000000000..3186c3fc5 --- /dev/null +++ b/changelog/3843.bugfix.rst @@ -0,0 +1 @@ +Fix collection error when specifying test functions directly in the command line using ``test.py::test`` syntax together with ``--doctest-module``. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index eae0bb255..947c6aa4b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -625,11 +625,12 @@ class Session(nodes.FSCollector): resultnodes.append(node) continue assert isinstance(node, nodes.Collector) - if node.nodeid in self._node_cache: - rep = self._node_cache[node.nodeid] + key = (type(node), node.nodeid) + if key in self._node_cache: + rep = self._node_cache[key] else: rep = collect_one_node(node) - self._node_cache[node.nodeid] = rep + self._node_cache[key] = rep if rep.passed: has_matched = False for x in rep.result: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index bc4e3bed8..5d6baf121 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -660,6 +660,16 @@ class TestInvocationVariants(object): ["*test_world.py::test_other*PASSED*", "*1 passed*"] ) + def test_invoke_test_and_doctestmodules(self, testdir): + p = testdir.makepyfile( + """ + def test(): + pass + """ + ) + result = testdir.runpytest(str(p) + "::test", "--doctest-modules") + result.stdout.fnmatch_lines(["*1 passed*"]) + @pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks") def test_cmdline_python_package_symlink(self, testdir, monkeypatch): """ From 80bea79512b28d266dc8b9b9be919426990fc7a3 Mon Sep 17 00:00:00 2001 From: Natan Lao Date: Tue, 21 Aug 2018 17:03:59 -0700 Subject: [PATCH 51/54] Add changelog entry --- changelog/3845.trivial.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/3845.trivial.rst diff --git a/changelog/3845.trivial.rst b/changelog/3845.trivial.rst new file mode 100644 index 000000000..8b32abaca --- /dev/null +++ b/changelog/3845.trivial.rst @@ -0,0 +1,2 @@ +Remove a reference to issue #568 from the documentation, which has since been +fixed. \ No newline at end of file From eb8d14519527dc3469c1663ecc1c5bbd9d73d454 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Aug 2018 21:08:21 -0300 Subject: [PATCH 52/54] Add link to issue in the CHANGELOG entry --- changelog/3845.trivial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/3845.trivial.rst b/changelog/3845.trivial.rst index 8b32abaca..29c45ab56 100644 --- a/changelog/3845.trivial.rst +++ b/changelog/3845.trivial.rst @@ -1,2 +1,2 @@ -Remove a reference to issue #568 from the documentation, which has since been -fixed. \ No newline at end of file +Remove a reference to issue `#568 `_ from the documentation, which has since been +fixed. From a12eadd9ef32a8312d49669d98aa533247db2fec Mon Sep 17 00:00:00 2001 From: Jennifer Rinker Date: Wed, 22 Aug 2018 15:37:35 +0200 Subject: [PATCH 53/54] resolving Issue #3824 - expanding docs --- AUTHORS | 1 + changelog/3824.doc.rst | 1 + doc/en/example/pythoncollection.rst | 16 +++++++++++++--- doc/en/reference.rst | 19 ++++++++++++++----- 4 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 changelog/3824.doc.rst diff --git a/AUTHORS b/AUTHORS index 9c3cb6a12..1641ea15e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -98,6 +98,7 @@ Javier Domingo Cansino Javier Romero Jeff Rackauckas Jeff Widman +Jenni Rinker John Eddie Ayson John Towler Jon Sonesen diff --git a/changelog/3824.doc.rst b/changelog/3824.doc.rst new file mode 100644 index 000000000..016065120 --- /dev/null +++ b/changelog/3824.doc.rst @@ -0,0 +1 @@ +Added example for multiple glob pattern matches in ``python_files``. diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 8e9d3ae62..6c86b8a63 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -100,8 +100,10 @@ Changing naming conventions You can configure different naming conventions by setting the :confval:`python_files`, :confval:`python_classes` and -:confval:`python_functions` configuration options. Example:: +:confval:`python_functions` configuration options. +Here is an example:: + # Example 1: have pytest look for "check" instead of "test" # content of pytest.ini # can also be defined in tox.ini or setup.cfg file, although the section # name in setup.cfg files should be "tool:pytest" @@ -112,7 +114,7 @@ the :confval:`python_files`, :confval:`python_classes` and This would make ``pytest`` look for tests in files that match the ``check_* .py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods -that match ``*_check``. For example, if we have:: +that match ``*_check``. For example, if we have:: # content of check_myapp.py class CheckMyApp(object): @@ -121,7 +123,7 @@ that match ``*_check``. For example, if we have:: def complex_check(self): pass -then the test collection looks like this:: +The test collection would look like this:: $ pytest --collect-only =========================== test session starts ============================ @@ -136,6 +138,14 @@ then the test collection looks like this:: ======================= no tests ran in 0.12 seconds ======================= +You can check for multiple glob patterns by adding a space between the patterns:: + + # Example 2: have pytest look for files with "test" and "example" + # content of pytest.ini, tox.ini, or setup.cfg file (replace "pytest" + # with "tool:pytest" for setup.cfg) + [pytest] + python_files=test_*.py example_*.py + .. note:: the ``python_functions`` and ``python_classes`` options has no effect diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 484c755da..28fc6805a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1229,7 +1229,8 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: python_classes One or more name prefixes or glob-style patterns determining which classes - are considered for test collection. By default, pytest will consider any + are considered for test collection. Search for multiple glob patterns by + adding a space between patterns. By default, pytest will consider any class prefixed with ``Test`` as a test collection. Here is an example of how to collect tests from classes that end in ``Suite``: @@ -1246,15 +1247,23 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: python_files One or more Glob-style file patterns determining which python files - are considered as test modules. By default, pytest will consider - any file matching with ``test_*.py`` and ``*_test.py`` globs as a test - module. + are considered as test modules. Search for multiple glob patterns by + adding a space between patterns:: + + .. code-block:: ini + + [pytest] + python_files = test_*.py check_*.py example_*.py + + By default, pytest will consider any file matching with ``test_*.py`` + and ``*_test.py`` globs as a test module. .. confval:: python_functions One or more name prefixes or glob-patterns determining which test functions - and methods are considered tests. By default, pytest will consider any + and methods are considered tests. Search for multiple glob patterns by + adding a space between patterns. By default, pytest will consider any function prefixed with ``test`` as a test. Here is an example of how to collect test functions and methods that end in ``_test``: From 5a7aa123ea29987287a4e01b8e61aacce282cd40 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 22 Aug 2018 11:22:30 -0300 Subject: [PATCH 54/54] Improve docs formatting --- doc/en/example/pythoncollection.rst | 10 +++++----- doc/en/reference.rst | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 6c86b8a63..b4950a75c 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -108,9 +108,9 @@ Here is an example:: # can also be defined in tox.ini or setup.cfg file, although the section # name in setup.cfg files should be "tool:pytest" [pytest] - python_files=check_*.py - python_classes=Check - python_functions=*_check + python_files = check_*.py + python_classes = Check + python_functions = *_check This would make ``pytest`` look for tests in files that match the ``check_* .py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods @@ -144,13 +144,13 @@ You can check for multiple glob patterns by adding a space between the patterns: # content of pytest.ini, tox.ini, or setup.cfg file (replace "pytest" # with "tool:pytest" for setup.cfg) [pytest] - python_files=test_*.py example_*.py + python_files = test_*.py example_*.py .. note:: the ``python_functions`` and ``python_classes`` options has no effect for ``unittest.TestCase`` test discovery because pytest delegates - detection of test case methods to unittest code. + discovery of test case methods to unittest code. Interpreting cmdline arguments as Python packages ----------------------------------------------------- diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 28fc6805a..042df9687 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1255,7 +1255,7 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] python_files = test_*.py check_*.py example_*.py - By default, pytest will consider any file matching with ``test_*.py`` + By default, pytest will consider any file matching with ``test_*.py`` and ``*_test.py`` globs as a test module.