Merge branch 'master' into fix-flaky-test

This commit is contained in:
Bruno Oliveira
2020-06-02 11:33:15 -03:00
committed by GitHub
70 changed files with 1579 additions and 1734 deletions

View File

@@ -46,7 +46,7 @@ if TYPE_CHECKING:
from typing_extensions import Literal
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native"]
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value"]
class Code:
@@ -268,10 +268,6 @@ class TracebackEntry:
return tbh
def __str__(self) -> str:
try:
fn = str(self.path)
except py.error.Error:
fn = "???"
name = self.frame.code.name
try:
line = str(self.statement).lstrip()
@@ -279,7 +275,7 @@ class TracebackEntry:
raise
except BaseException:
line = "???"
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
return " File %r:%d in %s\n %s\n" % (self.path, self.lineno + 1, name, line)
@property
def name(self) -> str:
@@ -587,7 +583,7 @@ class ExceptionInfo(Generic[_E]):
Show locals per traceback entry.
Ignored if ``style=="native"``.
:param str style: long|short|no|native traceback style
:param str style: long|short|no|native|value traceback style
:param bool abspath:
If paths should be changed to absolute or left unchanged.
@@ -762,16 +758,15 @@ class FormattedExcinfo:
def repr_traceback_entry(
self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None
) -> "ReprEntry":
source = self._getentrysource(entry)
if source is None:
source = Source("???")
line_index = 0
else:
line_index = entry.lineno - entry.getfirstlinesource()
lines = [] # type: List[str]
style = entry._repr_style if entry._repr_style is not None else self.style
if style in ("short", "long"):
source = self._getentrysource(entry)
if source is None:
source = Source("???")
line_index = 0
else:
line_index = entry.lineno - entry.getfirstlinesource()
short = style == "short"
reprargs = self.repr_args(entry) if not short else None
s = self.get_source(source, line_index, excinfo, short=short)
@@ -784,9 +779,14 @@ class FormattedExcinfo:
reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
localsrepr = self.repr_locals(entry.locals)
return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
if excinfo:
lines.extend(self.get_exconly(excinfo, indent=4))
return ReprEntry(lines, None, None, None, style)
elif style == "value":
if excinfo:
lines.extend(str(excinfo.value).split("\n"))
return ReprEntry(lines, None, None, None, style)
else:
if excinfo:
lines.extend(self.get_exconly(excinfo, indent=4))
return ReprEntry(lines, None, None, None, style)
def _makepath(self, path):
if not self.abspath:
@@ -810,6 +810,11 @@ class FormattedExcinfo:
last = traceback[-1]
entries = []
if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo)
entries.append(reprentry)
return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback):
einfo = (last == entry) and excinfo or None
reprentry = self.repr_traceback_entry(entry, einfo)
@@ -869,7 +874,9 @@ class FormattedExcinfo:
seen.add(id(e))
if excinfo_:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation]
reprcrash = (
excinfo_._getreprcrash() if self.style != "value" else None
) # type: Optional[ReprFileLocation]
else:
# fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work
@@ -1052,8 +1059,11 @@ class ReprEntry(TerminalRepr):
"Unexpected failure lines between source lines:\n"
+ "\n".join(self.lines)
)
indents.append(line[:indent_size])
source_lines.append(line[indent_size:])
if self.style == "value":
source_lines.append(line)
else:
indents.append(line[:indent_size])
source_lines.append(line[indent_size:])
else:
seeing_failures = True
failure_lines.append(line)

View File

@@ -2,12 +2,12 @@
import os
import shutil
import sys
import unicodedata
from functools import lru_cache
from typing import Optional
from typing import Sequence
from typing import TextIO
from .wcwidth import wcswidth
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
@@ -22,17 +22,6 @@ def get_terminal_width() -> int:
return width
@lru_cache(100)
def char_width(c: str) -> int:
# Fullwidth and Wide -> 2, all else (including Ambiguous) -> 1.
return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1
def get_line_width(text: str) -> int:
text = unicodedata.normalize("NFC", text)
return sum(char_width(c) for c in text)
def should_do_markup(file: TextIO) -> bool:
if os.environ.get("PY_COLORS") == "1":
return True
@@ -99,7 +88,7 @@ class TerminalWriter:
@property
def width_of_current_line(self) -> int:
"""Return an estimate of the width so far in the current line."""
return get_line_width(self._current_line)
return wcswidth(self._current_line)
def markup(self, text: str, **markup: bool) -> str:
for name in markup:

View File

@@ -0,0 +1,55 @@
import unicodedata
from functools import lru_cache
@lru_cache(100)
def wcwidth(c: str) -> int:
"""Determine how many columns are needed to display a character in a terminal.
Returns -1 if the character is not printable.
Returns 0, 1 or 2 for other characters.
"""
o = ord(c)
# ASCII fast path.
if 0x20 <= o < 0x07F:
return 1
# Some Cf/Zp/Zl characters which should be zero-width.
if (
o == 0x0000
or 0x200B <= o <= 0x200F
or 0x2028 <= o <= 0x202E
or 0x2060 <= o <= 0x2063
):
return 0
category = unicodedata.category(c)
# Control characters.
if category == "Cc":
return -1
# Combining characters with zero width.
if category in ("Me", "Mn"):
return 0
# Full/Wide east asian characters.
if unicodedata.east_asian_width(c) in ("F", "W"):
return 2
return 1
def wcswidth(s: str) -> int:
"""Determine how many columns are needed to display a string in a terminal.
Returns -1 if the string contains non-printable characters.
"""
width = 0
for c in unicodedata.normalize("NFC", s):
wc = wcwidth(c)
if wc < 0:
return -1
width += wc
return width

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,6 @@ from typing import Union
import attr
import py
from packaging.version import Version
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
@@ -1031,6 +1030,7 @@ class Config:
self.known_args_namespace = ns = self._parser.parse_known_args(
args, namespace=copy.copy(self.option)
)
self._validatekeys()
if self.known_args_namespace.confcutdir is None and self.inifile:
confcutdir = py.path.local(self.inifile).dirname
self.known_args_namespace.confcutdir = confcutdir
@@ -1059,6 +1059,9 @@ class Config:
minver = self.inicfg.get("minversion", None)
if minver:
# Imported lazily to improve start-up time.
from packaging.version import Version
if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError(
"%s:%d: requires pytest-%s, actual pytest-%s'"
@@ -1070,6 +1073,17 @@ class Config:
)
)
def _validatekeys(self):
for key in self._get_unknown_ini_keys():
message = "Unknown config ini key: {}\n".format(key)
if self.known_args_namespace.strict_config:
fail(message, pytrace=False)
sys.stderr.write("WARNING: {}".format(message))
def _get_unknown_ini_keys(self) -> List[str]:
parser_inicfg = self._parser._inidict
return [name for name in self.inicfg if name not in parser_inicfg]
def parse(self, args: List[str], addopts: bool = True) -> None:
# parse given cmdline arguments into this config object.
assert not hasattr(

View File

@@ -4,6 +4,7 @@ import functools
import sys
from _pytest import outcomes
from _pytest.config import ConftestImportFailure
from _pytest.config import hookimpl
from _pytest.config.exceptions import UsageError
@@ -338,6 +339,10 @@ def _postmortem_traceback(excinfo):
# A doctest.UnexpectedException is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.exc_info[2]
elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.excinfo[2]
else:
return excinfo._excinfo[2]

View File

@@ -80,3 +80,8 @@ MINUS_K_COLON = PytestDeprecationWarning(
"The `-k 'expr:'` syntax to -k is deprecated.\n"
"Please open an issue if you use this and want a replacement."
)
WARNING_CAPTURED_HOOK = PytestDeprecationWarning(
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
"Please use pytest_warning_recorded instead."
)

View File

@@ -41,8 +41,11 @@ def pytest_addoption(parser):
group.addoption(
"--version",
"-V",
action="store_true",
help="display pytest version and information about plugins.",
action="count",
default=0,
dest="version",
help="display pytest version and information about plugins."
"When given twice, also display information about plugins.",
)
group._addoption(
"-h",
@@ -116,19 +119,22 @@ def pytest_cmdline_parse():
def showversion(config):
sys.stderr.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
if config.option.version > 1:
sys.stderr.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
)
)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
else:
sys.stderr.write("pytest {}\n".format(pytest.__version__))
def pytest_cmdline_main(config):
if config.option.version:
if config.option.version > 0:
showversion(config)
return 0
elif config.option.help:

View File

@@ -8,9 +8,11 @@ from typing import Union
from pluggy import HookspecMarker
from .deprecated import COLLECT_DIRECTORY_HOOK
from .deprecated import WARNING_CAPTURED_HOOK
from _pytest.compat import TYPE_CHECKING
if TYPE_CHECKING:
import warnings
from _pytest.config import Config
from _pytest.main import Session
from _pytest.reports import BaseReport
@@ -620,8 +622,40 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""
@hookspec(historic=True)
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
def pytest_warning_captured(warning_message, when, item, location):
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
This hook is considered deprecated and will be removed in a future pytest version.
Use :func:`pytest_warning_recorded` instead.
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
the same attributes as the parameters of :py:func:`warnings.showwarning`.
:param str when:
Indicates when the warning was captured. Possible values:
* ``"config"``: during pytest configuration/initialization stage.
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
:param tuple location:
Holds information about the execution context of the captured warning (filename, linenumber, function).
``function`` evaluates to <module> when the execution context is at the module level.
"""
@hookspec(historic=True)
def pytest_warning_recorded(
warning_message: "warnings.WarningMessage",
when: str,
nodeid: str,
location: Tuple[str, int, str],
):
"""
Process a warning captured by the internal pytest warnings plugin.
@@ -636,11 +670,7 @@ def pytest_warning_captured(warning_message, when, item, location):
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
**DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None``
in a future release.
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
:param str nodeid: full id of the item
:param tuple location:
Holds information about the execution context of the captured warning (filename, linenumber, function).

View File

@@ -202,10 +202,8 @@ class _NodeReporter:
if hasattr(report, "wasxfail"):
self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly")
else:
if hasattr(report.longrepr, "reprcrash"):
if getattr(report.longrepr, "reprcrash", None) is not None:
message = report.longrepr.reprcrash.message
elif isinstance(report.longrepr, str):
message = report.longrepr
else:
message = str(report.longrepr)
message = bin_xml_escape(message)

View File

@@ -312,6 +312,14 @@ class LogCaptureHandler(logging.StreamHandler):
self.records = []
self.stream = StringIO()
def handleError(self, record: logging.LogRecord) -> None:
if logging.raiseExceptions:
# Fail the test if the log message is bad (emit failed).
# The default behavior of logging is to print "Logging error"
# to stderr with the call stack and some extra details.
# pytest wants to make such mistakes visible during testing.
raise
class LogCaptureFixture:
"""Provides access and control of log capturing."""
@@ -499,9 +507,7 @@ class LoggingPlugin:
# File logging.
self.log_file_level = get_log_level_for_setting(config, "log_file_level")
log_file = get_option_ini(config, "log_file") or os.devnull
self.log_file_handler = logging.FileHandler(
log_file, mode="w", encoding="UTF-8"
)
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
log_file_format = get_option_ini(config, "log_file_format", "log_format")
log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format"
@@ -687,6 +693,16 @@ class LoggingPlugin:
self.log_file_handler.close()
class _FileHandler(logging.FileHandler):
"""
Custom FileHandler with pytest tweaks.
"""
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
pass
class _LiveLoggingStreamHandler(logging.StreamHandler):
"""
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
@@ -737,6 +753,10 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
self._section_name_shown = True
super().emit(record)
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
pass
class _LiveLoggingNullHandler(logging.NullHandler):
"""A handler used when live logging is disabled."""
@@ -746,3 +766,7 @@ class _LiveLoggingNullHandler(logging.NullHandler):
def set_when(self, when):
pass
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
pass

View File

@@ -70,6 +70,11 @@ def pytest_addoption(parser):
default=0,
help="exit after first num failures or errors.",
)
group._addoption(
"--strict-config",
action="store_true",
help="invalid ini keys for the `pytest` section of the configuration file raise errors.",
)
group._addoption(
"--strict-markers",
"--strict",

View File

@@ -19,6 +19,7 @@ from _pytest._code.code import ReprExceptionInfo
from _pytest.compat import cached_property
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import PytestPluginManager
from _pytest.deprecated import NODE_USE_FROM_PARENT
from _pytest.fixtures import FixtureDef
@@ -28,7 +29,7 @@ from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail
from _pytest.outcomes import Failed
from _pytest.pathlib import Path
from _pytest.store import Store
if TYPE_CHECKING:
@@ -331,11 +332,13 @@ class Node(metaclass=NodeMeta):
pass
def _repr_failure_py(
self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None
self, excinfo: ExceptionInfo[BaseException], style=None,
) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]:
if isinstance(excinfo.value, ConftestImportFailure):
excinfo = ExceptionInfo(excinfo.value.excinfo)
if isinstance(excinfo.value, fail.Exception):
if not excinfo.value.pytrace:
return str(excinfo.value)
style = "value"
if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr()
if self.config.getoption("fulltrace", False):
@@ -359,9 +362,14 @@ class Node(metaclass=NodeMeta):
else:
truncate_locals = True
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
# It is possible for a fixture/test to change the CWD while this code runs, which
# would then result in the user seeing confusing paths in the failure message.
# To fix this, if the CWD changed, always display the full absolute path.
# It will be better to just always display paths relative to invocation_dir, but
# this requires a lot of plumbing (#6428).
try:
os.getcwd()
abspath = False
abspath = Path(os.getcwd()) != Path(self.config.invocation_dir)
except OSError:
abspath = True
@@ -456,10 +464,7 @@ def _check_initialpaths_for_relpath(session, fspath):
class FSHookProxy:
def __init__(
self, fspath: py.path.local, pm: PytestPluginManager, remove_mods
) -> None:
self.fspath = fspath
def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
self.pm = pm
self.remove_mods = remove_mods
@@ -510,7 +515,7 @@ class FSCollector(Collector):
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods:
# one or more conftests are not in use at this fspath
proxy = FSHookProxy(fspath, pm, remove_mods)
proxy = FSHookProxy(pm, remove_mods)
else:
# all plugins are active for this fspath
proxy = self.config.hook

View File

@@ -9,8 +9,6 @@ from typing import cast
from typing import Optional
from typing import TypeVar
from packaging.version import Version
TYPE_CHECKING = False # avoid circular import through compat
if TYPE_CHECKING:
@@ -217,6 +215,9 @@ def importorskip(
return mod
verattr = getattr(mod, "__version__", None)
if minversion is not None:
# Imported lazily to improve start-up time.
from packaging.version import Version
if verattr is None or Version(verattr) < Version(minversion):
raise Skipped(
"module %r has __version__ %r, required is: %r"

View File

@@ -100,10 +100,41 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
return True
def ensure_extended_length_path(path: Path) -> Path:
"""Get the extended-length version of a path (Windows).
On Windows, by default, the maximum length of a path (MAX_PATH) is 260
characters, and operations on paths longer than that fail. But it is possible
to overcome this by converting the path to "extended-length" form before
performing the operation:
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
On Windows, this function returns the extended-length absolute version of path.
On other platforms it returns path unchanged.
"""
if sys.platform.startswith("win32"):
path = path.resolve()
path = Path(get_extended_length_path_str(str(path)))
return path
def get_extended_length_path_str(path: str) -> str:
"""Converts to extended length path as a str"""
long_path_prefix = "\\\\?\\"
unc_long_path_prefix = "\\\\?\\UNC\\"
if path.startswith((long_path_prefix, unc_long_path_prefix)):
return path
# UNC
if path.startswith("\\\\"):
return unc_long_path_prefix + path[2:]
return long_path_prefix + path
def rm_rf(path: Path) -> None:
"""Remove the path contents recursively, even if some elements
are read-only.
"""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror)
@@ -220,6 +251,7 @@ def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
def maybe_delete_a_numbered_dir(path: Path) -> None:
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
path = ensure_extended_length_path(path)
lock_path = None
try:
lock_path = create_cleanup_lock(path)

View File

@@ -25,8 +25,7 @@ import py
import pytest
from _pytest import timing
from _pytest._code import Source
from _pytest.capture import MultiCapture
from _pytest.capture import SysCapture
from _pytest.capture import _get_multicapture
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
@@ -687,11 +686,41 @@ class Testdir:
return py.iniconfig.IniConfig(p)["pytest"]
def makepyfile(self, *args, **kwargs):
"""Shortcut for .makefile() with a .py extension."""
r"""Shortcut for .makefile() with a .py extension.
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
existing files.
Examples:
.. code-block:: python
def test_something(testdir):
# initial file is created test_something.py
testdir.makepyfile("foobar")
# to create multiple files, pass kwargs accordingly
testdir.makepyfile(custom="foobar")
# at this point, both 'test_something.py' & 'custom.py' exist in the test directory
"""
return self._makefile(".py", args, kwargs)
def maketxtfile(self, *args, **kwargs):
"""Shortcut for .makefile() with a .txt extension."""
r"""Shortcut for .makefile() with a .txt extension.
Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
existing files.
Examples:
.. code-block:: python
def test_something(testdir):
# initial file is created test_something.txt
testdir.maketxtfile("foobar")
# to create multiple files, pass kwargs accordingly
testdir.maketxtfile(custom="foobar")
# at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory
"""
return self._makefile(".txt", args, kwargs)
def syspathinsert(self, path=None):
@@ -942,7 +971,7 @@ class Testdir:
if syspathinsert:
self.syspathinsert()
now = timing.time()
capture = MultiCapture(Capture=SysCapture)
capture = _get_multicapture("sys")
capture.start_capturing()
try:
try:

View File

@@ -27,6 +27,7 @@ import pytest
from _pytest import nodes
from _pytest import timing
from _pytest._io import TerminalWriter
from _pytest._io.wcwidth import wcswidth
from _pytest.compat import order_preserving_dict
from _pytest.config import Config
from _pytest.config import ExitCode
@@ -227,7 +228,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
@attr.s
class WarningReport:
"""
Simple structure to hold warnings information captured by ``pytest_warning_captured``.
Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
:ivar str message: user friendly message about the warning
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
@@ -411,14 +412,12 @@ class TerminalReporter:
self.write_line("INTERNALERROR> " + line)
return 1
def pytest_warning_captured(self, warning_message, item):
# from _pytest.nodes import get_fslocation_from_item
def pytest_warning_recorded(self, warning_message, nodeid):
from _pytest.warnings import warning_record_to_str
fslocation = warning_message.filename, warning_message.lineno
message = warning_record_to_str(warning_message)
nodeid = item.nodeid if item is not None else ""
warning_report = WarningReport(
fslocation=fslocation, message=message, nodeid=nodeid
)
@@ -443,8 +442,7 @@ class TerminalReporter:
self.write_ensure_prefix(line, "")
self.flush()
elif self.showfspath:
fsid = nodeid.split("::")[0]
self.write_fspath_result(fsid, "")
self.write_fspath_result(nodeid, "")
self.flush()
def pytest_runtest_logreport(self, report: TestReport) -> None:
@@ -474,10 +472,7 @@ class TerminalReporter:
else:
markup = {}
if self.verbosity <= 0:
if not running_xdist and self.showfspath:
self.write_fspath_result(rep.nodeid, letter, **markup)
else:
self._tw.write(letter, **markup)
self._tw.write(letter, **markup)
else:
self._progress_nodeids_reported.add(rep.nodeid)
line = self._locationline(rep.nodeid, *rep.location)
@@ -1126,8 +1121,6 @@ def _get_pos(config, rep):
def _get_line_with_reprcrash_message(config, rep, termwidth):
"""Get summary line for a report, trying to add reprcrash message."""
from wcwidth import wcswidth
verbose_word = rep._get_verbose_word(config)
pos = _get_pos(config, rep)

View File

@@ -41,7 +41,7 @@ class UnitTestCase(Class):
if not getattr(cls, "__test__", True):
return
skipped = getattr(cls, "__unittest_skip__", False)
skipped = _is_skipped(cls)
if not skipped:
self._inject_setup_teardown_fixtures(cls)
self._inject_setup_class_fixture()
@@ -89,7 +89,7 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
@pytest.fixture(scope=scope, autouse=True)
def fixture(self, request):
if getattr(self, "__unittest_skip__", None):
if _is_skipped(self):
reason = self.__unittest_skip_why__
pytest.skip(reason)
if setup is not None:
@@ -220,7 +220,7 @@ class TestCaseFunction(Function):
# arguably we could always postpone tearDown(), but this changes the moment where the
# TestCase instance interacts with the results object, so better to only do it
# when absolutely needed
if self.config.getoption("usepdb"):
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
self._explicit_tearDown = self._testcase.tearDown
setattr(self._testcase, "tearDown", lambda *args: None)
@@ -301,3 +301,8 @@ def check_testcase_implements_trial_reporter(done=[]):
classImplements(TestCaseFunction, IReporter)
done.append(1)
def _is_skipped(obj) -> bool:
"""Return True if the given object has been marked with @unittest.skip"""
return bool(getattr(obj, "__unittest_skip__", False))

View File

@@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item):
``item`` can be None if we are not in the context of an item execution.
Each warning captured triggers the ``pytest_warning_captured`` hook.
Each warning captured triggers the ``pytest_warning_recorded`` hook.
"""
cmdline_filters = config.getoption("pythonwarnings") or []
inifilters = config.getini("filterwarnings")
@@ -102,6 +102,7 @@ def catch_warnings_for_item(config, ihook, when, item):
for arg in cmdline_filters:
warnings.filterwarnings(*_parse_filter(arg, escape=True))
nodeid = "" if item is None else item.nodeid
if item is not None:
for mark in item.iter_markers(name="filterwarnings"):
for arg in mark.args:
@@ -113,6 +114,14 @@ def catch_warnings_for_item(config, ihook, when, item):
ihook.pytest_warning_captured.call_historic(
kwargs=dict(warning_message=warning_message, when=when, item=item)
)
ihook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=warning_message,
nodeid=nodeid,
when=when,
location=None,
)
)
def warning_record_to_str(warning_message):
@@ -166,7 +175,7 @@ def pytest_sessionfinish(session):
def _issue_warning_captured(warning, hook, stacklevel):
"""
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
:param warning: the warning instance.
@@ -185,3 +194,8 @@ def _issue_warning_captured(warning, hook, stacklevel):
warning_message=records[0], when="config", item=None, location=location
)
)
hook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=records[0], when="config", nodeid="", location=location
)
)