diff --git a/AUTHORS b/AUTHORS
index 3b0448371e0..8599fa51aed 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
@@ -205,6 +206,7 @@ Trevor Bekolay
Tyler Goodlet
Tzu-ping Chung
Vasily Kuznetsov
+Victor Maryama
Victor Uriarte
Vidar T. Fauske
Virgil Dupras
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 90e8cc1acf1..6a864e8a3f9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,13 @@
+=================
+Changelog history
+=================
+
+Versions follow `Semantic Versioning `_ (``..``).
+
+Backward incompatible (breaking) changes will only be introduced in major versions
+with advance notice in the **Deprecations** section of releases.
+
+
..
You should *NOT* be adding new change log entries to this file, this
file is managed by towncrier. You *may* edit previous change logs to
@@ -8,6 +18,40 @@
.. towncrier release notes start
+pytest 3.7.2 (2018-08-16)
+=========================
+
+Bug Fixes
+---------
+
+- `#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`` hook 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/3819.bugfix.rst b/changelog/3819.bugfix.rst
new file mode 100644
index 00000000000..02b33f9b1b2
--- /dev/null
+++ b/changelog/3819.bugfix.rst
@@ -0,0 +1 @@
+Fix ``stdout/stderr`` not getting captured when real-time cli logging is active.
diff --git a/changelog/3826.trivial.rst b/changelog/3826.trivial.rst
new file mode 100644
index 00000000000..5354d0df9c0
--- /dev/null
+++ b/changelog/3826.trivial.rst
@@ -0,0 +1 @@
+Replace broken type annotations with type comments.
diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst
index d0f79a50084..1e7f2ce0f74 100644
--- a/doc/en/announce/index.rst
+++ b/doc/en/announce/index.rst
@@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2
+ release-3.7.2
release-3.7.1
release-3.7.0
release-3.6.4
diff --git a/doc/en/announce/release-3.7.2.rst b/doc/en/announce/release-3.7.2.rst
new file mode 100644
index 00000000000..4f7e0744d50
--- /dev/null
+++ b/doc/en/announce/release-3.7.2.rst
@@ -0,0 +1,25 @@
+pytest-3.7.2
+=======================================
+
+pytest 3.7.2 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+ pip install --upgrade pytest
+
+The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
+
+Thanks to all who contributed to this release, among them:
+
+* Anthony Sottile
+* Bruno Oliveira
+* Daniel Hahler
+* Josh Holland
+* Ronny Pfannschmidt
+* Sankt Petersbug
+* Wes Thomas
+* turturica
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst
index a59b3c7e250..f9a219a92ae 100644
--- a/doc/en/changelog.rst
+++ b/doc/en/changelog.rst
@@ -1,7 +1,4 @@
.. _changelog:
-Changelog history
-=================================
-
.. include:: ../../CHANGELOG.rst
diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst
index 93dc371970c..1ae99436d84 100644
--- a/doc/en/example/markers.rst
+++ b/doc/en/example/markers.rst
@@ -200,6 +200,8 @@ You can ask which markers exist for your test suite - the list includes our just
$ pytest --markers
@pytest.mark.webtest: mark a test as a webtest.
+ @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings
+
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
@@ -374,6 +376,8 @@ The ``--markers`` option always gives you a list of available markers::
$ pytest --markers
@pytest.mark.env(name): mark test to run only on named environment
+ @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings
+
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst
index 2bc70c4cc3a..bda15065ae7 100644
--- a/doc/en/example/nonpython.rst
+++ b/doc/en/example/nonpython.rst
@@ -84,8 +84,9 @@ interesting to just look at the collection tree::
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
collected 2 items
-
-
-
+
+
+
+
======================= no tests ran in 0.12 seconds =======================
diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst
index 43f4f598f66..fdc8025545b 100644
--- a/doc/en/example/parametrize.rst
+++ b/doc/en/example/parametrize.rst
@@ -411,11 +411,10 @@ is to be run with different sets of arguments for its three arguments:
Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize)::
. $ pytest -rs -q multipython.py
- ...ssssssssssssssssssssssss [100%]
+ ...sss...sssssssss...sss... [100%]
========================= short test summary info ==========================
- SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found
- SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.5' not found
- 3 passed, 24 skipped in 0.12 seconds
+ SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found
+ 12 passed, 15 skipped in 0.12 seconds
Indirect parametrization of optional implementations/imports
--------------------------------------------------------------------
diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py
index 78644db8a20..d6c5cd90edb 100644
--- a/src/_pytest/_code/code.py
+++ b/src/_pytest/_code/code.py
@@ -719,7 +719,9 @@ def repr_excinfo(self, excinfo):
repr_chain = []
e = excinfo.value
descr = None
- while e is not None:
+ seen = set()
+ while e is not None and id(e) not in seen:
+ seen.add(id(e))
if excinfo:
reprtraceback = self.repr_traceback(excinfo)
reprcrash = excinfo._getreprcrash()
diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py
index faa767a86c5..c84ba825eee 100644
--- a/src/_pytest/capture.py
+++ b/src/_pytest/capture.py
@@ -14,8 +14,7 @@
import six
import pytest
-from _pytest.compat import CaptureIO
-
+from _pytest.compat import CaptureIO, dummy_context_manager
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
@@ -85,6 +84,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,6 +121,19 @@ def suspend_global_capture(self, item=None, in_=False):
cap.suspend_capturing(in_=in_)
return outerr
+ @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 activate_fixture(self, item):
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
the global capture.
@@ -151,28 +164,34 @@ def pytest_make_collect_report(self, collector):
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
+ self._current_item = item
self.resume_global_capture()
# no need to activate a capture fixture because they activate themselves during creation; this
# only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
# 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):
+ 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
@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
@pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo):
@@ -314,17 +333,21 @@ def readouterr(self):
return self._outerr
@contextlib.contextmanager
- def disabled(self):
- """Temporarily disables capture while inside the 'with' block."""
+ def _suspend(self):
+ """Suspends this fixture's own capturing temporarily."""
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()
+ @contextlib.contextmanager
+ def disabled(self):
+ """Temporarily disables capture while inside the 'with' block."""
+ capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
+ with capmanager.global_and_fixture_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/compat.py b/src/_pytest/compat.py
index 52051ff2326..ea369ccf2af 100644
--- a/src/_pytest/compat.py
+++ b/src/_pytest/compat.py
@@ -8,6 +8,7 @@
import inspect
import re
import sys
+from contextlib import contextmanager
import py
@@ -151,6 +152,13 @@ def getfuncargnames(function, is_method=False, cls=None):
return arg_names
+@contextmanager
+def dummy_context_manager():
+ """Context manager that does nothing, useful in situations where you might need an actual context manager or not
+ depending on some condition. Using this allow to keep the same code"""
+ yield
+
+
def get_default_arg_names(function):
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
# to get the arguments which were excluded from its result because they had default values
@@ -228,12 +236,31 @@ def ascii_escaped(val):
return val.encode("unicode-escape")
+class _PytestWrapper(object):
+ """Dummy wrapper around a function object for internal use only.
+
+ Used to correctly unwrap the underlying function object
+ when we are creating fixtures, because we wrap the function object ourselves with a decorator
+ to issue warnings when the fixture function is called directly.
+ """
+
+ def __init__(self, obj):
+ self.obj = obj
+
+
def get_real_func(obj):
""" gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial.
"""
start_obj = obj
for i in range(100):
+ # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
+ # to trigger a warning if it gets called directly instead of by pytest: we don't
+ # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
+ new_obj = getattr(obj, "__pytest_wrapped__", None)
+ if isinstance(new_obj, _PytestWrapper):
+ obj = new_obj.obj
+ break
new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None:
break
diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py
index 3b2d622c357..2c4361407e1 100644
--- a/src/_pytest/config/__init__.py
+++ b/src/_pytest/config/__init__.py
@@ -1,6 +1,7 @@
""" command line options, ini-file and conftest.py processing. """
from __future__ import absolute_import, division, print_function
import argparse
+import inspect
import shlex
import traceback
import types
@@ -252,6 +253,10 @@ def parse_hookimpl_opts(self, plugin, name):
method = getattr(plugin, name)
opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name)
+ # consider only actual functions for hooks (#3775)
+ if not inspect.isroutine(method):
+ return
+
# collect unmarked hooks as long as they have the `pytest_' prefix
if opts is None and name.startswith("pytest_"):
opts = {}
diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py
index 21d99b0ceec..5a4e35b8813 100644
--- a/src/_pytest/config/argparsing.py
+++ b/src/_pytest/config/argparsing.py
@@ -174,23 +174,23 @@ def __init__(self, *names, **attrs):
if isinstance(typ, six.string_types):
if typ == "choice":
warnings.warn(
- "type argument to addoption() is a string %r."
- " For parsearg this is optional and when supplied"
- " should be a type."
+ "`type` argument to addoption() is the string %r."
+ " For choices this is optional and can be omitted, "
+ " but when supplied should be a type (for example `str` or `int`)."
" (options: %s)" % (typ, names),
DeprecationWarning,
- stacklevel=3,
+ stacklevel=4,
)
# argparse expects a type here take it from
# the type of the first element
attrs["type"] = type(attrs["choices"][0])
else:
warnings.warn(
- "type argument to addoption() is a string %r."
- " For parsearg this should be a type."
+ "`type` argument to addoption() is the string %r, "
+ " but when supplied should be a type (for example `str` or `int`)."
" (options: %s)" % (typ, names),
DeprecationWarning,
- stacklevel=3,
+ stacklevel=4,
)
attrs["type"] = Argument._typ_map[typ]
# used in test_parseopt -> test_parse_defaultgetter
diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py
index 818c5b81f74..cc8921e6599 100644
--- a/src/_pytest/fixtures.py
+++ b/src/_pytest/fixtures.py
@@ -31,6 +31,7 @@
safe_getattr,
FuncargnamesCompatAttr,
get_real_method,
+ _PytestWrapper,
)
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME
@@ -306,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
@@ -954,9 +955,6 @@ def _ensure_immutable_ids(ids):
def wrap_function_to_warning_if_called_directly(function, fixture_marker):
"""Wrap the given fixture function so we can issue warnings about it being called directly, instead of
used as an argument in a test function.
-
- The warning is emitted only in Python 3, because I didn't find a reliable way to make the wrapper function
- keep the original signature, and we probably will drop Python 2 in Pytest 4 anyway.
"""
is_yield_function = is_generator(function)
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
@@ -982,6 +980,10 @@ def result(*args, **kwargs):
if six.PY2:
result.__wrapped__ = function
+ # keep reference to the original function in our own custom attribute so we don't unwrap
+ # further than this point and lose useful wrappings like @mock.patch (#3774)
+ result.__pytest_wrapped__ = _PytestWrapper(function)
+
return result
diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py
index 1472b0dbdfd..c9c65c4c18d 100644
--- a/src/_pytest/logging.py
+++ b/src/_pytest/logging.py
@@ -6,6 +6,7 @@
import re
import six
+from _pytest.compat import dummy_context_manager
from _pytest.config import create_terminal_writer
import pytest
import py
@@ -369,11 +370,6 @@ def pytest_configure(config):
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
-@contextmanager
-def _dummy_context_manager():
- yield
-
-
class LoggingPlugin(object):
"""Attaches to the logging module and captures log messages for each test.
"""
@@ -537,7 +533,7 @@ def _setup_cli_logging(self):
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
)
else:
- self.live_logs_context = _dummy_context_manager()
+ self.live_logs_context = dummy_context_manager()
class _LiveLoggingStreamHandler(logging.StreamHandler):
@@ -572,9 +568,12 @@ def set_when(self, when):
self._test_outcome_written = False
def emit(self, record):
- if self.capture_manager is not None:
- self.capture_manager.suspend_global_capture()
- try:
+ ctx_manager = (
+ self.capture_manager.global_and_fixture_disabled()
+ if self.capture_manager
+ else dummy_context_manager()
+ )
+ with ctx_manager:
if not self._first_record_emitted:
self.stream.write("\n")
self._first_record_emitted = True
@@ -586,6 +585,3 @@ def emit(self, record):
self.stream.section("live log " + self._when, sep="-", bold=True)
self._section_name_shown = True
logging.StreamHandler.emit(self, record)
- finally:
- if self.capture_manager is not None:
- self.capture_manager.resume_global_capture()
diff --git a/src/_pytest/main.py b/src/_pytest/main.py
index 105891e4686..eae0bb25548 100644
--- a/src/_pytest/main.py
+++ b/src/_pytest/main.py
@@ -505,8 +505,9 @@ def _collect(self, arg):
root = self._node_cache[pkginit]
else:
col = root._collectfile(pkginit)
- if col and isinstance(col, Package):
- root = col[0]
+ if col:
+ if isinstance(col[0], Package):
+ root = col[0]
self._node_cache[root.fspath] = root
# If it's a directory argument, recurse and look for any Subpackages.
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index 5b8305e7763..e269b3bb4c3 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,14 +542,12 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
self.name = fspath.dirname
self.trace = session.trace
self._norecursepatterns = session._norecursepatterns
- for path in list(session.config.pluginmanager._duplicatepaths):
- if path.dirname == fspath.dirname and path != fspath:
- session.config.pluginmanager._duplicatepaths.remove(path)
+ self.fspath = fspath
def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath())
if ihook.pytest_ignore_collect(path=path, config=self.config):
- return
+ return False
for pat in self._norecursepatterns:
if path.check(fnmatch=pat):
return False
@@ -594,9 +580,21 @@ def isinitpath(self, path):
return path in self.session._initialpaths
def collect(self):
- path = self.fspath.dirpath()
+ # 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 path.visit(fil=lambda x: 1, rec=self._recurse, bf=True, sort=True):
+ for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
+ # we will visit our own __init__.py file, in which case we skip it
+ if path.basename == "__init__.py" and path.dirpath() == this_path:
+ continue
if pkg_prefix and pkg_prefix in path.parts():
continue
for x in self._collectfile(path):
@@ -880,12 +878,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.
diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py
index d6e7cb27247..a135dbd53f3 100644
--- a/src/_pytest/unittest.py
+++ b/src/_pytest/unittest.py
@@ -69,6 +69,7 @@ def collect(self):
class TestCaseFunction(Function):
nofuncargs = True
_excinfo = None
+ _testcase = None
def setup(self):
self._testcase = self.parent.obj(self.name)
diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py
index abd04801bde..f2f23a6e2be 100644
--- a/src/_pytest/warnings.py
+++ b/src/_pytest/warnings.py
@@ -49,6 +49,14 @@ def pytest_addoption(parser):
)
+def pytest_configure(config):
+ config.addinivalue_line(
+ "markers",
+ "filterwarnings(warning): add a warning filter to the given test. "
+ "see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings ",
+ )
+
+
@contextmanager
def catch_warnings_for_item(item):
"""
diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py
index 6f12cc3351a..bc4e3bed85f 100644
--- a/testing/acceptance_test.py
+++ b/testing/acceptance_test.py
@@ -1044,3 +1044,10 @@ def test2():
)
result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"])
+
+
+def test_fixture_mock_integration(testdir):
+ """Test that decorators applied to fixture are left working (#3774)"""
+ p = testdir.copy_example("acceptance/fixture_mock_integration.py")
+ result = testdir.runpytest(p)
+ result.stdout.fnmatch_lines("*1 passed*")
diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py
index 403063ad6e7..fbdaeacf753 100644
--- a/testing/code/test_excinfo.py
+++ b/testing/code/test_excinfo.py
@@ -4,6 +4,7 @@
import operator
import os
import sys
+import textwrap
import _pytest
import py
import pytest
@@ -1265,6 +1266,50 @@ def g():
]
)
+ @pytest.mark.skipif("sys.version_info[0] < 3")
+ def test_exc_chain_repr_cycle(self, importasmod):
+ mod = importasmod(
+ """
+ class Err(Exception):
+ pass
+ def fail():
+ return 0 / 0
+ def reraise():
+ try:
+ fail()
+ except ZeroDivisionError as e:
+ raise Err() from e
+ def unreraise():
+ try:
+ reraise()
+ except Err as e:
+ raise e.__cause__
+ """
+ )
+ excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
+ r = excinfo.getrepr(style="short")
+ tw = TWMock()
+ r.toterminal(tw)
+ out = "\n".join(line for line in tw.lines if isinstance(line, str))
+ expected_out = textwrap.dedent(
+ """\
+ :13: in unreraise
+ reraise()
+ :10: in reraise
+ raise Err() from e
+ E test_exc_chain_repr_cycle0.mod.Err
+
+ During handling of the above exception, another exception occurred:
+ :15: in unreraise
+ raise e.__cause__
+ :8: in reraise
+ fail()
+ :5: in fail
+ return 0 / 0
+ E ZeroDivisionError: division by zero"""
+ )
+ assert out == expected_out
+
@pytest.mark.parametrize("style", ["short", "long"])
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
diff --git a/testing/example_scripts/acceptance/fixture_mock_integration.py b/testing/example_scripts/acceptance/fixture_mock_integration.py
new file mode 100644
index 00000000000..51f46f82cae
--- /dev/null
+++ b/testing/example_scripts/acceptance/fixture_mock_integration.py
@@ -0,0 +1,17 @@
+"""Reproduces issue #3774"""
+
+import mock
+
+import pytest
+
+config = {"mykey": "ORIGINAL"}
+
+
+@pytest.fixture(scope="function")
+@mock.patch.dict(config, {"mykey": "MOCKED"})
+def my_fixture():
+ return config["mykey"]
+
+
+def test_foobar(my_fixture):
+ assert my_fixture == "MOCKED"
diff --git a/testing/example_scripts/collect/package_infinite_recursion/conftest.py b/testing/example_scripts/collect/package_infinite_recursion/conftest.py
new file mode 100644
index 00000000000..9629fa646af
--- /dev/null
+++ b/testing/example_scripts/collect/package_infinite_recursion/conftest.py
@@ -0,0 +1,2 @@
+def pytest_ignore_collect(path):
+ return False
diff --git a/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py b/testing/example_scripts/collect/package_infinite_recursion/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py b/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py
new file mode 100644
index 00000000000..f174823854e
--- /dev/null
+++ b/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py
@@ -0,0 +1,2 @@
+def test():
+ pass
diff --git a/testing/example_scripts/config/collect_pytest_prefix/conftest.py b/testing/example_scripts/config/collect_pytest_prefix/conftest.py
new file mode 100644
index 00000000000..56a4c71d358
--- /dev/null
+++ b/testing/example_scripts/config/collect_pytest_prefix/conftest.py
@@ -0,0 +1,2 @@
+class pytest_something(object):
+ pass
diff --git a/testing/example_scripts/config/collect_pytest_prefix/test_foo.py b/testing/example_scripts/config/collect_pytest_prefix/test_foo.py
new file mode 100644
index 00000000000..8f2d73cfa4f
--- /dev/null
+++ b/testing/example_scripts/config/collect_pytest_prefix/test_foo.py
@@ -0,0 +1,2 @@
+def test_foo():
+ pass
diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py
index 07c092191c5..820295886f4 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,11 +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")
+ @contextlib.contextmanager
+ def global_and_fixture_disabled(self):
+ self.calls.append("enter disabled")
+ yield
+ self.calls.append("exit disabled")
# sanity check
assert CaptureManager.suspend_capture_item
@@ -908,10 +909,7 @@ def section(self, *args, **kwargs):
logger.critical("some message")
if has_capture_manager:
- assert MockCaptureManager.calls == [
- "suspend_global_capture",
- "resume_global_capture",
- ]
+ assert MockCaptureManager.calls == ["enter disabled", "exit disabled"]
else:
assert MockCaptureManager.calls == []
assert out_file.getvalue() == "\nsome message\n"
diff --git a/testing/python/collect.py b/testing/python/collect.py
index a76cecada07..c040cc09e68 100644
--- a/testing/python/collect.py
+++ b/testing/python/collect.py
@@ -1577,3 +1577,49 @@ def test_real():
)
result = testdir.runpytest("--keep-duplicates", a.strpath, a.strpath)
result.stdout.fnmatch_lines(["*collected 2 item*"])
+
+
+def test_package_collection_infinite_recursion(testdir):
+ testdir.copy_example("collect/package_infinite_recursion")
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines("*1 passed*")
+
+
+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)
diff --git a/testing/test_capture.py b/testing/test_capture.py
index 5f5e1b98da1..782971af04a 100644
--- a/testing/test_capture.py
+++ b/testing/test_capture.py
@@ -1385,3 +1385,34 @@ 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")
+
+ logging.info("something")
+
+ captured = capsys.readouterr()
+ assert captured.out == "next\\n"
+ """
+ )
+ result = testdir.runpytest_subprocess("--log-cli-level=INFO")
+ assert result.ret == 0
diff --git a/testing/test_collection.py b/testing/test_collection.py
index 23d82cb141b..5b494ba31af 100644
--- a/testing/test_collection.py
+++ b/testing/test_collection.py
@@ -638,6 +638,10 @@ def test_global_file(self, testdir, tmpdir):
assert col.config is config
def test_pkgfile(self, testdir):
+ """Verify nesting when a module is within a package.
+ The parent chain should match: Module -> Package -> Session.
+ Session's parent should always be None.
+ """
tmpdir = testdir.tmpdir
subdir = tmpdir.join("subdir")
x = subdir.ensure("x.py")
@@ -645,9 +649,12 @@ def test_pkgfile(self, testdir):
with subdir.as_cwd():
config = testdir.parseconfigure(x)
col = testdir.getnode(config, x)
- assert isinstance(col, pytest.Module)
assert col.name == "x.py"
- assert col.parent.parent is None
+ 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
diff --git a/testing/test_compat.py b/testing/test_compat.py
index 399b0d342b5..a6249d14b2e 100644
--- a/testing/test_compat.py
+++ b/testing/test_compat.py
@@ -1,8 +1,11 @@
from __future__ import absolute_import, division, print_function
import sys
+from functools import wraps
+
+import six
import pytest
-from _pytest.compat import is_generator, get_real_func, safe_getattr
+from _pytest.compat import is_generator, get_real_func, safe_getattr, _PytestWrapper
from _pytest.outcomes import OutcomeException
@@ -38,6 +41,33 @@ def __getattr__(self, attr):
print(res)
+def test_get_real_func():
+ """Check that get_real_func correctly unwraps decorators until reaching the real function"""
+
+ def decorator(f):
+ @wraps(f)
+ def inner():
+ pass
+
+ if six.PY2:
+ inner.__wrapped__ = f
+ return inner
+
+ def func():
+ pass
+
+ wrapped_func = decorator(decorator(func))
+ assert get_real_func(wrapped_func) is func
+
+ wrapped_func2 = decorator(decorator(wrapped_func))
+ assert get_real_func(wrapped_func2) is func
+
+ # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point
+ # a function was wrapped by pytest itself
+ wrapped_func2.__pytest_wrapped__ = _PytestWrapper(wrapped_func)
+ assert get_real_func(wrapped_func2) is wrapped_func
+
+
@pytest.mark.skipif(
sys.version_info < (3, 4), reason="asyncio available in Python 3.4+"
)
diff --git a/testing/test_config.py b/testing/test_config.py
index d58bda255cf..f619e7e50ec 100644
--- a/testing/test_config.py
+++ b/testing/test_config.py
@@ -765,6 +765,24 @@ def test_get_plugin_specs_as_list():
assert _get_plugin_specs_as_list(("foo", "bar")) == ["foo", "bar"]
+def test_collect_pytest_prefix_bug_integration(testdir):
+ """Integration test for issue #3775"""
+ p = testdir.copy_example("config/collect_pytest_prefix")
+ result = testdir.runpytest(p)
+ result.stdout.fnmatch_lines("* 1 passed *")
+
+
+def test_collect_pytest_prefix_bug(pytestconfig):
+ """Ensure we collect only actual functions from conftest files (#3775)"""
+
+ class Dummy(object):
+ class pytest_something(object):
+ pass
+
+ pm = pytestconfig.pluginmanager
+ assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None
+
+
class TestWarning(object):
def test_warn_config(self, testdir):
testdir.makeconftest(
diff --git a/testing/test_unittest.py b/testing/test_unittest.py
index 482e8928007..56fdebf4877 100644
--- a/testing/test_unittest.py
+++ b/testing/test_unittest.py
@@ -989,3 +989,24 @@ def test_two(self):
result = testdir.runpytest("-s")
result.assert_outcomes(passed=2)
+
+
+def test_testcase_handles_init_exceptions(testdir):
+ """
+ Regression test to make sure exceptions in the __init__ method are bubbled up correctly.
+ See https://github.com/pytest-dev/pytest/issues/3788
+ """
+ testdir.makepyfile(
+ """
+ from unittest import TestCase
+ import pytest
+ class MyTestCase(TestCase):
+ def __init__(self, *args, **kwargs):
+ raise Exception("should raise this exception")
+ def test_hello(self):
+ pass
+ """
+ )
+ result = testdir.runpytest()
+ assert "should raise this exception" in result.stdout.str()
+ assert "ERROR at teardown of MyTestCase.test_hello" not in result.stdout.str()
diff --git a/testing/test_warnings.py b/testing/test_warnings.py
index 15ec36600d6..a26fb459776 100644
--- a/testing/test_warnings.py
+++ b/testing/test_warnings.py
@@ -287,3 +287,18 @@ def test():
)
result = testdir.runpytest("-W", "always")
result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"])
+
+
+def test_filterwarnings_mark_registration(testdir):
+ """Ensure filterwarnings mark is registered"""
+ testdir.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.filterwarnings('error')
+ def test_func():
+ pass
+ """
+ )
+ result = testdir.runpytest("--strict")
+ assert result.ret == 0
diff --git a/tox.ini b/tox.ini
index a126dbbf13e..6514421b64a 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