Compare commits
2 Commits
7.3.1
...
7.4.0.dev0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec8e23951d | ||
|
|
bf47357511 |
1
AUTHORS
1
AUTHORS
@@ -128,6 +128,7 @@ Erik M. Bray
|
||||
Evan Kepner
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
|
||||
@@ -6,7 +6,6 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.3.1
|
||||
release-7.3.0
|
||||
release-7.2.2
|
||||
release-7.2.1
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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,29 +28,6 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. 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)
|
||||
=========================
|
||||
|
||||
@@ -105,7 +82,6 @@ Bug Fixes
|
||||
|
||||
|
||||
- `#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.
|
||||
|
||||
|
||||
|
||||
@@ -119,12 +95,7 @@ Improved Documentation
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
|
||||
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
|
||||
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
|
||||
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
|
||||
|
||||
Nice diffs for ``attrs`` classes are still supported though.
|
||||
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
|
||||
|
||||
|
||||
pytest 7.2.2 (2023-03-03)
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.3.1
|
||||
pytest 7.3.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]):
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self) -> TracebackEntry:
|
||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
||||
for i in range(-1, -len(self) - 1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
return self[-1]
|
||||
return None
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
@@ -602,11 +602,13 @@ class ExceptionInfo(Generic[E]):
|
||||
"""
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self) -> "ReprFileLocation":
|
||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
if entry:
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
return None
|
||||
|
||||
def getrepr(
|
||||
self,
|
||||
@@ -944,9 +946,14 @@ class FormattedExcinfo:
|
||||
)
|
||||
else:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
reprcrash: Optional[ReprFileLocation] = (
|
||||
excinfo_._getreprcrash() if self.style != "value" else None
|
||||
)
|
||||
|
||||
# will be None if all traceback entries are hidden
|
||||
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
|
||||
if reprcrash:
|
||||
if self.style == "value":
|
||||
repr_chain += [(reprtraceback, None, descr)]
|
||||
else:
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -954,8 +961,8 @@ class FormattedExcinfo:
|
||||
traceback.format_exception(type(e), e, None)
|
||||
)
|
||||
reprcrash = None
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo_ = (
|
||||
@@ -1046,7 +1053,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
reprtraceback: "ReprTraceback"
|
||||
reprcrash: "ReprFileLocation"
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
self.reprtraceback.toterminal(tw)
|
||||
|
||||
@@ -6,7 +6,6 @@ import itertools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import uuid
|
||||
import warnings
|
||||
from enum import Enum
|
||||
@@ -29,8 +28,6 @@ from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -66,33 +63,21 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
return path.joinpath(".lock")
|
||||
|
||||
|
||||
def on_rm_rf_error(
|
||||
func,
|
||||
path: str,
|
||||
excinfo: Union[
|
||||
BaseException,
|
||||
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
|
||||
],
|
||||
*,
|
||||
start_path: Path,
|
||||
) -> bool:
|
||||
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||
"""Handle known read-only errors during rmtree.
|
||||
|
||||
The returned value is used only by our own tests.
|
||||
"""
|
||||
if isinstance(excinfo, BaseException):
|
||||
exc = excinfo
|
||||
else:
|
||||
exc = excinfo[1]
|
||||
exctype, excvalue = exc[:2]
|
||||
|
||||
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
||||
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
if isinstance(excvalue, FileNotFoundError):
|
||||
return False
|
||||
|
||||
if not isinstance(exc, PermissionError):
|
||||
if not isinstance(excvalue, PermissionError):
|
||||
warnings.warn(
|
||||
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
|
||||
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -101,7 +86,7 @@ def on_rm_rf_error(
|
||||
warnings.warn(
|
||||
PytestWarning(
|
||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||
func, path, type(exc), exc
|
||||
func, path, exctype, excvalue
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -164,10 +149,7 @@ def rm_rf(path: Path) -> None:
|
||||
are read-only."""
|
||||
path = ensure_extended_length_path(path)
|
||||
onerror = partial(on_rm_rf_error, start_path=path)
|
||||
if sys.version_info >= (3, 12):
|
||||
shutil.rmtree(str(path), onexc=onerror)
|
||||
else:
|
||||
shutil.rmtree(str(path), onerror=onerror)
|
||||
shutil.rmtree(str(path), onerror=onerror)
|
||||
|
||||
|
||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||
@@ -353,7 +335,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_dead_symlinks(root: Path):
|
||||
def cleanup_dead_symlink(root: Path):
|
||||
for left_dir in root.iterdir():
|
||||
if left_dir.is_symlink():
|
||||
if not left_dir.resolve().exists():
|
||||
@@ -371,7 +353,7 @@ def cleanup_numbered_dir(
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
cleanup_dead_symlinks(root)
|
||||
cleanup_dead_symlink(root)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(
|
||||
|
||||
@@ -347,6 +347,10 @@ class TestReport(BaseReport):
|
||||
elif isinstance(excinfo.value, skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
if r is None:
|
||||
raise ValueError(
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
if excinfo.value._use_item_location:
|
||||
path, line = item.reportinfo()[:2]
|
||||
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_with_cleanup
|
||||
from .pathlib import rm_rf
|
||||
from .pathlib import cleanup_dead_symlinks
|
||||
from .pathlib import cleanup_dead_symlink
|
||||
from _pytest.compat import final, get_user_id
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
@@ -289,30 +289,31 @@ def tmp_path(
|
||||
|
||||
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]):
|
||||
"""After each session, remove base directory if all the tests passed,
|
||||
the policy is "failed", and the basetemp is not specified by a user.
|
||||
"""
|
||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||
basetemp = tmp_path_factory._basetemp
|
||||
if basetemp is None:
|
||||
if tmp_path_factory._basetemp is None:
|
||||
return
|
||||
|
||||
policy = tmp_path_factory._retention_policy
|
||||
if (
|
||||
exitstatus == 0
|
||||
and policy == "failed"
|
||||
and tmp_path_factory._given_basetemp is None
|
||||
):
|
||||
if basetemp.is_dir():
|
||||
passed_dir = tmp_path_factory._basetemp
|
||||
if passed_dir.exists():
|
||||
# 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.
|
||||
rmtree(basetemp, ignore_errors=True)
|
||||
|
||||
# Remove dead symlinks.
|
||||
if basetemp.is_dir():
|
||||
cleanup_dead_symlinks(basetemp)
|
||||
rmtree(passed_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
|
||||
@@ -298,9 +298,6 @@ class TestCaseFunction(Function):
|
||||
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
||||
pass
|
||||
|
||||
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
|
||||
pass
|
||||
|
||||
def runtest(self) -> None:
|
||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||
|
||||
|
||||
@@ -294,6 +294,7 @@ class TestTraceback_f_g_h:
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
assert entry is not None
|
||||
co = _pytest._code.Code.from_function(h)
|
||||
assert entry.frame.code.path == co.path
|
||||
assert entry.lineno == co.firstlineno + 1
|
||||
@@ -311,10 +312,7 @@ class TestTraceback_f_g_h:
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
co = _pytest._code.Code.from_function(g)
|
||||
assert entry.frame.code.path == co.path
|
||||
assert entry.lineno == co.firstlineno + 2
|
||||
assert entry.frame.code.name == "g"
|
||||
assert entry is None
|
||||
|
||||
|
||||
def test_excinfo_exconly():
|
||||
@@ -1575,20 +1573,3 @@ 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
|
||||
pytest.importorskip("exceptiongroup")
|
||||
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
||||
|
||||
|
||||
def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None:
|
||||
"""Regression test for #10903.
|
||||
|
||||
We're not really sure what should be *displayed* here, so this test
|
||||
just verified that at least it doesn't crash.
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test():
|
||||
__tracebackhide__ = True
|
||||
1 / 0
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
|
||||
@@ -512,20 +512,20 @@ class TestRmRf:
|
||||
|
||||
# unknown exception
|
||||
with pytest.warns(pytest.PytestWarning):
|
||||
exc_info1 = (RuntimeError, RuntimeError(), None)
|
||||
exc_info1 = (None, RuntimeError(), None)
|
||||
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
|
||||
assert fn.is_file()
|
||||
|
||||
# we ignore FileNotFoundError
|
||||
exc_info2 = (FileNotFoundError, FileNotFoundError(), None)
|
||||
exc_info2 = (None, FileNotFoundError(), None)
|
||||
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
|
||||
|
||||
# unknown function
|
||||
with pytest.warns(
|
||||
pytest.PytestWarning,
|
||||
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\n<class 'PermissionError'>: ",
|
||||
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ",
|
||||
):
|
||||
exc_info3 = (PermissionError, PermissionError(), None)
|
||||
exc_info3 = (None, PermissionError(), None)
|
||||
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
|
||||
assert fn.is_file()
|
||||
|
||||
@@ -533,12 +533,12 @@ class TestRmRf:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with pytest.warns(None) as warninfo: # type: ignore[call-overload]
|
||||
exc_info4 = PermissionError()
|
||||
exc_info4 = (None, PermissionError(), None)
|
||||
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
|
||||
assert fn.is_file()
|
||||
assert not [x.message for x in warninfo]
|
||||
|
||||
exc_info5 = PermissionError()
|
||||
exc_info5 = (None, PermissionError(), None)
|
||||
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
|
||||
assert not fn.is_file()
|
||||
|
||||
|
||||
25
testing/test_tracebackhide.py
Normal file
25
testing/test_tracebackhide.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
Reference in New Issue
Block a user