Merge remote-tracking branch 'upstream/master' into merge-master-into-features

This commit is contained in:
Bruno Oliveira 2018-08-18 15:54:53 -03:00
commit c64a8c9c7f
38 changed files with 457 additions and 77 deletions

View File

@ -182,6 +182,7 @@ Russel Winder
Ryan Wooden Ryan Wooden
Samuel Dion-Girardeau Samuel Dion-Girardeau
Samuele Pedroni Samuele Pedroni
Sankt Petersbug
Segev Finer Segev Finer
Serhii Mozghovyi Serhii Mozghovyi
Simon Gomizelj Simon Gomizelj
@ -205,6 +206,7 @@ Trevor Bekolay
Tyler Goodlet Tyler Goodlet
Tzu-ping Chung Tzu-ping Chung
Vasily Kuznetsov Vasily Kuznetsov
Victor Maryama
Victor Uriarte Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Virgil Dupras Virgil Dupras

View File

@ -1,3 +1,13 @@
=================
Changelog history
=================
Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.<patch>``).
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 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 file is managed by towncrier. You *may* edit previous change logs to
@ -8,6 +18,40 @@
.. towncrier release notes start .. towncrier release notes start
pytest 3.7.2 (2018-08-16)
=========================
Bug Fixes
---------
- `#3671 <https://github.com/pytest-dev/pytest/issues/3671>`_: Fix ``filterwarnings`` not being registered as a builtin mark.
- `#3768 <https://github.com/pytest-dev/pytest/issues/3768>`_, `#3789 <https://github.com/pytest-dev/pytest/issues/3789>`_: Fix test collection from packages mixed with normal directories.
- `#3771 <https://github.com/pytest-dev/pytest/issues/3771>`_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` hook returns ``False`` instead of ``None``.
- `#3774 <https://github.com/pytest-dev/pytest/issues/3774>`_: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``).
- `#3775 <https://github.com/pytest-dev/pytest/issues/3775>`_: Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``.
- `#3788 <https://github.com/pytest-dev/pytest/issues/3788>`_: Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``.
- `#3804 <https://github.com/pytest-dev/pytest/issues/3804>`_: Fix traceback reporting for exceptions with ``__cause__`` cycles.
Improved Documentation
----------------------
- `#3746 <https://github.com/pytest-dev/pytest/issues/3746>`_: Add documentation for ``metafunc.config`` that had been mistakenly hidden.
pytest 3.7.1 (2018-08-02) pytest 3.7.1 (2018-08-02)
========================= =========================

View File

@ -0,0 +1 @@
Fix ``stdout/stderr`` not getting captured when real-time cli logging is active.

View File

@ -0,0 +1 @@
Replace broken type annotations with type comments.

View File

@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-3.7.2
release-3.7.1 release-3.7.1
release-3.7.0 release-3.7.0
release-3.6.4 release-3.6.4

View File

@ -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

View File

@ -1,7 +1,4 @@
.. _changelog: .. _changelog:
Changelog history
=================================
.. include:: ../../CHANGELOG.rst .. include:: ../../CHANGELOG.rst

View File

@ -200,6 +200,8 @@ You can ask which markers exist for your test suite - the list includes our just
$ pytest --markers $ pytest --markers
@pytest.mark.webtest: mark a test as a webtest. @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.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 @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 --markers
@pytest.mark.env(name): mark test to run only on named environment @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.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 @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

View File

@ -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 platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR/nonpython, inifile: rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
collected 2 items collected 2 items
<YamlFile 'test_simple.yml'> <Package '$REGENDOC_TMPDIR/nonpython'>
<YamlItem 'hello'> <YamlFile 'test_simple.yml'>
<YamlItem 'ok'> <YamlItem 'hello'>
<YamlItem 'ok'>
======================= no tests ran in 0.12 seconds ======================= ======================= no tests ran in 0.12 seconds =======================

View File

@ -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):: 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 . $ pytest -rs -q multipython.py
...ssssssssssssssssssssssss [100%] ...sss...sssssssss...sss... [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found
SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.5' not found 12 passed, 15 skipped in 0.12 seconds
3 passed, 24 skipped in 0.12 seconds
Indirect parametrization of optional implementations/imports Indirect parametrization of optional implementations/imports
-------------------------------------------------------------------- --------------------------------------------------------------------

View File

@ -719,7 +719,9 @@ class FormattedExcinfo(object):
repr_chain = [] repr_chain = []
e = excinfo.value e = excinfo.value
descr = None 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: if excinfo:
reprtraceback = self.repr_traceback(excinfo) reprtraceback = self.repr_traceback(excinfo)
reprcrash = excinfo._getreprcrash() reprcrash = excinfo._getreprcrash()

View File

@ -14,8 +14,7 @@ from tempfile import TemporaryFile
import six import six
import pytest import pytest
from _pytest.compat import CaptureIO from _pytest.compat import CaptureIO, dummy_context_manager
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
@ -85,6 +84,7 @@ class CaptureManager(object):
def __init__(self, method): def __init__(self, method):
self._method = method self._method = method
self._global_capturing = None self._global_capturing = None
self._current_item = None
def _getcapture(self, method): def _getcapture(self, method):
if method == "fd": if method == "fd":
@ -121,6 +121,19 @@ class CaptureManager(object):
cap.suspend_capturing(in_=in_) cap.suspend_capturing(in_=in_)
return outerr 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): def activate_fixture(self, item):
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
the global capture. the global capture.
@ -151,28 +164,34 @@ class CaptureManager(object):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item): def pytest_runtest_setup(self, item):
self._current_item = item
self.resume_global_capture() self.resume_global_capture()
# no need to activate a capture fixture because they activate themselves during creation; this # 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 # only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
# be activated during pytest_runtest_call # be activated during pytest_runtest_call
yield yield
self.suspend_capture_item(item, "setup") self.suspend_capture_item(item, "setup")
self._current_item = None
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item): def pytest_runtest_call(self, item):
self._current_item = item
self.resume_global_capture() self.resume_global_capture()
# it is important to activate this fixture during the call phase so it overwrites the "global" # it is important to activate this fixture during the call phase so it overwrites the "global"
# capture # capture
self.activate_fixture(item) self.activate_fixture(item)
yield yield
self.suspend_capture_item(item, "call") self.suspend_capture_item(item, "call")
self._current_item = None
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item): def pytest_runtest_teardown(self, item):
self._current_item = item
self.resume_global_capture() self.resume_global_capture()
self.activate_fixture(item) self.activate_fixture(item)
yield yield
self.suspend_capture_item(item, "teardown") self.suspend_capture_item(item, "teardown")
self._current_item = None
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo): def pytest_keyboard_interrupt(self, excinfo):
@ -314,17 +333,21 @@ class CaptureFixture(object):
return self._outerr return self._outerr
@contextlib.contextmanager @contextlib.contextmanager
def disabled(self): def _suspend(self):
"""Temporarily disables capture while inside the 'with' block.""" """Suspends this fixture's own capturing temporarily."""
self._capture.suspend_capturing() self._capture.suspend_capturing()
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
capmanager.suspend_global_capture(item=None, in_=False)
try: try:
yield yield
finally: finally:
capmanager.resume_global_capture()
self._capture.resume_capturing() 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"): def safe_text_dupfile(f, mode, default_encoding="UTF8"):
""" return an open text file object that's a duplicate of f on the """ return an open text file object that's a duplicate of f on the

View File

@ -8,6 +8,7 @@ import functools
import inspect import inspect
import re import re
import sys import sys
from contextlib import contextmanager
import py import py
@ -151,6 +152,13 @@ def getfuncargnames(function, is_method=False, cls=None):
return arg_names 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): def get_default_arg_names(function):
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames, # 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 # to get the arguments which were excluded from its result because they had default values
@ -228,12 +236,31 @@ else:
return val.encode("unicode-escape") 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): def get_real_func(obj):
""" gets the real function object of the (possibly) wrapped object by """ gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial. functools.wraps or functools.partial.
""" """
start_obj = obj start_obj = obj
for i in range(100): 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) new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None: if new_obj is None:
break break

View File

@ -1,6 +1,7 @@
""" command line options, ini-file and conftest.py processing. """ """ command line options, ini-file and conftest.py processing. """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import argparse import argparse
import inspect
import shlex import shlex
import traceback import traceback
import types import types
@ -252,6 +253,10 @@ class PytestPluginManager(PluginManager):
method = getattr(plugin, name) method = getattr(plugin, name)
opts = super(PytestPluginManager, self).parse_hookimpl_opts(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 # collect unmarked hooks as long as they have the `pytest_' prefix
if opts is None and name.startswith("pytest_"): if opts is None and name.startswith("pytest_"):
opts = {} opts = {}

View File

@ -174,23 +174,23 @@ class Argument(object):
if isinstance(typ, six.string_types): if isinstance(typ, six.string_types):
if typ == "choice": if typ == "choice":
warnings.warn( warnings.warn(
"type argument to addoption() is a string %r." "`type` argument to addoption() is the string %r."
" For parsearg this is optional and when supplied" " For choices this is optional and can be omitted, "
" should be a type." " but when supplied should be a type (for example `str` or `int`)."
" (options: %s)" % (typ, names), " (options: %s)" % (typ, names),
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=4,
) )
# argparse expects a type here take it from # argparse expects a type here take it from
# the type of the first element # the type of the first element
attrs["type"] = type(attrs["choices"][0]) attrs["type"] = type(attrs["choices"][0])
else: else:
warnings.warn( warnings.warn(
"type argument to addoption() is a string %r." "`type` argument to addoption() is the string %r, "
" For parsearg this should be a type." " but when supplied should be a type (for example `str` or `int`)."
" (options: %s)" % (typ, names), " (options: %s)" % (typ, names),
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=4,
) )
attrs["type"] = Argument._typ_map[typ] attrs["type"] = Argument._typ_map[typ]
# used in test_parseopt -> test_parse_defaultgetter # used in test_parseopt -> test_parse_defaultgetter

View File

@ -31,6 +31,7 @@ from _pytest.compat import (
safe_getattr, safe_getattr,
FuncargnamesCompatAttr, FuncargnamesCompatAttr,
get_real_method, get_real_method,
_PytestWrapper,
) )
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME 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 # fixture names specified via usefixtures and via autouse=True in fixture
# definitions. # definitions.
initialnames = attr.ib(type=tuple) initialnames = attr.ib(type=tuple)
names_closure = attr.ib(type="List[str]") names_closure = attr.ib() # type: List[str]
name2fixturedefs = attr.ib(type="List[str, List[FixtureDef]]") name2fixturedefs = attr.ib() # type: List[str, List[FixtureDef]]
def prune_dependency_tree(self): def prune_dependency_tree(self):
"""Recompute names_closure from initialnames and name2fixturedefs """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): 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 """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. 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) is_yield_function = is_generator(function)
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
@ -982,6 +980,10 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
if six.PY2: if six.PY2:
result.__wrapped__ = function 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 return result

View File

@ -6,6 +6,7 @@ from contextlib import closing, contextmanager
import re import re
import six import six
from _pytest.compat import dummy_context_manager
from _pytest.config import create_terminal_writer from _pytest.config import create_terminal_writer
import pytest import pytest
import py import py
@ -369,11 +370,6 @@ def pytest_configure(config):
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
@contextmanager
def _dummy_context_manager():
yield
class LoggingPlugin(object): class LoggingPlugin(object):
"""Attaches to the logging module and captures log messages for each test. """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 log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
) )
else: else:
self.live_logs_context = _dummy_context_manager() self.live_logs_context = dummy_context_manager()
class _LiveLoggingStreamHandler(logging.StreamHandler): class _LiveLoggingStreamHandler(logging.StreamHandler):
@ -572,9 +568,12 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
self._test_outcome_written = False self._test_outcome_written = False
def emit(self, record): def emit(self, record):
if self.capture_manager is not None: ctx_manager = (
self.capture_manager.suspend_global_capture() self.capture_manager.global_and_fixture_disabled()
try: if self.capture_manager
else dummy_context_manager()
)
with ctx_manager:
if not self._first_record_emitted: if not self._first_record_emitted:
self.stream.write("\n") self.stream.write("\n")
self._first_record_emitted = True self._first_record_emitted = True
@ -586,6 +585,3 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
self.stream.section("live log " + self._when, sep="-", bold=True) self.stream.section("live log " + self._when, sep="-", bold=True)
self._section_name_shown = True self._section_name_shown = True
logging.StreamHandler.emit(self, record) logging.StreamHandler.emit(self, record)
finally:
if self.capture_manager is not None:
self.capture_manager.resume_global_capture()

View File

@ -505,8 +505,9 @@ class Session(nodes.FSCollector):
root = self._node_cache[pkginit] root = self._node_cache[pkginit]
else: else:
col = root._collectfile(pkginit) col = root._collectfile(pkginit)
if col and isinstance(col, Package): if col:
root = col[0] if isinstance(col[0], Package):
root = col[0]
self._node_cache[root.fspath] = root self._node_cache[root.fspath] = root
# If it's a directory argument, recurse and look for any Subpackages. # If it's a directory argument, recurse and look for any Subpackages.

View File

@ -216,18 +216,6 @@ def pytest_pycollect_makemodule(path, parent):
return Module(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) @hookimpl(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield outcome = yield
@ -554,14 +542,12 @@ class Package(Module):
self.name = fspath.dirname self.name = fspath.dirname
self.trace = session.trace self.trace = session.trace
self._norecursepatterns = session._norecursepatterns self._norecursepatterns = session._norecursepatterns
for path in list(session.config.pluginmanager._duplicatepaths): self.fspath = fspath
if path.dirname == fspath.dirname and path != fspath:
session.config.pluginmanager._duplicatepaths.remove(path)
def _recurse(self, path): def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath()) ihook = self.gethookproxy(path.dirpath())
if ihook.pytest_ignore_collect(path=path, config=self.config): if ihook.pytest_ignore_collect(path=path, config=self.config):
return return False
for pat in self._norecursepatterns: for pat in self._norecursepatterns:
if path.check(fnmatch=pat): if path.check(fnmatch=pat):
return False return False
@ -594,9 +580,21 @@ class Package(Module):
return path in self.session._initialpaths return path in self.session._initialpaths
def collect(self): 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 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(): if pkg_prefix and pkg_prefix in path.parts():
continue continue
for x in self._collectfile(path): for x in self._collectfile(path):
@ -880,12 +878,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
""" """
def __init__(self, definition, fixtureinfo, config, cls=None, module=None): def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
#: access to the :class:`_pytest.config.Config` object for the test session
assert ( assert (
isinstance(definition, FunctionDefinition) isinstance(definition, FunctionDefinition)
or type(definition).__name__ == "DefinitionMock" or type(definition).__name__ == "DefinitionMock"
) )
self.definition = definition self.definition = definition
#: access to the :class:`_pytest.config.Config` object for the test session
self.config = config self.config = config
#: the module object where the test function is defined in. #: the module object where the test function is defined in.

View File

@ -69,6 +69,7 @@ class UnitTestCase(Class):
class TestCaseFunction(Function): class TestCaseFunction(Function):
nofuncargs = True nofuncargs = True
_excinfo = None _excinfo = None
_testcase = None
def setup(self): def setup(self):
self._testcase = self.parent.obj(self.name) self._testcase = self.parent.obj(self.name)

View File

@ -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 @contextmanager
def catch_warnings_for_item(item): def catch_warnings_for_item(item):
""" """

View File

@ -1044,3 +1044,10 @@ def test_frame_leak_on_failing_test(testdir):
) )
result = testdir.runpytest_subprocess() result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) 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*")

View File

@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function
import operator import operator
import os import os
import sys import sys
import textwrap
import _pytest import _pytest
import py import py
import pytest 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("style", ["short", "long"])
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])

View File

@ -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"

View File

@ -0,0 +1,2 @@
def pytest_ignore_collect(path):
return False

View File

@ -0,0 +1,2 @@
def test():
pass

View File

@ -0,0 +1,2 @@
class pytest_something(object):
pass

View File

@ -0,0 +1,2 @@
def test_foo():
pass

View File

@ -876,6 +876,7 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
is installed. is installed.
""" """
import logging import logging
import contextlib
from functools import partial from functools import partial
from _pytest.capture import CaptureManager from _pytest.capture import CaptureManager
from _pytest.logging import _LiveLoggingStreamHandler from _pytest.logging import _LiveLoggingStreamHandler
@ -883,11 +884,11 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
class MockCaptureManager: class MockCaptureManager:
calls = [] calls = []
def suspend_global_capture(self): @contextlib.contextmanager
self.calls.append("suspend_global_capture") def global_and_fixture_disabled(self):
self.calls.append("enter disabled")
def resume_global_capture(self): yield
self.calls.append("resume_global_capture") self.calls.append("exit disabled")
# sanity check # sanity check
assert CaptureManager.suspend_capture_item assert CaptureManager.suspend_capture_item
@ -908,10 +909,7 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
logger.critical("some message") logger.critical("some message")
if has_capture_manager: if has_capture_manager:
assert MockCaptureManager.calls == [ assert MockCaptureManager.calls == ["enter disabled", "exit disabled"]
"suspend_global_capture",
"resume_global_capture",
]
else: else:
assert MockCaptureManager.calls == [] assert MockCaptureManager.calls == []
assert out_file.getvalue() == "\nsome message\n" assert out_file.getvalue() == "\nsome message\n"

View File

@ -1577,3 +1577,49 @@ def test_keep_duplicates(testdir):
) )
result = testdir.runpytest("--keep-duplicates", a.strpath, a.strpath) result = testdir.runpytest("--keep-duplicates", a.strpath, a.strpath)
result.stdout.fnmatch_lines(["*collected 2 item*"]) 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)

View File

@ -1385,3 +1385,34 @@ def test_pickling_and_unpickling_encoded_file():
ef = capture.EncodedFile(None, None) ef = capture.EncodedFile(None, None)
ef_as_str = pickle.dumps(ef) ef_as_str = pickle.dumps(ef)
pickle.loads(ef_as_str) 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

View File

@ -638,6 +638,10 @@ class Test_getinitialnodes(object):
assert col.config is config assert col.config is config
def test_pkgfile(self, testdir): def test_pkgfile(self, testdir):
"""Verify nesting when a module is within a package.
The parent chain should match: Module<x.py> -> Package<subdir> -> Session.
Session's parent should always be None.
"""
tmpdir = testdir.tmpdir tmpdir = testdir.tmpdir
subdir = tmpdir.join("subdir") subdir = tmpdir.join("subdir")
x = subdir.ensure("x.py") x = subdir.ensure("x.py")
@ -645,9 +649,12 @@ class Test_getinitialnodes(object):
with subdir.as_cwd(): with subdir.as_cwd():
config = testdir.parseconfigure(x) config = testdir.parseconfigure(x)
col = testdir.getnode(config, x) col = testdir.getnode(config, x)
assert isinstance(col, pytest.Module)
assert col.name == "x.py" 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(): for col in col.listchain():
assert col.config is config assert col.config is config

View File

@ -1,8 +1,11 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys import sys
from functools import wraps
import six
import pytest 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 from _pytest.outcomes import OutcomeException
@ -38,6 +41,33 @@ def test_real_func_loop_limit():
print(res) 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( @pytest.mark.skipif(
sys.version_info < (3, 4), reason="asyncio available in Python 3.4+" sys.version_info < (3, 4), reason="asyncio available in Python 3.4+"
) )

View File

@ -765,6 +765,24 @@ def test_get_plugin_specs_as_list():
assert _get_plugin_specs_as_list(("foo", "bar")) == ["foo", "bar"] 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): class TestWarning(object):
def test_warn_config(self, testdir): def test_warn_config(self, testdir):
testdir.makeconftest( testdir.makeconftest(

View File

@ -989,3 +989,24 @@ def test_usefixtures_marker_on_unittest(base, testdir):
result = testdir.runpytest("-s") result = testdir.runpytest("-s")
result.assert_outcomes(passed=2) 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()

View File

@ -287,3 +287,18 @@ def test_non_string_warning_argument(testdir):
) )
result = testdir.runpytest("-W", "always") result = testdir.runpytest("-W", "always")
result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"]) 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

View File

@ -115,8 +115,6 @@ skipsdist = True
usedevelop = True usedevelop = True
changedir = doc/en changedir = doc/en
deps = deps =
attrs
more-itertools
PyYAML PyYAML
sphinx sphinx
sphinxcontrib-trio sphinxcontrib-trio