Merge pull request #3831 from nicoddemus/merge-master-into-features
Merge master into features
This commit is contained in:
commit
a6cdd0d9da
2
AUTHORS
2
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
|
||||
|
|
|
@ -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
|
||||
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 <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)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fix ``stdout/stderr`` not getting captured when real-time cli logging is active.
|
|
@ -0,0 +1 @@
|
|||
Replace broken type annotations with type comments.
|
|
@ -6,6 +6,7 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.7.2
|
||||
release-3.7.1
|
||||
release-3.7.0
|
||||
release-3.6.4
|
||||
|
|
|
@ -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
|
|
@ -1,7 +1,4 @@
|
|||
|
||||
.. _changelog:
|
||||
|
||||
Changelog history
|
||||
=================================
|
||||
|
||||
.. include:: ../../CHANGELOG.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
|
||||
|
|
|
@ -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
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
<Package '$REGENDOC_TMPDIR/nonpython'>
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
|
|
@ -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
|
||||
--------------------------------------------------------------------
|
||||
|
|
|
@ -719,7 +719,9 @@ class FormattedExcinfo(object):
|
|||
repr_chain = []
|
||||
e = excinfo.value
|
||||
descr = None
|
||||
while e is not None:
|
||||
seen = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
if excinfo:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
|
|
|
@ -14,8 +14,7 @@ from tempfile import TemporaryFile
|
|||
|
||||
import six
|
||||
import pytest
|
||||
from _pytest.compat import CaptureIO
|
||||
|
||||
from _pytest.compat import CaptureIO, dummy_context_manager
|
||||
|
||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||
|
||||
|
@ -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 @@ class CaptureManager(object):
|
|||
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 @@ class CaptureManager(object):
|
|||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
self._current_item = item
|
||||
self.resume_global_capture()
|
||||
# no need to activate a capture fixture because they activate themselves during creation; this
|
||||
# only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
|
||||
# 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 @@ class CaptureFixture(object):
|
|||
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
|
||||
|
|
|
@ -8,6 +8,7 @@ import functools
|
|||
import inspect
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
import py
|
||||
|
||||
|
@ -151,6 +152,13 @@ def getfuncargnames(function, is_method=False, cls=None):
|
|||
return arg_names
|
||||
|
||||
|
||||
@contextmanager
|
||||
def dummy_context_manager():
|
||||
"""Context manager that does nothing, useful in situations where you might need an actual context manager or not
|
||||
depending on some condition. Using this allow to keep the same code"""
|
||||
yield
|
||||
|
||||
|
||||
def get_default_arg_names(function):
|
||||
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
|
||||
# to get the arguments which were excluded from its result because they had default values
|
||||
|
@ -228,12 +236,31 @@ else:
|
|||
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
|
||||
|
|
|
@ -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 @@ class PytestPluginManager(PluginManager):
|
|||
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 = {}
|
||||
|
|
|
@ -174,23 +174,23 @@ class Argument(object):
|
|||
if isinstance(typ, six.string_types):
|
||||
if typ == "choice":
|
||||
warnings.warn(
|
||||
"type argument to addoption() is a string %r."
|
||||
" For parsearg this is optional and when supplied"
|
||||
" should be a type."
|
||||
"`type` argument to addoption() is the string %r."
|
||||
" For choices this is optional and can be omitted, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: %s)" % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
stacklevel=4,
|
||||
)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs["type"] = type(attrs["choices"][0])
|
||||
else:
|
||||
warnings.warn(
|
||||
"type argument to addoption() is a string %r."
|
||||
" For parsearg this should be a type."
|
||||
"`type` argument to addoption() is the string %r, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: %s)" % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
stacklevel=4,
|
||||
)
|
||||
attrs["type"] = Argument._typ_map[typ]
|
||||
# used in test_parseopt -> test_parse_defaultgetter
|
||||
|
|
|
@ -31,6 +31,7 @@ from _pytest.compat import (
|
|||
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 wrap_function_to_warning_if_called_directly(function, fixture_marker):
|
|||
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
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from contextlib import closing, contextmanager
|
|||
import re
|
||||
import six
|
||||
|
||||
from _pytest.compat import dummy_context_manager
|
||||
from _pytest.config import create_terminal_writer
|
||||
import pytest
|
||||
import py
|
||||
|
@ -369,11 +370,6 @@ def pytest_configure(config):
|
|||
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _dummy_context_manager():
|
||||
yield
|
||||
|
||||
|
||||
class LoggingPlugin(object):
|
||||
"""Attaches to the logging module and captures log messages for each test.
|
||||
"""
|
||||
|
@ -537,7 +533,7 @@ class LoggingPlugin(object):
|
|||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
else:
|
||||
self.live_logs_context = _dummy_context_manager()
|
||||
self.live_logs_context = dummy_context_manager()
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
|
@ -572,9 +568,12 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
|||
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 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
|||
self.stream.section("live log " + self._when, sep="-", bold=True)
|
||||
self._section_name_shown = True
|
||||
logging.StreamHandler.emit(self, record)
|
||||
finally:
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.resume_global_capture()
|
||||
|
|
|
@ -505,8 +505,9 @@ class Session(nodes.FSCollector):
|
|||
root = self._node_cache[pkginit]
|
||||
else:
|
||||
col = root._collectfile(pkginit)
|
||||
if col and isinstance(col, Package):
|
||||
root = col[0]
|
||||
if col:
|
||||
if isinstance(col[0], Package):
|
||||
root = col[0]
|
||||
self._node_cache[root.fspath] = root
|
||||
|
||||
# If it's a directory argument, recurse and look for any Subpackages.
|
||||
|
|
|
@ -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 @@ class Package(Module):
|
|||
self.name = fspath.dirname
|
||||
self.trace = session.trace
|
||||
self._norecursepatterns = session._norecursepatterns
|
||||
for path in list(session.config.pluginmanager._duplicatepaths):
|
||||
if path.dirname == fspath.dirname and path != fspath:
|
||||
session.config.pluginmanager._duplicatepaths.remove(path)
|
||||
self.fspath = fspath
|
||||
|
||||
def _recurse(self, path):
|
||||
ihook = self.gethookproxy(path.dirpath())
|
||||
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 @@ class Package(Module):
|
|||
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.
|
||||
|
|
|
@ -69,6 +69,7 @@ class UnitTestCase(Class):
|
|||
class TestCaseFunction(Function):
|
||||
nofuncargs = True
|
||||
_excinfo = None
|
||||
_testcase = None
|
||||
|
||||
def setup(self):
|
||||
self._testcase = self.parent.obj(self.name)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -1044,3 +1044,10 @@ def test_frame_leak_on_failing_test(testdir):
|
|||
)
|
||||
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*")
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function
|
|||
import operator
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import _pytest
|
||||
import py
|
||||
import pytest
|
||||
|
@ -1265,6 +1266,50 @@ raise ValueError()
|
|||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif("sys.version_info[0] < 3")
|
||||
def test_exc_chain_repr_cycle(self, importasmod):
|
||||
mod = importasmod(
|
||||
"""
|
||||
class Err(Exception):
|
||||
pass
|
||||
def fail():
|
||||
return 0 / 0
|
||||
def reraise():
|
||||
try:
|
||||
fail()
|
||||
except ZeroDivisionError as e:
|
||||
raise Err() from e
|
||||
def unreraise():
|
||||
try:
|
||||
reraise()
|
||||
except Err as e:
|
||||
raise e.__cause__
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
|
||||
r = excinfo.getrepr(style="short")
|
||||
tw = TWMock()
|
||||
r.toterminal(tw)
|
||||
out = "\n".join(line for line in tw.lines if isinstance(line, str))
|
||||
expected_out = textwrap.dedent(
|
||||
"""\
|
||||
:13: in unreraise
|
||||
reraise()
|
||||
:10: in reraise
|
||||
raise Err() from e
|
||||
E test_exc_chain_repr_cycle0.mod.Err
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
:15: in unreraise
|
||||
raise e.__cause__
|
||||
:8: in reraise
|
||||
fail()
|
||||
:5: in fail
|
||||
return 0 / 0
|
||||
E ZeroDivisionError: division by zero"""
|
||||
)
|
||||
assert out == expected_out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("style", ["short", "long"])
|
||||
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
|
||||
|
|
|
@ -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"
|
|
@ -0,0 +1,2 @@
|
|||
def pytest_ignore_collect(path):
|
||||
return False
|
|
@ -0,0 +1,2 @@
|
|||
def test():
|
||||
pass
|
|
@ -0,0 +1,2 @@
|
|||
class pytest_something(object):
|
||||
pass
|
|
@ -0,0 +1,2 @@
|
|||
def test_foo():
|
||||
pass
|
|
@ -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 test_live_logging_suspends_capture(has_capture_manager, request):
|
|||
|
||||
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"
|
||||
|
|
|
@ -1577,3 +1577,49 @@ def test_keep_duplicates(testdir):
|
|||
)
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -638,6 +638,10 @@ class Test_getinitialnodes(object):
|
|||
assert col.config is config
|
||||
|
||||
def test_pkgfile(self, testdir):
|
||||
"""Verify nesting when a module is within a package.
|
||||
The parent chain should match: Module<x.py> -> Package<subdir> -> Session.
|
||||
Session's parent should always be None.
|
||||
"""
|
||||
tmpdir = testdir.tmpdir
|
||||
subdir = tmpdir.join("subdir")
|
||||
x = subdir.ensure("x.py")
|
||||
|
@ -645,9 +649,12 @@ class Test_getinitialnodes(object):
|
|||
with subdir.as_cwd():
|
||||
config = testdir.parseconfigure(x)
|
||||
col = testdir.getnode(config, x)
|
||||
assert isinstance(col, pytest.Module)
|
||||
assert col.name == "x.py"
|
||||
assert 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
|
||||
|
||||
|
|
|
@ -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 test_real_func_loop_limit():
|
|||
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+"
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -989,3 +989,24 @@ def test_usefixtures_marker_on_unittest(base, testdir):
|
|||
|
||||
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()
|
||||
|
|
|
@ -287,3 +287,18 @@ def test_non_string_warning_argument(testdir):
|
|||
)
|
||||
result = testdir.runpytest("-W", "always")
|
||||
result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"])
|
||||
|
||||
|
||||
def test_filterwarnings_mark_registration(testdir):
|
||||
"""Ensure filterwarnings mark is registered"""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.mark.filterwarnings('error')
|
||||
def test_func():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--strict")
|
||||
assert result.ret == 0
|
||||
|
|
Loading…
Reference in New Issue