Merge branch 'master' into fix-flaky-test
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
55
src/_pytest/_io/wcwidth.py
Normal file
55
src/_pytest/_io/wcwidth.py
Normal 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
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user