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