From 9c2247ec1b9566266478e6f561cad30079533ed5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 12:09:35 +0300 Subject: [PATCH 1/6] code: drop Experimental API label from ExceptionInfo.from_exc_info This API is OK, I don't think we're going to change something about it at this point. --- src/_pytest/_code/code.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 032b83beb..6fd1100df 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -476,10 +476,6 @@ class ExceptionInfo(Generic[E]): ) -> "ExceptionInfo[E]": """Return an ExceptionInfo for an existing exc_info tuple. - .. warning:: - - Experimental API - :param exprinfo: A text string helping to determine if we should strip ``AssertionError`` from the output. Defaults to the exception From 424c3eebde1192b671767359c23b6344c795dc70 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 12:24:59 +0300 Subject: [PATCH 2/6] code: add ExceptionInfo.from_exception The old-style `sys.exc_info()` triplet is redundant nowadays with `(type(exc), exc, exc.__traceback__)`, and is beginning to get soft-deprecated in Python 3.12. Add a nicer API to ExceptionInfo which takes just the exc instead of the triplet. There are already a few internal uses which benefit. --- changelog/10901.feature.rst | 2 ++ src/_pytest/_code/code.py | 43 ++++++++++++++++++++++-------------- src/_pytest/python_api.py | 6 +---- testing/code/test_excinfo.py | 14 ++++++++++++ 4 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 changelog/10901.feature.rst diff --git a/changelog/10901.feature.rst b/changelog/10901.feature.rst new file mode 100644 index 000000000..0d99d66f6 --- /dev/null +++ b/changelog/10901.feature.rst @@ -0,0 +1,2 @@ +Added :func:`ExceptionInfo.from_exception() `, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception. +This can replace :func:`ExceptionInfo.from_exc_info() ` for most uses. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 032b83beb..e2451bcdf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -469,22 +469,41 @@ class ExceptionInfo(Generic[E]): self._traceback = traceback @classmethod - def from_exc_info( + def from_exception( 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, ) -> "ExceptionInfo[E]": - """Return an ExceptionInfo for an existing exc_info tuple. + """Return an ExceptionInfo for an existing exception. - .. warning:: - - Experimental API + The exception must have a non-``None`` ``__traceback__`` attribute, + otherwise this function fails with an assertion error. This means that + the exception must have been raised, or added a traceback with the + :py:meth:`~BaseException.with_traceback()` method. :param exprinfo: A text string helping to determine if we should strip ``AssertionError`` from the output. Defaults to the exception 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 = "" if exprinfo is None and isinstance(exc_info[1], AssertionError): exprinfo = getattr(exc_info[1], "msg", None) @@ -965,21 +984,13 @@ class FormattedExcinfo: if e.__cause__ is not None and self.chain: e = e.__cause__ - excinfo_ = ( - ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) - if e.__traceback__ - else None - ) + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None descr = "The above exception was the direct cause of the following exception:" elif ( e.__context__ is not None and not e.__suppress_context__ and self.chain ): e = e.__context__ - excinfo_ = ( - ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) - if e.__traceback__ - else None - ) + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None descr = "During handling of the above exception, another exception occurred:" else: e = None diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index b03a251ab..4213bd098 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -950,11 +950,7 @@ def raises( # noqa: F811 try: func(*args[1:], **kwargs) except expected_exception as e: - # We just caught the exception - there is a traceback. - assert e.__traceback__ is not None - return _pytest._code.ExceptionInfo.from_exc_info( - (type(e), e, e.__traceback__) - ) + return _pytest._code.ExceptionInfo.from_exception(e) fail(message) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 918c97276..8ac8a42ab 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None: 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 g(): raise ValueError From 90412827c3c87ceb1a903743110f0b892f13feec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 18:35:59 +0300 Subject: [PATCH 3/6] Revert "Correctly handle tracebackhide for chained exceptions (#10772)" This reverts commit 431ec6d34ef99d80f90b330876ed6231144a3ce7. Fix #10903. Reopen #1904. --- AUTHORS | 1 - changelog/10903.bugfix.rst | 2 ++ doc/en/changelog.rst | 1 + src/_pytest/_code/code.py | 27 ++++++++++----------------- src/_pytest/reports.py | 4 ---- testing/code/test_excinfo.py | 23 +++++++++++++++++++++-- testing/test_tracebackhide.py | 25 ------------------------- 7 files changed, 34 insertions(+), 49 deletions(-) create mode 100644 changelog/10903.bugfix.rst delete mode 100644 testing/test_tracebackhide.py diff --git a/AUTHORS b/AUTHORS index 1aa5265e6..a4c7f8568 100644 --- a/AUTHORS +++ b/AUTHORS @@ -128,7 +128,6 @@ Erik M. Bray Evan Kepner Fabien Zarifian Fabio Zadrozny -Felix Hofstätter Felix Nieuwenhuizen Feng Ma Florian Bruhin diff --git a/changelog/10903.bugfix.rst b/changelog/10903.bugfix.rst new file mode 100644 index 000000000..34fbcd002 --- /dev/null +++ b/changelog/10903.bugfix.rst @@ -0,0 +1,2 @@ +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. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index e57e4fd72..5a2a26b87 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -82,6 +82,7 @@ Bug Fixes - `#1904 `_: Correctly handle ``__tracebackhide__`` for chained exceptions. + NOTE: This change was reverted in version 7.3.1. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 032b83beb..9024cc013 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]): """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self) -> Optional[TracebackEntry]: + def getcrashentry(self) -> 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 None + return self[-1] def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if @@ -602,13 +602,11 @@ class ExceptionInfo(Generic[E]): """ return isinstance(self.value, exc) - def _getreprcrash(self) -> Optional["ReprFileLocation"]: + def _getreprcrash(self) -> "ReprFileLocation": exconly = self.exconly(tryshort=True) entry = self.traceback.getcrashentry() - if entry: - path, lineno = entry.frame.code.raw.co_filename, entry.lineno - return ReprFileLocation(path, lineno + 1, exconly) - return None + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + return ReprFileLocation(path, lineno + 1, exconly) def getrepr( self, @@ -946,14 +944,9 @@ class FormattedExcinfo: ) else: reprtraceback = self.repr_traceback(excinfo_) - - # 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)] + reprcrash: Optional[ReprFileLocation] = ( + excinfo_._getreprcrash() if self.style != "value" else None + ) else: # Fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work. @@ -961,8 +954,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_ = ( @@ -1053,7 +1046,7 @@ class ExceptionChainRepr(ExceptionRepr): @dataclasses.dataclass(eq=False) class ReprExceptionInfo(ExceptionRepr): reprtraceback: "ReprTraceback" - reprcrash: Optional["ReprFileLocation"] + reprcrash: "ReprFileLocation" def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c0a76f92b..2e36514ea 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -347,10 +347,6 @@ 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 diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 918c97276..957ac6fc5 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -294,7 +294,6 @@ 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 @@ -312,7 +311,10 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback entry = tb.getcrashentry() - assert entry is None + 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" def test_excinfo_exconly(): @@ -1573,3 +1575,20 @@ 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 diff --git a/testing/test_tracebackhide.py b/testing/test_tracebackhide.py deleted file mode 100644 index 88f9c4fc0..000000000 --- a/testing/test_tracebackhide.py +++ /dev/null @@ -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 From e3b179976666b53f170b95c586322e1feb483a3c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 22:03:31 +0300 Subject: [PATCH 4/6] code: handle repr'ing empty tracebacks gracefully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By "empty traceback" I mean a traceback all of whose entries have been filtered/cut/pruned out. Currently, if an empty traceback needs to be repr'ed, the last entry before the filtering is used instead (added in accd962c9f88dbd5b2b0eef6efe7bf6fe5444b29). Showing a hidden frame is not so good IMO. This commit does the following instead: 1. Shows details of the exception. 2. Shows a message about how the full trace can be seen. Example: ``` _____________ test _____________ E ZeroDivisionError: division by zero All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames. ``` Also handles `--tb=native`, though there the `--full-trace` bit is not shown. This commit contains some pieces from 431ec6d34ef99d80f90b330876ed6231144a3ce7 (which has been reverted). Helps towards fixing issue # 1904. Co-authored-by: Felix Hofstätter --- AUTHORS | 1 + src/_pytest/_code/code.py | 52 +++++++++++++++++++++++------------- src/_pytest/nodes.py | 3 --- src/_pytest/reports.py | 3 +++ testing/code/test_excinfo.py | 23 +++++++--------- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/AUTHORS b/AUTHORS index a4c7f8568..1aa5265e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -128,6 +128,7 @@ Erik M. Bray Evan Kepner Fabien Zarifian Fabio Zadrozny +Felix Hofstätter Felix Nieuwenhuizen Feng Ma Florian Bruhin diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 872bea38b..2eabe5831 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -411,13 +411,14 @@ class Traceback(List[TracebackEntry]): """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self) -> TracebackEntry: - """Return last non-hidden traceback entry that lead to the exception of a traceback.""" + def getcrashentry(self) -> Optional[TracebackEntry]: + """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): 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 @@ -598,9 +599,11 @@ 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() + if entry is None: + return None path, lineno = entry.frame.code.raw.co_filename, entry.lineno return ReprFileLocation(path, lineno + 1, exconly) @@ -647,7 +650,9 @@ class ExceptionInfo(Generic[E]): return ReprExceptionInfo( reprtraceback=ReprTracebackNative( 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(), @@ -803,12 +808,16 @@ class FormattedExcinfo: def repr_traceback_entry( self, - entry: TracebackEntry, + entry: Optional[TracebackEntry], excinfo: Optional[ExceptionInfo[BaseException]] = None, ) -> "ReprEntry": lines: List[str] = [] - style = entry._repr_style if entry._repr_style is not None else self.style - if style in ("short", "long"): + style = ( + 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) if source is None: source = Source("???") @@ -857,17 +866,21 @@ class FormattedExcinfo: else: 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] - entries = [] if self.style == "value": - reprentry = self.repr_traceback_entry(last, excinfo) - entries.append(reprentry) + entries = [self.repr_traceback_entry(last, excinfo)] 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) - entries.append(reprentry) + entries = [ + self.repr_traceback_entry(entry, excinfo if last == entry else None) + for entry in traceback + ] return ReprTraceback(entries, extraline, style=self.style) def _truncate_recursive_traceback( @@ -924,6 +937,7 @@ class FormattedExcinfo: seen: Set[int] = set() while e is not None and id(e) not in seen: seen.add(id(e)) + if excinfo_: # Fall back to native traceback as a temporary workaround until # full support for exception groups added to ExceptionInfo. @@ -950,8 +964,8 @@ class FormattedExcinfo: traceback.format_exception(type(e), e, None) ) reprcrash = None - repr_chain += [(reprtraceback, reprcrash, descr)] + if e.__cause__ is not None and self.chain: e = e.__cause__ excinfo_ = ( @@ -1042,7 +1056,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) @@ -1147,8 +1161,8 @@ class ReprEntry(TerminalRepr): def toterminal(self, tw: TerminalWriter) -> None: if self.style == "short": - assert self.reprfileloc is not None - self.reprfileloc.toterminal(tw) + if self.reprfileloc: + self.reprfileloc.toterminal(tw) self._write_entry_lines(tw) if self.reprlocals: self.reprlocals.toterminal(tw, indent=" " * 8) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c74740dbc..ea016786e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -452,10 +452,7 @@ class Node(metaclass=NodeMeta): if self.config.getoption("fulltrace", False): style = "long" else: - tb = _pytest._code.Traceback([excinfo.traceback[-1]]) self._prunetraceback(excinfo) - if len(excinfo.traceback) == 0: - excinfo.traceback = tb if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 2e36514ea..74e8794b2 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -347,6 +347,9 @@ class TestReport(BaseReport): elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() + assert ( + r is not None + ), "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 diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 957ac6fc5..b6793fd72 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -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 @@ -309,12 +310,7 @@ class TestTraceback_f_g_h: g() 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 excinfo.traceback.getcrashentry() is None def test_excinfo_exconly(): @@ -1577,12 +1573,9 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None: _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. - """ +@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(): @@ -1590,5 +1583,9 @@ def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None: 1 / 0 """ ) - result = pytester.runpytest() + 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.*"]) From d38077106556184026137fe83d8351970f08ac89 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 14 Apr 2023 13:24:12 -0300 Subject: [PATCH 5/6] Fix tmp_path regression introduced in 7.3.0 (#10911) The problem is that we would loop over all directories of the basetemp directory searching for dead symlinks, for each test, which would compound over the test session run. Doing the cleanup just once, at the end of the session, fixes the problem. Fix #10896 --- changelog/10896.bugfix.rst | 1 + src/_pytest/pathlib.py | 4 ++-- src/_pytest/tmpdir.py | 21 ++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 changelog/10896.bugfix.rst diff --git a/changelog/10896.bugfix.rst b/changelog/10896.bugfix.rst new file mode 100644 index 000000000..87af0e301 --- /dev/null +++ b/changelog/10896.bugfix.rst @@ -0,0 +1 @@ +Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 30ed76cf8..2c9d5870b 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -353,7 +353,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: yield path -def cleanup_dead_symlink(root: Path): +def cleanup_dead_symlinks(root: Path): for left_dir in root.iterdir(): if left_dir.is_symlink(): if not left_dir.resolve().exists(): @@ -371,7 +371,7 @@ def cleanup_numbered_dir( for path in root.glob("garbage-*"): try_cleanup(path, consider_lock_dead_if_created_before) - cleanup_dead_symlink(root) + cleanup_dead_symlinks(root) def make_numbered_dir_with_cleanup( diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 5f347665f..d7f5ab9b4 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -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_symlink +from .pathlib import cleanup_dead_symlinks from _pytest.compat import final, get_user_id from _pytest.config import Config from _pytest.config import ExitCode @@ -289,31 +289,30 @@ 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 - if tmp_path_factory._basetemp is None: + basetemp = tmp_path_factory._basetemp + if basetemp is None: return + policy = tmp_path_factory._retention_policy if ( exitstatus == 0 and policy == "failed" and tmp_path_factory._given_basetemp is None ): - passed_dir = tmp_path_factory._basetemp - if passed_dir.exists(): + if basetemp.is_dir(): # 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(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) From ece756fcb4c3ca2bf6368f3abf52db9530930212 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 14 Apr 2023 15:12:22 -0300 Subject: [PATCH 6/6] Merge pull request #10913 from pytest-dev/release-7.3.1 Prepare release 7.3.1 (cherry picked from commit a1f7a204df45a4fd07d5d62128f61198e5dd7f23) --- changelog/10875.improvement.rst | 1 - changelog/10890.improvement.rst | 1 - changelog/10896.bugfix.rst | 1 - changelog/10903.bugfix.rst | 2 -- doc/en/announce/index.rst | 1 + doc/en/announce/release-7.3.1.rst | 18 ++++++++++++++++++ doc/en/changelog.rst | 23 +++++++++++++++++++++++ doc/en/getting-started.rst | 2 +- 8 files changed, 43 insertions(+), 6 deletions(-) delete mode 100644 changelog/10875.improvement.rst delete mode 100644 changelog/10890.improvement.rst delete mode 100644 changelog/10896.bugfix.rst delete mode 100644 changelog/10903.bugfix.rst create mode 100644 doc/en/announce/release-7.3.1.rst diff --git a/changelog/10875.improvement.rst b/changelog/10875.improvement.rst deleted file mode 100644 index eeaf04635..000000000 --- a/changelog/10875.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests. diff --git a/changelog/10890.improvement.rst b/changelog/10890.improvement.rst deleted file mode 100644 index 9c367da31..000000000 --- a/changelog/10890.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`. diff --git a/changelog/10896.bugfix.rst b/changelog/10896.bugfix.rst deleted file mode 100644 index 87af0e301..000000000 --- a/changelog/10896.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option. diff --git a/changelog/10903.bugfix.rst b/changelog/10903.bugfix.rst deleted file mode 100644 index 34fbcd002..000000000 --- a/changelog/10903.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -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. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 96db2e248..e3919f88e 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-7.3.1 release-7.3.0 release-7.2.2 release-7.2.1 diff --git a/doc/en/announce/release-7.3.1.rst b/doc/en/announce/release-7.3.1.rst new file mode 100644 index 000000000..e920fa8af --- /dev/null +++ b/doc/en/announce/release-7.3.1.rst @@ -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 diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 5a2a26b87..c13c05936 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,29 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 7.3.1 (2023-04-14) +========================= + +Improvements +------------ + +- `#10875 `_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests. + + +- `#10890 `_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`. + + + +Bug Fixes +--------- + +- `#10896 `_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option. + + +- `#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) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 8c9c4e75a..f41571141 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 7.3.0 + pytest 7.3.1 .. _`simpletest`: