Merge branch 'pytest-dev:main' into fix_none_failure_with_pytrace_false
This commit is contained in:
commit
e4964ff580
|
@ -1 +0,0 @@
|
||||||
Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
|
|
@ -1 +0,0 @@
|
||||||
Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||||
|
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
|
@ -6,6 +6,7 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.3.1
|
||||||
release-7.3.0
|
release-7.3.0
|
||||||
release-7.2.2
|
release-7.2.2
|
||||||
release-7.2.1
|
release-7.2.1
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
pytest-7.3.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.1 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 https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -28,6 +28,29 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.3.1 (2023-04-14)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
|
||||||
|
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
|
||||||
|
|
||||||
|
|
||||||
pytest 7.3.0 (2023-04-08)
|
pytest 7.3.0 (2023-04-08)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -82,6 +105,7 @@ Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||||
|
NOTE: This change was reverted in version 7.3.1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.3.0
|
pytest 7.3.1
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -412,7 +412,8 @@ class Traceback(List[TracebackEntry]):
|
||||||
return Traceback(filter(fn, self), self._excinfo)
|
return Traceback(filter(fn, self), self._excinfo)
|
||||||
|
|
||||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
def getcrashentry(self) -> Optional[TracebackEntry]:
|
||||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
"""Return last non-hidden traceback entry that lead to the exception of
|
||||||
|
a traceback, or None if all hidden."""
|
||||||
for i in range(-1, -len(self) - 1, -1):
|
for i in range(-1, -len(self) - 1, -1):
|
||||||
entry = self[i]
|
entry = self[i]
|
||||||
if not entry.ishidden():
|
if not entry.ishidden():
|
||||||
|
@ -469,22 +470,41 @@ class ExceptionInfo(Generic[E]):
|
||||||
self._traceback = traceback
|
self._traceback = traceback
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_exc_info(
|
def from_exception(
|
||||||
cls,
|
cls,
|
||||||
exc_info: Tuple[Type[E], E, TracebackType],
|
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||||
|
# This is OK to ignore because this class is (conceptually) readonly.
|
||||||
|
# See https://github.com/python/mypy/issues/7049.
|
||||||
|
exception: E, # type: ignore[misc]
|
||||||
exprinfo: Optional[str] = None,
|
exprinfo: Optional[str] = None,
|
||||||
) -> "ExceptionInfo[E]":
|
) -> "ExceptionInfo[E]":
|
||||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
"""Return an ExceptionInfo for an existing exception.
|
||||||
|
|
||||||
.. warning::
|
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||||
|
otherwise this function fails with an assertion error. This means that
|
||||||
Experimental API
|
the exception must have been raised, or added a traceback with the
|
||||||
|
:py:meth:`~BaseException.with_traceback()` method.
|
||||||
|
|
||||||
:param exprinfo:
|
:param exprinfo:
|
||||||
A text string helping to determine if we should strip
|
A text string helping to determine if we should strip
|
||||||
``AssertionError`` from the output. Defaults to the exception
|
``AssertionError`` from the output. Defaults to the exception
|
||||||
message/``__str__()``.
|
message/``__str__()``.
|
||||||
|
|
||||||
|
.. versionadded:: 7.4
|
||||||
"""
|
"""
|
||||||
|
assert (
|
||||||
|
exception.__traceback__
|
||||||
|
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||||
|
exc_info = (type(exception), exception, exception.__traceback__)
|
||||||
|
return cls.from_exc_info(exc_info, exprinfo)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_exc_info(
|
||||||
|
cls,
|
||||||
|
exc_info: Tuple[Type[E], E, TracebackType],
|
||||||
|
exprinfo: Optional[str] = None,
|
||||||
|
) -> "ExceptionInfo[E]":
|
||||||
|
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||||
_striptext = ""
|
_striptext = ""
|
||||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||||
exprinfo = getattr(exc_info[1], "msg", None)
|
exprinfo = getattr(exc_info[1], "msg", None)
|
||||||
|
@ -605,10 +625,10 @@ class ExceptionInfo(Generic[E]):
|
||||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||||
exconly = self.exconly(tryshort=True)
|
exconly = self.exconly(tryshort=True)
|
||||||
entry = self.traceback.getcrashentry()
|
entry = self.traceback.getcrashentry()
|
||||||
if entry:
|
if entry is None:
|
||||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
return None
|
||||||
return ReprFileLocation(path, lineno + 1, exconly)
|
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||||
return None
|
return ReprFileLocation(path, lineno + 1, exconly)
|
||||||
|
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
|
@ -653,7 +673,9 @@ class ExceptionInfo(Generic[E]):
|
||||||
return ReprExceptionInfo(
|
return ReprExceptionInfo(
|
||||||
reprtraceback=ReprTracebackNative(
|
reprtraceback=ReprTracebackNative(
|
||||||
traceback.format_exception(
|
traceback.format_exception(
|
||||||
self.type, self.value, self.traceback[0]._rawentry
|
self.type,
|
||||||
|
self.value,
|
||||||
|
self.traceback[0]._rawentry if self.traceback else None,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
reprcrash=self._getreprcrash(),
|
reprcrash=self._getreprcrash(),
|
||||||
|
@ -809,12 +831,16 @@ class FormattedExcinfo:
|
||||||
|
|
||||||
def repr_traceback_entry(
|
def repr_traceback_entry(
|
||||||
self,
|
self,
|
||||||
entry: TracebackEntry,
|
entry: Optional[TracebackEntry],
|
||||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||||
) -> "ReprEntry":
|
) -> "ReprEntry":
|
||||||
lines: List[str] = []
|
lines: List[str] = []
|
||||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
style = (
|
||||||
if style in ("short", "long"):
|
entry._repr_style
|
||||||
|
if entry is not None and entry._repr_style is not None
|
||||||
|
else self.style
|
||||||
|
)
|
||||||
|
if style in ("short", "long") and entry is not None:
|
||||||
source = self._getentrysource(entry)
|
source = self._getentrysource(entry)
|
||||||
if source is None:
|
if source is None:
|
||||||
source = Source("???")
|
source = Source("???")
|
||||||
|
@ -863,17 +889,21 @@ class FormattedExcinfo:
|
||||||
else:
|
else:
|
||||||
extraline = None
|
extraline = None
|
||||||
|
|
||||||
|
if not traceback:
|
||||||
|
if extraline is None:
|
||||||
|
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||||
|
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||||
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
last = traceback[-1]
|
last = traceback[-1]
|
||||||
entries = []
|
|
||||||
if self.style == "value":
|
if self.style == "value":
|
||||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||||
entries.append(reprentry)
|
|
||||||
return ReprTraceback(entries, None, style=self.style)
|
return ReprTraceback(entries, None, style=self.style)
|
||||||
|
|
||||||
for index, entry in enumerate(traceback):
|
entries = [
|
||||||
einfo = (last == entry) and excinfo or None
|
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
for entry in traceback
|
||||||
entries.append(reprentry)
|
]
|
||||||
return ReprTraceback(entries, extraline, style=self.style)
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
def _truncate_recursive_traceback(
|
def _truncate_recursive_traceback(
|
||||||
|
@ -930,6 +960,7 @@ class FormattedExcinfo:
|
||||||
seen: Set[int] = set()
|
seen: Set[int] = set()
|
||||||
while e is not None and id(e) not in seen:
|
while e is not None and id(e) not in seen:
|
||||||
seen.add(id(e))
|
seen.add(id(e))
|
||||||
|
|
||||||
if excinfo_:
|
if excinfo_:
|
||||||
# Fall back to native traceback as a temporary workaround until
|
# Fall back to native traceback as a temporary workaround until
|
||||||
# full support for exception groups added to ExceptionInfo.
|
# full support for exception groups added to ExceptionInfo.
|
||||||
|
@ -946,14 +977,9 @@ class FormattedExcinfo:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
reprtraceback = self.repr_traceback(excinfo_)
|
reprtraceback = self.repr_traceback(excinfo_)
|
||||||
|
reprcrash: Optional[ReprFileLocation] = (
|
||||||
# will be None if all traceback entries are hidden
|
excinfo_._getreprcrash() if self.style != "value" else None
|
||||||
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
|
)
|
||||||
if reprcrash:
|
|
||||||
if self.style == "value":
|
|
||||||
repr_chain += [(reprtraceback, None, descr)]
|
|
||||||
else:
|
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
|
||||||
else:
|
else:
|
||||||
# Fallback to native repr if the exception doesn't have a traceback:
|
# Fallback to native repr if the exception doesn't have a traceback:
|
||||||
# ExceptionInfo objects require a full traceback to work.
|
# ExceptionInfo objects require a full traceback to work.
|
||||||
|
@ -961,25 +987,17 @@ class FormattedExcinfo:
|
||||||
traceback.format_exception(type(e), e, None)
|
traceback.format_exception(type(e), e, None)
|
||||||
)
|
)
|
||||||
reprcrash = None
|
reprcrash = None
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||||
|
|
||||||
if e.__cause__ is not None and self.chain:
|
if e.__cause__ is not None and self.chain:
|
||||||
e = e.__cause__
|
e = e.__cause__
|
||||||
excinfo_ = (
|
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
||||||
if e.__traceback__
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
descr = "The above exception was the direct cause of the following exception:"
|
descr = "The above exception was the direct cause of the following exception:"
|
||||||
elif (
|
elif (
|
||||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||||
):
|
):
|
||||||
e = e.__context__
|
e = e.__context__
|
||||||
excinfo_ = (
|
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
||||||
if e.__traceback__
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
descr = "During handling of the above exception, another exception occurred:"
|
descr = "During handling of the above exception, another exception occurred:"
|
||||||
else:
|
else:
|
||||||
e = None
|
e = None
|
||||||
|
@ -1158,8 +1176,8 @@ class ReprEntry(TerminalRepr):
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
if self.style == "short":
|
if self.style == "short":
|
||||||
assert self.reprfileloc is not None
|
if self.reprfileloc:
|
||||||
self.reprfileloc.toterminal(tw)
|
self.reprfileloc.toterminal(tw)
|
||||||
self._write_entry_lines(tw)
|
self._write_entry_lines(tw)
|
||||||
if self.reprlocals:
|
if self.reprlocals:
|
||||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||||
|
|
|
@ -452,10 +452,7 @@ class Node(metaclass=NodeMeta):
|
||||||
if self.config.getoption("fulltrace", False):
|
if self.config.getoption("fulltrace", False):
|
||||||
style = "long"
|
style = "long"
|
||||||
else:
|
else:
|
||||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
|
||||||
self._prunetraceback(excinfo)
|
self._prunetraceback(excinfo)
|
||||||
if len(excinfo.traceback) == 0:
|
|
||||||
excinfo.traceback = tb
|
|
||||||
if style == "auto":
|
if style == "auto":
|
||||||
style = "long"
|
style = "long"
|
||||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||||
|
|
|
@ -353,7 +353,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||||
yield path
|
yield path
|
||||||
|
|
||||||
|
|
||||||
def cleanup_dead_symlink(root: Path):
|
def cleanup_dead_symlinks(root: Path):
|
||||||
for left_dir in root.iterdir():
|
for left_dir in root.iterdir():
|
||||||
if left_dir.is_symlink():
|
if left_dir.is_symlink():
|
||||||
if not left_dir.resolve().exists():
|
if not left_dir.resolve().exists():
|
||||||
|
@ -371,7 +371,7 @@ def cleanup_numbered_dir(
|
||||||
for path in root.glob("garbage-*"):
|
for path in root.glob("garbage-*"):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
|
|
||||||
cleanup_dead_symlink(root)
|
cleanup_dead_symlinks(root)
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir_with_cleanup(
|
def make_numbered_dir_with_cleanup(
|
||||||
|
|
|
@ -950,11 +950,7 @@ def raises( # noqa: F811
|
||||||
try:
|
try:
|
||||||
func(*args[1:], **kwargs)
|
func(*args[1:], **kwargs)
|
||||||
except expected_exception as e:
|
except expected_exception as e:
|
||||||
# We just caught the exception - there is a traceback.
|
return _pytest._code.ExceptionInfo.from_exception(e)
|
||||||
assert e.__traceback__ is not None
|
|
||||||
return _pytest._code.ExceptionInfo.from_exc_info(
|
|
||||||
(type(e), e, e.__traceback__)
|
|
||||||
)
|
|
||||||
fail(message)
|
fail(message)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -347,10 +347,9 @@ class TestReport(BaseReport):
|
||||||
elif isinstance(excinfo.value, skip.Exception):
|
elif isinstance(excinfo.value, skip.Exception):
|
||||||
outcome = "skipped"
|
outcome = "skipped"
|
||||||
r = excinfo._getreprcrash()
|
r = excinfo._getreprcrash()
|
||||||
if r is None:
|
assert (
|
||||||
raise ValueError(
|
r is not None
|
||||||
"There should always be a traceback entry for skipping a test."
|
), "There should always be a traceback entry for skipping a test."
|
||||||
)
|
|
||||||
if excinfo.value._use_item_location:
|
if excinfo.value._use_item_location:
|
||||||
path, line = item.reportinfo()[:2]
|
path, line = item.reportinfo()[:2]
|
||||||
assert line is not None
|
assert line is not None
|
||||||
|
|
|
@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
|
||||||
from .pathlib import make_numbered_dir
|
from .pathlib import make_numbered_dir
|
||||||
from .pathlib import make_numbered_dir_with_cleanup
|
from .pathlib import make_numbered_dir_with_cleanup
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
from .pathlib import cleanup_dead_symlink
|
from .pathlib import cleanup_dead_symlinks
|
||||||
from _pytest.compat import final, get_user_id
|
from _pytest.compat import final, get_user_id
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -289,31 +289,30 @@ def tmp_path(
|
||||||
|
|
||||||
del request.node.stash[tmppath_result_key]
|
del request.node.stash[tmppath_result_key]
|
||||||
|
|
||||||
# remove dead symlink
|
|
||||||
basetemp = tmp_path_factory._basetemp
|
|
||||||
if basetemp is None:
|
|
||||||
return
|
|
||||||
cleanup_dead_symlink(basetemp)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||||
"""After each session, remove base directory if all the tests passed,
|
"""After each session, remove base directory if all the tests passed,
|
||||||
the policy is "failed", and the basetemp is not specified by a user.
|
the policy is "failed", and the basetemp is not specified by a user.
|
||||||
"""
|
"""
|
||||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||||
if tmp_path_factory._basetemp is None:
|
basetemp = tmp_path_factory._basetemp
|
||||||
|
if basetemp is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
policy = tmp_path_factory._retention_policy
|
policy = tmp_path_factory._retention_policy
|
||||||
if (
|
if (
|
||||||
exitstatus == 0
|
exitstatus == 0
|
||||||
and policy == "failed"
|
and policy == "failed"
|
||||||
and tmp_path_factory._given_basetemp is None
|
and tmp_path_factory._given_basetemp is None
|
||||||
):
|
):
|
||||||
passed_dir = tmp_path_factory._basetemp
|
if basetemp.is_dir():
|
||||||
if passed_dir.exists():
|
|
||||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||||
# permissions, etc, in which case we ignore it.
|
# permissions, etc, in which case we ignore it.
|
||||||
rmtree(passed_dir, ignore_errors=True)
|
rmtree(basetemp, ignore_errors=True)
|
||||||
|
|
||||||
|
# Remove dead symlinks.
|
||||||
|
if basetemp.is_dir():
|
||||||
|
cleanup_dead_symlinks(basetemp)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
|
|
@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
|
||||||
assert info.type == ValueError
|
assert info.type == ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def test_excinfo_from_exception_simple() -> None:
|
||||||
|
try:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError as e:
|
||||||
|
assert e.__traceback__ is not None
|
||||||
|
info = _pytest._code.ExceptionInfo.from_exception(e)
|
||||||
|
assert info.type == ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def test_excinfo_from_exception_missing_traceback_assertion() -> None:
|
||||||
|
with pytest.raises(AssertionError, match=r"must have.*__traceback__"):
|
||||||
|
_pytest._code.ExceptionInfo.from_exception(ValueError())
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_getstatement():
|
def test_excinfo_getstatement():
|
||||||
def g():
|
def g():
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
@ -310,9 +324,7 @@ class TestTraceback_f_g_h:
|
||||||
g()
|
g()
|
||||||
|
|
||||||
excinfo = pytest.raises(ValueError, f)
|
excinfo = pytest.raises(ValueError, f)
|
||||||
tb = excinfo.traceback
|
assert excinfo.traceback.getcrashentry() is None
|
||||||
entry = tb.getcrashentry()
|
|
||||||
assert entry is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_exconly():
|
def test_excinfo_exconly():
|
||||||
|
@ -1573,3 +1585,21 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
|
||||||
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
||||||
pytest.importorskip("exceptiongroup")
|
pytest.importorskip("exceptiongroup")
|
||||||
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
|
||||||
|
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
|
||||||
|
"""Regression test for #10903."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test():
|
||||||
|
__tracebackhide__ = True
|
||||||
|
1 / 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("--tb", tbstyle)
|
||||||
|
assert result.ret == 1
|
||||||
|
if tbstyle != "line":
|
||||||
|
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
|
||||||
|
if tbstyle not in ("line", "native"):
|
||||||
|
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
def test_tbh_chained(testdir):
|
|
||||||
"""Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904)."""
|
|
||||||
p = testdir.makepyfile(
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
def f1():
|
|
||||||
__tracebackhide__ = True
|
|
||||||
try:
|
|
||||||
return f1.meh
|
|
||||||
except AttributeError:
|
|
||||||
pytest.fail("fail")
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def fix():
|
|
||||||
f1()
|
|
||||||
|
|
||||||
|
|
||||||
def test(fix):
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = testdir.runpytest(str(p))
|
|
||||||
assert "'function' object has no attribute 'meh'" not in result.stdout.str()
|
|
||||||
assert result.ret == 1
|
|
Loading…
Reference in New Issue