From cc23ec91d042ee15145b890aea04e96f6e831101 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 23:17:54 +0300 Subject: [PATCH 01/14] code: stop storing weakref to ExceptionInfo on Traceback and TracebackEntry TracebackEntry needs the excinfo for the `__tracebackhide__ = callback` functionality, where `callback` accepts the excinfo. Currently it achieves this by storing a weakref to the excinfo which created it. I think this is not great, mixing layers and bloating the objects. Instead, have `ishidden` (and transitively, `Traceback.filter()`) take the excinfo as a parameter. --- src/_pytest/_code/code.py | 53 ++++++++++++++++++++---------------- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- src/_pytest/unittest.py | 2 +- testing/code/test_excinfo.py | 24 ++++++++-------- testing/python/collect.py | 4 +-- 6 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 5bc78f478..26db750e6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -31,7 +31,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -from weakref import ref import pluggy @@ -52,7 +51,6 @@ from _pytest.pathlib import bestrelpath if TYPE_CHECKING: from typing_extensions import Literal from typing_extensions import SupportsIndex - from weakref import ReferenceType _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] @@ -194,15 +192,13 @@ class Frame: class TracebackEntry: """A single entry in a Traceback.""" - __slots__ = ("_rawentry", "_excinfo", "_repr_style") + __slots__ = ("_rawentry", "_repr_style") def __init__( self, rawentry: TracebackType, - excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: self._rawentry = rawentry - self._excinfo = excinfo self._repr_style: Optional['Literal["short", "long"]'] = None @property @@ -272,7 +268,7 @@ class TracebackEntry: source = property(getsource) - def ishidden(self) -> bool: + def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool: """Return True if the current frame has a var __tracebackhide__ resolving to True. @@ -296,7 +292,7 @@ class TracebackEntry: else: break if tbh and callable(tbh): - return tbh(None if self._excinfo is None else self._excinfo()) + return tbh(excinfo) return tbh def __str__(self) -> str: @@ -329,16 +325,14 @@ class Traceback(List[TracebackEntry]): def __init__( self, tb: Union[TracebackType, Iterable[TracebackEntry]], - excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: """Initialize from given python traceback object and ExceptionInfo.""" - self._excinfo = excinfo if isinstance(tb, TracebackType): def f(cur: TracebackType) -> Iterable[TracebackEntry]: cur_: Optional[TracebackType] = cur while cur_ is not None: - yield TracebackEntry(cur_, excinfo=excinfo) + yield TracebackEntry(cur_) cur_ = cur_.tb_next super().__init__(f(tb)) @@ -378,7 +372,7 @@ class Traceback(List[TracebackEntry]): continue if firstlineno is not None and x.frame.code.firstlineno != firstlineno: continue - return Traceback(x._rawentry, self._excinfo) + return Traceback(x._rawentry) return self @overload @@ -398,25 +392,36 @@ class Traceback(List[TracebackEntry]): return super().__getitem__(key) def filter( - self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() + self, + # TODO(py38): change to positional only. + _excinfo_or_fn: Union[ + "ExceptionInfo[BaseException]", + Callable[[TracebackEntry], bool], + ], ) -> "Traceback": - """Return a Traceback instance with certain items removed + """Return a Traceback instance with certain items removed. - fn is a function that gets a single argument, a TracebackEntry - instance, and should return True when the item should be added - to the Traceback, False when not. + If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s + which are hidden (see ishidden() above). - By default this removes all the TracebackEntries which are hidden - (see ishidden() above). + Otherwise, the filter is a function that gets a single argument, a + ``TracebackEntry`` instance, and should return True when the item should + be added to the ``Traceback``, False when not. """ - return Traceback(filter(fn, self), self._excinfo) + if isinstance(_excinfo_or_fn, ExceptionInfo): + fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731 + else: + fn = _excinfo_or_fn + return Traceback(filter(fn, self)) - def getcrashentry(self) -> Optional[TracebackEntry]: + def getcrashentry( + self, excinfo: Optional["ExceptionInfo[BaseException]"] + ) -> 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(): + if not entry.ishidden(excinfo): return entry return None @@ -583,7 +588,7 @@ class ExceptionInfo(Generic[E]): def traceback(self) -> Traceback: """The traceback.""" if self._traceback is None: - self._traceback = Traceback(self.tb, excinfo=ref(self)) + self._traceback = Traceback(self.tb) return self._traceback @traceback.setter @@ -624,7 +629,7 @@ class ExceptionInfo(Generic[E]): def _getreprcrash(self) -> Optional["ReprFileLocation"]: exconly = self.exconly(tryshort=True) - entry = self.traceback.getcrashentry() + entry = self.traceback.getcrashentry(self) if entry is None: return None path, lineno = entry.frame.code.raw.co_filename, entry.lineno @@ -882,7 +887,7 @@ class FormattedExcinfo: def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback if self.tbfilter: - traceback = traceback.filter() + traceback = traceback.filter(excinfo) if isinstance(excinfo.value, RecursionError): traceback, extraline = self._truncate_recursive_traceback(traceback) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index ea016786e..738ab97e9 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -560,7 +560,7 @@ class Collector(Node): ntraceback = traceback.cut(path=self.path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=tracebackcutdir) - excinfo.traceback = ntraceback.filter() + excinfo.traceback = ntraceback.filter(excinfo) def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d04b6fa4d..c65c41b97 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1814,7 +1814,7 @@ class Function(PyobjMixin, nodes.Item): if not ntraceback: ntraceback = traceback - excinfo.traceback = ntraceback.filter() + excinfo.traceback = ntraceback.filter(excinfo) # issue364: mark all but first and last frames to # only show a single-line message for each frame. if self.config.getoption("tbstyle", "auto") == "auto": diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index c660aa75d..7a5e73661 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -339,7 +339,7 @@ class TestCaseFunction(Function): ) -> None: super()._prunetraceback(excinfo) traceback = excinfo.traceback.filter( - lambda x: not x.frame.f_globals.get("__unittest") + lambda x: not x.frame.f_globals.get("__unittest"), ) if traceback: excinfo.traceback = traceback diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 6a720e64c..d0eb5e646 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -186,7 +186,7 @@ class TestTraceback_f_g_h: def test_traceback_filter(self): traceback = self.excinfo.traceback - ntraceback = traceback.filter() + ntraceback = traceback.filter(self.excinfo) assert len(ntraceback) == len(traceback) - 1 @pytest.mark.parametrize( @@ -217,7 +217,7 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, h) traceback = excinfo.traceback - ntraceback = traceback.filter() + ntraceback = traceback.filter(excinfo) print(f"old: {traceback!r}") print(f"new: {ntraceback!r}") @@ -307,7 +307,7 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback - entry = tb.getcrashentry() + entry = tb.getcrashentry(excinfo) assert entry is not None co = _pytest._code.Code.from_function(h) assert entry.frame.code.path == co.path @@ -324,7 +324,7 @@ class TestTraceback_f_g_h: g() excinfo = pytest.raises(ValueError, f) - assert excinfo.traceback.getcrashentry() is None + assert excinfo.traceback.getcrashentry(excinfo) is None def test_excinfo_exconly(): @@ -626,7 +626,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.func1) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) p = FormattedExcinfo() reprtb = p.repr_traceback_entry(excinfo.traceback[-1]) @@ -659,7 +659,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) entry = excinfo.traceback[-1] p = FormattedExcinfo(funcargs=True) reprfuncargs = p.repr_args(entry) @@ -686,7 +686,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d") - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) entry = excinfo.traceback[-1] p = FormattedExcinfo(funcargs=True) reprfuncargs = p.repr_args(entry) @@ -960,7 +960,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) assert tw_mock.lines[0] == "" @@ -994,7 +994,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.f) tmp_path.joinpath("mod.py").unlink() - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) assert tw_mock.lines[0] == "" @@ -1026,7 +1026,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.f) tmp_path.joinpath("mod.py").write_text("asdf") - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) assert tw_mock.lines[0] == "" @@ -1123,7 +1123,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) excinfo.traceback[1].set_repr_style("short") excinfo.traceback[2].set_repr_style("short") r = excinfo.getrepr(style="long") @@ -1391,7 +1391,7 @@ raise ValueError() with pytest.raises(TypeError) as excinfo: mod.f() # previously crashed with `AttributeError: list has no attribute get` - excinfo.traceback.filter() + excinfo.traceback.filter(excinfo) @pytest.mark.parametrize("style", ["short", "long"]) diff --git a/testing/python/collect.py b/testing/python/collect.py index ac3edd395..41845517b 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1003,9 +1003,9 @@ class TestTracebackCutting: with pytest.raises(pytest.skip.Exception) as excinfo: pytest.skip("xxx") assert excinfo.traceback[-1].frame.code.name == "skip" - assert excinfo.traceback[-1].ishidden() + assert excinfo.traceback[-1].ishidden(excinfo) assert excinfo.traceback[-2].frame.code.name == "test_skip_simple" - assert not excinfo.traceback[-2].ishidden() + assert not excinfo.traceback[-2].ishidden(excinfo) def test_traceback_argsetup(self, pytester: Pytester) -> None: pytester.makeconftest( From 0a20452f78a2f5401cf0fc05dad04c8aeee170d7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 23:43:00 +0300 Subject: [PATCH 02/14] code: inline `Traceback.getcrashentry` into `ExceptionInfo._getreprcrash` Since `Traceback.getcrashentry` takes the `ExceptionInfo`, it is not really independent of it and is in the wrong layer. Prevent nonsensical mistakes by inlining it. --- src/_pytest/_code/code.py | 26 +++++++++----------------- testing/code/test_excinfo.py | 18 ++++++++---------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 26db750e6..581399949 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -414,17 +414,6 @@ class Traceback(List[TracebackEntry]): fn = _excinfo_or_fn return Traceback(filter(fn, self)) - def getcrashentry( - self, excinfo: Optional["ExceptionInfo[BaseException]"] - ) -> 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(excinfo): - return entry - return None - def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred.""" @@ -628,12 +617,15 @@ class ExceptionInfo(Generic[E]): return isinstance(self.value, exc) def _getreprcrash(self) -> Optional["ReprFileLocation"]: - exconly = self.exconly(tryshort=True) - entry = self.traceback.getcrashentry(self) - if entry is None: - return None - path, lineno = entry.frame.code.raw.co_filename, entry.lineno - return ReprFileLocation(path, lineno + 1, exconly) + # Find last non-hidden traceback entry that led to the exception of the + # traceback, or None if all hidden. + for i in range(-1, -len(self.traceback) - 1, -1): + entry = self.traceback[i] + if not entry.ishidden(self): + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + exconly = self.exconly(tryshort=True) + return ReprFileLocation(path, lineno + 1, exconly) + return None def getrepr( self, diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index d0eb5e646..3b05390be 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -11,7 +11,7 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import _pytest +import _pytest._code import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo @@ -290,7 +290,7 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, fail) assert excinfo.traceback.recursionindex() is None - def test_traceback_getcrashentry(self): + def test_getreprcrash(self): def i(): __tracebackhide__ = True raise ValueError @@ -306,15 +306,13 @@ class TestTraceback_f_g_h: g() excinfo = pytest.raises(ValueError, f) - tb = excinfo.traceback - entry = tb.getcrashentry(excinfo) - assert entry is not None + reprcrash = excinfo._getreprcrash() + assert reprcrash is not None co = _pytest._code.Code.from_function(h) - assert entry.frame.code.path == co.path - assert entry.lineno == co.firstlineno + 1 - assert entry.frame.code.name == "h" + assert reprcrash.path == str(co.path) + assert reprcrash.lineno == co.firstlineno + 1 + 1 - def test_traceback_getcrashentry_empty(self): + def test_getreprcrash_empty(self): def g(): __tracebackhide__ = True raise ValueError @@ -324,7 +322,7 @@ class TestTraceback_f_g_h: g() excinfo = pytest.raises(ValueError, f) - assert excinfo.traceback.getcrashentry(excinfo) is None + assert excinfo._getreprcrash() is None def test_excinfo_exconly(): From 6f7f89f3c42861448a370fe7bfc343083850f600 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 12 Apr 2023 22:58:57 +0300 Subject: [PATCH 03/14] code: make TracebackEntry immutable TracebackEntry being mutable caught me by surprise and makes reasoning about the exception formatting code harder. Make it a proper value. --- src/_pytest/_code/code.py | 15 +++++++++------ src/_pytest/python.py | 9 +++++++-- testing/code/test_excinfo.py | 6 ++++-- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 581399949..5148dcdbe 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -49,6 +49,7 @@ from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath if TYPE_CHECKING: + from typing_extensions import Final from typing_extensions import Literal from typing_extensions import SupportsIndex @@ -197,18 +198,20 @@ class TracebackEntry: def __init__( self, rawentry: TracebackType, + repr_style: Optional['Literal["short", "long"]'] = None, ) -> None: - self._rawentry = rawentry - self._repr_style: Optional['Literal["short", "long"]'] = None + self._rawentry: "Final" = rawentry + self._repr_style: "Final" = repr_style + + def with_repr_style( + self, repr_style: Optional['Literal["short", "long"]'] + ) -> "TracebackEntry": + return TracebackEntry(self._rawentry, repr_style) @property def lineno(self) -> int: return self._rawentry.tb_lineno - 1 - def set_repr_style(self, mode: "Literal['short', 'long']") -> None: - assert mode in ("short", "long") - self._repr_style = mode - @property def frame(self) -> Frame: return Frame(self._rawentry.tb_frame) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c65c41b97..8ed6b46df 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -35,6 +35,7 @@ from _pytest._code import filter_traceback from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr +from _pytest._code.code import Traceback from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped @@ -1819,8 +1820,12 @@ class Function(PyobjMixin, nodes.Item): # only show a single-line message for each frame. if self.config.getoption("tbstyle", "auto") == "auto": if len(excinfo.traceback) > 2: - for entry in excinfo.traceback[1:-1]: - entry.set_repr_style("short") + excinfo.traceback = Traceback( + entry + if i == 0 or i == len(excinfo.traceback) - 1 + else entry.with_repr_style("short") + for i, entry in enumerate(excinfo.traceback) + ) # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 3b05390be..bda6cd4cd 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1122,8 +1122,10 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.f) excinfo.traceback = excinfo.traceback.filter(excinfo) - excinfo.traceback[1].set_repr_style("short") - excinfo.traceback[2].set_repr_style("short") + excinfo.traceback = _pytest._code.Traceback( + entry if i not in (1, 2) else entry.with_repr_style("short") + for i, entry in enumerate(excinfo.traceback) + ) r = excinfo.getrepr(style="long") r.toterminal(tw_mock) for line in tw_mock.lines: From fcada1ea4763c0e3471cd58ac4b89e7c874e6264 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 13 Apr 2023 13:48:44 +0300 Subject: [PATCH 04/14] nodes: change _prunetraceback to return the new traceback instead of modifying excinfo This makes it usable as a general function, and just more understandable in general. --- src/_pytest/nodes.py | 12 +++++++----- src/_pytest/python.py | 15 +++++++++------ src/_pytest/unittest.py | 13 +++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 738ab97e9..1a1a47a28 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -22,6 +22,7 @@ import _pytest._code from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr +from _pytest._code.code import Traceback from _pytest.compat import cached_property from _pytest.compat import LEGACY_PATH from _pytest.config import Config @@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta): assert current is None or isinstance(current, cls) return current - def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: - pass + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + return excinfo.traceback def _repr_failure_py( self, @@ -452,7 +453,7 @@ class Node(metaclass=NodeMeta): if self.config.getoption("fulltrace", False): style = "long" else: - self._prunetraceback(excinfo) + excinfo.traceback = self._traceback_filter(excinfo) if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? @@ -554,13 +555,14 @@ class Collector(Node): return self._repr_failure_py(excinfo, style=tbstyle) - def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: if hasattr(self, "path"): traceback = excinfo.traceback ntraceback = traceback.cut(path=self.path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=tracebackcutdir) - excinfo.traceback = ntraceback.filter(excinfo) + return excinfo.traceback.filter(excinfo) + return excinfo.traceback def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8ed6b46df..ae09da48e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1802,7 +1802,7 @@ class Function(PyobjMixin, nodes.Item): def setup(self) -> None: self._request._fillfixtures() - def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): code = _pytest._code.Code.from_function(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno @@ -1814,19 +1814,22 @@ class Function(PyobjMixin, nodes.Item): ntraceback = ntraceback.filter(filter_traceback) if not ntraceback: ntraceback = traceback + ntraceback = ntraceback.filter(excinfo) - excinfo.traceback = ntraceback.filter(excinfo) # issue364: mark all but first and last frames to # only show a single-line message for each frame. if self.config.getoption("tbstyle", "auto") == "auto": - if len(excinfo.traceback) > 2: - excinfo.traceback = Traceback( + if len(ntraceback) > 2: + ntraceback = Traceback( entry - if i == 0 or i == len(excinfo.traceback) - 1 + if i == 0 or i == len(ntraceback) - 1 else entry.with_repr_style("short") - for i, entry in enumerate(excinfo.traceback) + for i, entry in enumerate(ntraceback) ) + return ntraceback + return excinfo.traceback + # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] self, diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 7a5e73661..d42a12a3a 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -334,15 +334,16 @@ class TestCaseFunction(Function): finally: delattr(self._testcase, self.name) - def _prunetraceback( + def _traceback_filter( self, excinfo: _pytest._code.ExceptionInfo[BaseException] - ) -> None: - super()._prunetraceback(excinfo) - traceback = excinfo.traceback.filter( + ) -> _pytest._code.Traceback: + traceback = super()._traceback_filter(excinfo) + ntraceback = traceback.filter( lambda x: not x.frame.f_globals.get("__unittest"), ) - if traceback: - excinfo.traceback = traceback + if not ntraceback: + ntraceback = traceback + return ntraceback @hookimpl(tryfirst=True) From 4f3f36c396b52f8398bc4734ff0c00c57cf1fed1 Mon Sep 17 00:00:00 2001 From: Chris Mahoney <44449504+chrimaho@users.noreply.github.com> Date: Fri, 26 May 2023 20:56:18 +1000 Subject: [PATCH 05/14] Add alias `--config-file` to `-c` (#11036) Fixes #11031 Signed-off-by: Chris Mahoney Co-authored-by: Chris Mahoney --- AUTHORS | 1 + changelog/11031.trivial.rst | 1 + doc/en/reference/reference.rst | 3 ++- src/_pytest/main.py | 7 ++++--- testing/test_config.py | 11 +++++++++++ 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 changelog/11031.trivial.rst diff --git a/AUTHORS b/AUTHORS index c9e990bf1..309088707 100644 --- a/AUTHORS +++ b/AUTHORS @@ -72,6 +72,7 @@ Charles Cloud Charles Machalow Charnjit SiNGH (CCSJ) Cheuk Ting Ho +Chris Mahoney Chris Lamb Chris NeJame Chris Rose diff --git a/changelog/11031.trivial.rst b/changelog/11031.trivial.rst new file mode 100644 index 000000000..fad2cf24d --- /dev/null +++ b/changelog/11031.trivial.rst @@ -0,0 +1 @@ +Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 6a5512cc1..7107218b3 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1918,7 +1918,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: --strict-markers Markers not registered in the `markers` section of the configuration file raise errors --strict (Deprecated) alias to --strict-markers - -c file Load configuration from `file` instead of trying to + -c, --config-file FILE + Load configuration from `FILE` instead of trying to locate one of the implicit configuration files --continue-on-collection-errors Force test execution even if collection errors occur diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 5f8ac4689..c9a495380 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -122,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None: ) group._addoption( "-c", - metavar="file", + "--config-file", + metavar="FILE", type=str, dest="inifilename", - help="Load configuration from `file` instead of trying to locate one of the " - "implicit configuration files", + help="Load configuration from `FILE` instead of trying to locate one of the " + "implicit configuration files.", ) group._addoption( "--continue-on-collection-errors", diff --git a/testing/test_config.py b/testing/test_config.py index 6754cd15b..1291e85f9 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -514,6 +514,8 @@ class TestConfigCmdlineParsing: ) config = pytester.parseconfig("-c", "custom.ini") assert config.getini("custom") == "1" + config = pytester.parseconfig("--config-file", "custom.ini") + assert config.getini("custom") == "1" pytester.makefile( ".cfg", @@ -524,6 +526,8 @@ class TestConfigCmdlineParsing: ) config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" + config = pytester.parseconfig("--config-file", "custom_tool_pytest_section.cfg") + assert config.getini("custom") == "1" pytester.makefile( ".toml", @@ -536,6 +540,8 @@ class TestConfigCmdlineParsing: ) config = pytester.parseconfig("-c", "custom.toml") assert config.getini("custom") == "1" + config = pytester.parseconfig("--config-file", "custom.toml") + assert config.getini("custom") == "1" def test_absolute_win32_path(self, pytester: Pytester) -> None: temp_ini_file = pytester.makefile( @@ -550,6 +556,8 @@ class TestConfigCmdlineParsing: temp_ini_file_norm = normpath(str(temp_ini_file)) ret = pytest.main(["-c", temp_ini_file_norm]) assert ret == ExitCode.OK + ret = pytest.main(["--config-file", temp_ini_file_norm]) + assert ret == ExitCode.OK class TestConfigAPI: @@ -1907,6 +1915,9 @@ class TestSetupCfg: with pytest.raises(pytest.fail.Exception): pytester.runpytest("-c", "custom.cfg") + with pytest.raises(pytest.fail.Exception): + pytester.runpytest("--config-file", "custom.cfg") + class TestPytestPluginsVariable: def test_pytest_plugins_in_non_top_level_conftest_unsupported( From 5313d50e187ab4dc9f267c88c15ff2a7d757fb05 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 28 May 2023 00:22:05 +0000 Subject: [PATCH 06/14] [automated] Update plugin list --- doc/en/reference/plugin_list.rst | 132 ++++++++++++++++++------------- 1 file changed, 78 insertions(+), 54 deletions(-) diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index 152c2597d..fafc8c876 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -11,7 +11,7 @@ automatically. Packages classified as inactive are excluded. creating a PDF, because otherwise the table gets far too wide for the page. -This list contains 1256 plugins. +This list contains 1259 plugins. .. only:: not latex @@ -38,7 +38,7 @@ This list contains 1256 plugins. :pypi:`pytest-aioworkers` A plugin to test aioworkers project with pytest May 01, 2023 5 - Production/Stable pytest>=6.1.0 :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A - :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Apr 18, 2023 N/A pytest (>=6.0) + :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. May 23, 2023 N/A pytest (>=6.0) :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) @@ -213,7 +213,7 @@ This list contains 1256 plugins. :pypi:`pytest-concurrent` Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) :pypi:`pytest-config` Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A :pypi:`pytest-confluence-report` Package stands for pytest plugin to upload results into Confluence page. Apr 17, 2022 N/A N/A - :pypi:`pytest-console-scripts` Pytest plugin for testing console scripts Mar 18, 2022 4 - Beta N/A + :pypi:`pytest-console-scripts` Pytest plugin for testing console scripts May 22, 2023 4 - Beta N/A :pypi:`pytest-consul` pytest plugin with fixtures for testing consul aware apps Nov 24, 2018 3 - Alpha pytest :pypi:`pytest-container` Pytest fixtures for writing container based tests Mar 21, 2023 4 - Beta pytest (>=3.10) :pypi:`pytest-contextfixture` Define pytest fixtures as context managers. Mar 12, 2013 4 - Beta N/A @@ -221,14 +221,14 @@ This list contains 1256 plugins. :pypi:`pytest-cookies` The pytest plugin for your Cookiecutter templates. 🍪 Mar 22, 2023 5 - Production/Stable pytest (>=3.9.0) :pypi:`pytest-couchdbkit` py.test extension for per-test couchdb databases using couchdbkit Apr 17, 2012 N/A N/A :pypi:`pytest-count` count erros and send email Jan 12, 2018 4 - Beta N/A - :pypi:`pytest-cov` Pytest plugin for measuring coverage. Sep 28, 2022 5 - Production/Stable pytest (>=4.6) + :pypi:`pytest-cov` Pytest plugin for measuring coverage. May 24, 2023 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-cover` Pytest plugin for measuring coverage. Forked from \`pytest-cov\`. Aug 01, 2015 5 - Production/Stable N/A :pypi:`pytest-coverage` Jun 17, 2015 N/A N/A :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jan 04, 2021 4 - Beta pytest (>=6.1.0) :pypi:`pytest-coveragemarkers` Using pytest markers to track functional coverage and filtering of tests Nov 29, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-cov-exclude` Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Jan 30, 2023 5 - Production/Stable pytest (>=7.0) - :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types Apr 20, 2023 N/A N/A + :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types May 25, 2023 N/A N/A :pypi:`pytest-cqase` Custom qase pytest plugin Aug 22, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-cram` Run cram tests with pytest. Aug 08, 2020 N/A N/A :pypi:`pytest-crate` Manages CrateDB instances during your integration tests May 28, 2019 3 - Alpha pytest (>=4.0) @@ -304,7 +304,7 @@ This list contains 1256 plugins. :pypi:`pytest-django-filefield` Replaces FileField.storage with something you can patch globally. May 09, 2022 5 - Production/Stable pytest >= 5.2 :pypi:`pytest-django-gcir` A Django plugin for pytest. Mar 06, 2018 5 - Production/Stable N/A :pypi:`pytest-django-haystack` Cleanup your Haystack indexes between tests Sep 03, 2017 5 - Production/Stable pytest (>=2.3.4) - :pypi:`pytest-django-ifactory` A model instance factory for pytest-django Feb 09, 2022 3 - Alpha N/A + :pypi:`pytest-django-ifactory` A model instance factory for pytest-django May 21, 2023 5 - Production/Stable N/A :pypi:`pytest-django-lite` The bare minimum to integrate py.test with Django. Jan 30, 2014 N/A N/A :pypi:`pytest-django-liveserver-ssl` Jan 20, 2022 3 - Alpha N/A :pypi:`pytest-django-model` A Simple Way to Test your Django Models Feb 14, 2019 4 - Beta N/A @@ -486,7 +486,7 @@ This list contains 1256 plugins. :pypi:`pytest-gherkin` A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) :pypi:`pytest-gh-log-group` pytest plugin for gh actions Jan 11, 2022 3 - Alpha pytest :pypi:`pytest-ghostinspector` For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A - :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. May 20, 2023 N/A N/A + :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. May 22, 2023 N/A N/A :pypi:`pytest-git` Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-gitcov` Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A :pypi:`pytest-git-fixtures` Pytest fixtures for testing with git. Mar 11, 2021 4 - Beta pytest @@ -499,7 +499,7 @@ This list contains 1256 plugins. :pypi:`pytest-glamor-allure` Extends allure-pytest functionality Jul 22, 2022 4 - Beta pytest :pypi:`pytest-gnupg-fixtures` Pytest fixtures for testing with gnupg. Mar 04, 2021 4 - Beta pytest :pypi:`pytest-golden` Plugin for pytest that offloads expected outputs to data files Jul 18, 2022 N/A pytest (>=6.1.2) - :pypi:`pytest-goldie` A plugin to support golden tests with pytest. Apr 12, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-goldie` A plugin to support golden tests with pytest. May 23, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-google-chat` Notify google chat channel for test results Mar 27, 2022 4 - Beta pytest :pypi:`pytest-graphql-schema` Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A :pypi:`pytest-greendots` Green progress dots Feb 08, 2014 3 - Alpha N/A @@ -518,7 +518,7 @@ This list contains 1256 plugins. :pypi:`pytest-historic` Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest :pypi:`pytest-historic-hook` Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A - :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components May 15, 2023 3 - Alpha pytest (==7.3.1) + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components May 24, 2023 3 - Alpha pytest (==7.3.1) :pypi:`pytest-honey` A simple plugin to use with pytest Jan 07, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A :pypi:`pytest-hot-reloading` May 18, 2023 N/A N/A @@ -539,7 +539,7 @@ This list contains 1256 plugins. :pypi:`pytest-httpdbg` A pytest plugin to record HTTP(S) requests with stack trace May 09, 2023 3 - Alpha pytest (>=7.0.0) :pypi:`pytest-http-mocker` Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A :pypi:`pytest-httpretty` A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A - :pypi:`pytest-httpserver` pytest-httpserver is a httpserver for pytest May 16, 2023 3 - Alpha N/A + :pypi:`pytest-httpserver` pytest-httpserver is a httpserver for pytest May 22, 2023 3 - Alpha N/A :pypi:`pytest-httptesting` http_testing framework on top of pytest Apr 19, 2023 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-httpx` Send responses to httpx. Apr 12, 2023 5 - Production/Stable pytest (<8.0,>=6.0) :pypi:`pytest-httpx-blockage` Disable httpx requests during a test run Feb 16, 2023 N/A pytest (>=7.2.1) @@ -569,7 +569,7 @@ This list contains 1256 plugins. :pypi:`pytest-instafail` pytest plugin to show failures instantly Mar 31, 2023 4 - Beta pytest (>=5) :pypi:`pytest-instrument` pytest plugin to instrument tests Apr 05, 2020 5 - Production/Stable pytest (>=5.1.0) :pypi:`pytest-integration` Organizing pytests by integration or not Nov 17, 2022 N/A N/A - :pypi:`pytest-integration-mark` Automatic integration test marking and excluding plugin for pytest Jul 19, 2021 N/A pytest (>=5.2,<7.0) + :pypi:`pytest-integration-mark` Automatic integration test marking and excluding plugin for pytest May 22, 2023 N/A pytest (>=5.2) :pypi:`pytest-interactive` A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A :pypi:`pytest-intercept-remote` Pytest plugin for intercepting outgoing connection requests during pytest run. May 24, 2021 4 - Beta pytest (>=4.6) :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. May 09, 2023 4 - Beta pytest @@ -649,7 +649,7 @@ This list contains 1256 plugins. :pypi:`pytest-logger` Plugin configuring handlers for loggers from Python logging module. Jul 25, 2019 4 - Beta pytest (>=3.2) :pypi:`pytest-logging` Configures logging and allows tweaking the log level with a py.test flag Nov 04, 2015 4 - Beta N/A :pypi:`pytest-logging-end-to-end-test-tool` Sep 23, 2022 N/A pytest (>=7.1.2,<8.0.0) - :pypi:`pytest-logikal` Common testing environment May 15, 2023 5 - Production/Stable pytest (==7.3.1) + :pypi:`pytest-logikal` Common testing environment May 27, 2023 5 - Production/Stable pytest (==7.3.1) :pypi:`pytest-log-report` Package for creating a pytest test run reprot Dec 26, 2019 N/A N/A :pypi:`pytest-loguru` Pytest Loguru Apr 12, 2022 5 - Production/Stable N/A :pypi:`pytest-loop` pytest plugin for looping tests Jul 22, 2022 5 - Production/Stable pytest (>=6) @@ -680,9 +680,9 @@ This list contains 1256 plugins. :pypi:`pytest-mesh` pytest_mesh插件 Aug 05, 2022 N/A pytest (==7.1.2) :pypi:`pytest-message` Pytest plugin for sending report message of marked tests execution Aug 04, 2022 N/A pytest (>=6.2.5) :pypi:`pytest-messenger` Pytest to Slack reporting plugin Nov 24, 2022 5 - Production/Stable N/A - :pypi:`pytest-metadata` pytest plugin for test session metadata Oct 30, 2022 5 - Production/Stable pytest (>=3.0.0,<8.0.0) + :pypi:`pytest-metadata` pytest plugin for test session metadata May 27, 2023 5 - Production/Stable pytest>=7.0.0 :pypi:`pytest-metrics` Custom metrics report for pytest Apr 04, 2020 N/A pytest - :pypi:`pytest-mh` Pytest multihost plugin May 04, 2023 N/A pytest + :pypi:`pytest-mh` Pytest multihost plugin May 25, 2023 N/A pytest :pypi:`pytest-mimesis` Mimesis integration with the pytest test runner Mar 21, 2020 5 - Production/Stable pytest (>=4.2) :pypi:`pytest-minecraft` A pytest plugin for running tests against Minecraft releases Apr 06, 2022 N/A pytest (>=6.0.1) :pypi:`pytest-mini` A plugin to test mp Feb 06, 2023 N/A pytest (>=7.2.0,<8.0.0) @@ -695,7 +695,7 @@ This list contains 1256 plugins. :pypi:`pytest-mock-helper` Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest :pypi:`pytest-mockito` Base fixtures for mockito Jul 11, 2018 4 - Beta N/A :pypi:`pytest-mockredis` An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A - :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. May 03, 2023 N/A pytest (>=1.0) + :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. May 23, 2023 N/A pytest (>=1.0) :pypi:`pytest-mock-server` Mock server plugin for pytest Jan 09, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-mockservers` A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) :pypi:`pytest-mocktcp` A pytest plugin for testing TCP clients Oct 11, 2022 N/A pytest @@ -825,6 +825,7 @@ This list contains 1256 plugins. :pypi:`pytest-plus` PyTest Plus Plugin :: extends pytest functionality Dec 24, 2022 5 - Production/Stable pytest (>=6.0.1) :pypi:`pytest-pmisc` Mar 21, 2019 5 - Production/Stable N/A :pypi:`pytest-pointers` Pytest plugin to define functions you test with special marks for better navigation and reports Dec 26, 2022 N/A N/A + :pypi:`pytest-pokie` Pokie plugin for pytest May 22, 2023 5 - Production/Stable N/A :pypi:`pytest-polarion-cfme` pytest plugin for collecting test cases and recording test results Nov 13, 2017 3 - Alpha N/A :pypi:`pytest-polarion-collect` pytest plugin for collecting polarion test cases data Jun 18, 2020 3 - Alpha pytest :pypi:`pytest-polecat` Provides Polecat pytest fixtures Aug 12, 2019 4 - Beta N/A @@ -910,11 +911,12 @@ This list contains 1256 plugins. :pypi:`pytest-redmine` Pytest plugin for redmine Mar 19, 2018 1 - Planning N/A :pypi:`pytest-ref` A plugin to store reference files to ease regression testing Nov 23, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-reference-formatter` Conveniently run pytest with a dot-formatted test reference. Oct 01, 2019 4 - Beta N/A + :pypi:`pytest-regex` Select pytest tests with regular expressions May 23, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-regex-dependency` Management of Pytest dependencies via regex patterns Jun 12, 2022 N/A pytest :pypi:`pytest-regressions` Easy to use fixtures to write regression tests. Jan 13, 2023 5 - Production/Stable pytest (>=6.2.0) :pypi:`pytest-regtest` pytest plugin for regression tests Jul 08, 2022 N/A N/A :pypi:`pytest-relative-order` a pytest plugin that sorts tests using "before" and "after" markers May 17, 2021 4 - Beta N/A - :pypi:`pytest-relaxed` Relaxed test discovery/organization for pytest Dec 31, 2022 5 - Production/Stable pytest (>=7) + :pypi:`pytest-relaxed` Relaxed test discovery/organization for pytest May 23, 2023 5 - Production/Stable pytest (>=7) :pypi:`pytest-remfiles` Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A :pypi:`pytest-remotedata` Pytest plugin for controlling remote data access. Dec 12, 2022 3 - Alpha pytest (>=4.6) :pypi:`pytest-remote-response` Pytest plugin for capturing and mocking connection requests. Apr 26, 2023 5 - Production/Stable pytest (>=4.6) @@ -929,10 +931,10 @@ This list contains 1256 plugins. :pypi:`pytest-reporter-html-dots` A basic HTML report for pytest using Jinja2 template engine. Jan 22, 2023 N/A N/A :pypi:`pytest-reportinfra` Pytest plugin for reportinfra Aug 11, 2019 3 - Alpha N/A :pypi:`pytest-reporting` A plugin to report summarized results in a table format Oct 25, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility Apr 26, 2023 3 - Alpha pytest + :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility May 22, 2023 3 - Alpha pytest :pypi:`pytest-report-me` A pytest plugin to generate report. Dec 31, 2020 N/A pytest :pypi:`pytest-report-parameters` pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) - :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Apr 21, 2023 N/A pytest (>=3.8.0) + :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal May 24, 2023 N/A pytest (>=3.8.0) :pypi:`pytest-reqs` pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) :pypi:`pytest-requests` A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-requestselapsed` collect and show http requests elapsed time Aug 14, 2022 N/A N/A @@ -989,18 +991,19 @@ This list contains 1256 plugins. :pypi:`pytest-sanic` a pytest plugin for Sanic Oct 25, 2021 N/A pytest (>=5.2) :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. May 12, 2023 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. May 25, 2023 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A :pypi:`pytest-schema` 👍 Validate return values against a schema-like object in testing Mar 14, 2022 5 - Production/Stable pytest (>=3.5.0) :pypi:`pytest-securestore` An encrypted password store for use within pytest cases Nov 08, 2021 4 - Beta N/A :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) :pypi:`pytest-selenium` pytest plugin for Selenium Sep 21, 2022 5 - Production/Stable pytest (>=6.0.0,<7.0.0) - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. May 12, 2023 5 - Production/Stable N/A + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. May 25, 2023 5 - Production/Stable N/A :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A :pypi:`pytest-sentry` A pytest plugin to send testrun information to Sentry.io Jan 05, 2023 N/A N/A + :pypi:`pytest-sequence-markers` Pytest plugin for sequencing markers for execution of tests May 23, 2023 5 - Production/Stable N/A :pypi:`pytest-server-fixtures` Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-serverless` Automatically mocks resources from serverless.yml in pytest using moto. May 09, 2022 4 - Beta N/A :pypi:`pytest-servers` pytest servers Apr 15, 2023 3 - Alpha pytest (>=6.2) @@ -1102,7 +1105,7 @@ This list contains 1256 plugins. :pypi:`pytest-tagging` a pytest plugin to tag tests Apr 01, 2023 N/A pytest (>=7.1.3,<8.0.0) :pypi:`pytest-takeltest` Fixtures for ansible, testinfra and molecule Feb 15, 2023 N/A N/A :pypi:`pytest-talisker` Nov 28, 2021 N/A N/A - :pypi:`pytest-tally` A Pytest plugin to generate realtime summary stats, and display them in-console using a text-based dashboard. May 20, 2023 4 - Beta pytest (>=6.2.5) + :pypi:`pytest-tally` A Pytest plugin to generate realtime summary stats, and display them in-console using a text-based dashboard. May 22, 2023 4 - Beta pytest (>=6.2.5) :pypi:`pytest-tap` Test Anything Protocol (TAP) reporting plugin for pytest Oct 27, 2021 5 - Production/Stable pytest (>=3.0) :pypi:`pytest-tape` easy assertion with expected results saved to yaml files Mar 17, 2021 4 - Beta N/A :pypi:`pytest-target` Pytest plugin for remote target orchestration. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) @@ -1121,7 +1124,7 @@ This list contains 1256 plugins. :pypi:`pytest-testdox` A testdox format reporter for pytest Apr 19, 2022 5 - Production/Stable pytest (>=4.6.0) :pypi:`pytest-test-grouping` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Feb 01, 2023 5 - Production/Stable pytest (>=2.5) :pypi:`pytest-test-groups` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Oct 25, 2016 5 - Production/Stable N/A - :pypi:`pytest-testinfra` Test infrastructures May 19, 2023 5 - Production/Stable pytest (!=3.0.2) + :pypi:`pytest-testinfra` Test infrastructures May 21, 2023 5 - Production/Stable pytest (!=3.0.2) :pypi:`pytest-testlink-adaptor` pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) :pypi:`pytest-testmon` selects tests affected by changed files and methods May 18, 2023 4 - Beta pytest (<8,>=5) :pypi:`pytest-testmon-dev` selects tests affected by changed files and methods Mar 30, 2023 4 - Beta pytest (<8,>=5) @@ -1208,7 +1211,7 @@ This list contains 1256 plugins. :pypi:`pytest-utils` Some helpers for pytest. Feb 02, 2023 4 - Beta pytest (>=7.0.0,<8.0.0) :pypi:`pytest-vagrant` A py.test plugin providing access to vagrant. Sep 07, 2021 5 - Production/Stable pytest :pypi:`pytest-valgrind` May 19, 2021 N/A N/A - :pypi:`pytest-variables` pytest plugin for providing variables to tests/fixtures Mar 27, 2022 5 - Production/Stable pytest (>=3.0.0,<8.0.0) + :pypi:`pytest-variables` pytest plugin for providing variables to tests/fixtures May 27, 2023 5 - Production/Stable pytest>=7.0.0 :pypi:`pytest-variant` Variant support for Pytest Jun 06, 2022 N/A N/A :pypi:`pytest-vcr` Plugin for managing VCR.py cassettes Apr 26, 2019 5 - Production/Stable pytest (>=3.6.0) :pypi:`pytest-vcr-delete-on-fail` A pytest plugin that automates vcrpy cassettes deletion on test failure. Jun 20, 2022 5 - Production/Stable pytest (>=6.2.2) @@ -1235,7 +1238,7 @@ This list contains 1256 plugins. :pypi:`pytest-web3-data` Sep 15, 2022 4 - Beta pytest :pypi:`pytest-webdriver` Selenium webdriver fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-wetest` Welian API Automation test framework pytest plugin Nov 10, 2018 4 - Beta N/A - :pypi:`pytest-when` Utility which makes mocking more readable and controllable May 19, 2023 N/A N/A + :pypi:`pytest-when` Utility which makes mocking more readable and controllable May 22, 2023 N/A pytest>=7.3.1 :pypi:`pytest-whirlwind` Testing Tornado. Jun 12, 2020 N/A N/A :pypi:`pytest-wholenodeid` pytest addon for displaying the whole node id for failures Aug 26, 2015 4 - Beta pytest (>=2.0) :pypi:`pytest-win32consoletitle` Pytest progress in console title (Win32 only) Aug 08, 2021 N/A N/A @@ -1262,7 +1265,7 @@ This list contains 1256 plugins. :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml Mar 17, 2023 N/A pytest>=7.2.0 :pypi:`pytest-yamltree` Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) :pypi:`pytest-yamlwsgi` Run tests against wsgi apps defined in yaml May 11, 2010 N/A N/A - :pypi:`pytest-yaml-yoyo` http/https API run by yaml May 20, 2023 N/A pytest (>=7.2.0) + :pypi:`pytest-yaml-yoyo` http/https API run by yaml May 22, 2023 N/A pytest (>=7.2.0) :pypi:`pytest-yapf` Run yapf Jul 06, 2017 4 - Beta pytest (>=3.1.1) :pypi:`pytest-yapf3` Validate your Python file format with yapf Mar 29, 2023 5 - Production/Stable pytest (>=7) :pypi:`pytest-yield` PyTest plugin to run tests concurrently, each \`yield\` switch context to other one Jan 23, 2019 N/A N/A @@ -1420,7 +1423,7 @@ This list contains 1256 plugins. :pypi:`pytest-alembic` - *last release*: Apr 18, 2023, + *last release*: May 23, 2023, *status*: N/A, *requires*: pytest (>=6.0) @@ -2645,7 +2648,7 @@ This list contains 1256 plugins. Package stands for pytest plugin to upload results into Confluence page. :pypi:`pytest-console-scripts` - *last release*: Mar 18, 2022, + *last release*: May 22, 2023, *status*: 4 - Beta, *requires*: N/A @@ -2701,7 +2704,7 @@ This list contains 1256 plugins. count erros and send email :pypi:`pytest-cov` - *last release*: Sep 28, 2022, + *last release*: May 24, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=4.6) @@ -2750,7 +2753,7 @@ This list contains 1256 plugins. Use pytest's runner to discover and execute C++ tests :pypi:`pytest-cppython` - *last release*: Apr 20, 2023, + *last release*: May 25, 2023, *status*: N/A, *requires*: N/A @@ -3282,8 +3285,8 @@ This list contains 1256 plugins. Cleanup your Haystack indexes between tests :pypi:`pytest-django-ifactory` - *last release*: Feb 09, 2022, - *status*: 3 - Alpha, + *last release*: May 21, 2023, + *status*: 5 - Production/Stable, *requires*: N/A A model instance factory for pytest-django @@ -4556,7 +4559,7 @@ This list contains 1256 plugins. For finding/executing Ghost Inspector tests :pypi:`pytest-girder` - *last release*: May 20, 2023, + *last release*: May 22, 2023, *status*: N/A, *requires*: N/A @@ -4647,7 +4650,7 @@ This list contains 1256 plugins. Plugin for pytest that offloads expected outputs to data files :pypi:`pytest-goldie` - *last release*: Apr 12, 2023, + *last release*: May 23, 2023, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) @@ -4780,7 +4783,7 @@ This list contains 1256 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: May 15, 2023, + *last release*: May 24, 2023, *status*: 3 - Alpha, *requires*: pytest (==7.3.1) @@ -4927,7 +4930,7 @@ This list contains 1256 plugins. A thin wrapper of HTTPretty for pytest :pypi:`pytest-httpserver` - *last release*: May 16, 2023, + *last release*: May 22, 2023, *status*: 3 - Alpha, *requires*: N/A @@ -5137,9 +5140,9 @@ This list contains 1256 plugins. Organizing pytests by integration or not :pypi:`pytest-integration-mark` - *last release*: Jul 19, 2021, + *last release*: May 22, 2023, *status*: N/A, - *requires*: pytest (>=5.2,<7.0) + *requires*: pytest (>=5.2) Automatic integration test marking and excluding plugin for pytest @@ -5697,7 +5700,7 @@ This list contains 1256 plugins. :pypi:`pytest-logikal` - *last release*: May 15, 2023, + *last release*: May 27, 2023, *status*: 5 - Production/Stable, *requires*: pytest (==7.3.1) @@ -5914,9 +5917,9 @@ This list contains 1256 plugins. Pytest to Slack reporting plugin :pypi:`pytest-metadata` - *last release*: Oct 30, 2022, + *last release*: May 27, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0,<8.0.0) + *requires*: pytest>=7.0.0 pytest plugin for test session metadata @@ -5928,7 +5931,7 @@ This list contains 1256 plugins. Custom metrics report for pytest :pypi:`pytest-mh` - *last release*: May 04, 2023, + *last release*: May 25, 2023, *status*: N/A, *requires*: pytest @@ -6019,7 +6022,7 @@ This list contains 1256 plugins. An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. :pypi:`pytest-mock-resources` - *last release*: May 03, 2023, + *last release*: May 23, 2023, *status*: N/A, *requires*: pytest (>=1.0) @@ -6928,6 +6931,13 @@ This list contains 1256 plugins. Pytest plugin to define functions you test with special marks for better navigation and reports + :pypi:`pytest-pokie` + *last release*: May 22, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Pokie plugin for pytest + :pypi:`pytest-polarion-cfme` *last release*: Nov 13, 2017, *status*: 3 - Alpha, @@ -7523,6 +7533,13 @@ This list contains 1256 plugins. Conveniently run pytest with a dot-formatted test reference. + :pypi:`pytest-regex` + *last release*: May 23, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + Select pytest tests with regular expressions + :pypi:`pytest-regex-dependency` *last release*: Jun 12, 2022, *status*: N/A, @@ -7552,7 +7569,7 @@ This list contains 1256 plugins. a pytest plugin that sorts tests using "before" and "after" markers :pypi:`pytest-relaxed` - *last release*: Dec 31, 2022, + *last release*: May 23, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=7) @@ -7657,7 +7674,7 @@ This list contains 1256 plugins. A plugin to report summarized results in a table format :pypi:`pytest-reportlog` - *last release*: Apr 26, 2023, + *last release*: May 22, 2023, *status*: 3 - Alpha, *requires*: pytest @@ -7678,7 +7695,7 @@ This list contains 1256 plugins. pytest plugin for adding tests' parameters to junit report :pypi:`pytest-reportportal` - *last release*: Apr 21, 2023, + *last release*: May 24, 2023, *status*: N/A, *requires*: pytest (>=3.8.0) @@ -8077,7 +8094,7 @@ This list contains 1256 plugins. :pypi:`pytest-sbase` - *last release*: May 12, 2023, + *last release*: May 25, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8126,7 +8143,7 @@ This list contains 1256 plugins. pytest plugin for Selenium :pypi:`pytest-seleniumbase` - *last release*: May 12, 2023, + *last release*: May 25, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8160,6 +8177,13 @@ This list contains 1256 plugins. A pytest plugin to send testrun information to Sentry.io + :pypi:`pytest-sequence-markers` + *last release*: May 23, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Pytest plugin for sequencing markers for execution of tests + :pypi:`pytest-server-fixtures` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -8868,7 +8892,7 @@ This list contains 1256 plugins. :pypi:`pytest-tally` - *last release*: May 20, 2023, + *last release*: May 22, 2023, *status*: 4 - Beta, *requires*: pytest (>=6.2.5) @@ -9001,7 +9025,7 @@ This list contains 1256 plugins. A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. :pypi:`pytest-testinfra` - *last release*: May 19, 2023, + *last release*: May 21, 2023, *status*: 5 - Production/Stable, *requires*: pytest (!=3.0.2) @@ -9610,9 +9634,9 @@ This list contains 1256 plugins. :pypi:`pytest-variables` - *last release*: Mar 27, 2022, + *last release*: May 27, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0,<8.0.0) + *requires*: pytest>=7.0.0 pytest plugin for providing variables to tests/fixtures @@ -9799,9 +9823,9 @@ This list contains 1256 plugins. Welian API Automation test framework pytest plugin :pypi:`pytest-when` - *last release*: May 19, 2023, + *last release*: May 22, 2023, *status*: N/A, - *requires*: N/A + *requires*: pytest>=7.3.1 Utility which makes mocking more readable and controllable @@ -9988,7 +10012,7 @@ This list contains 1256 plugins. Run tests against wsgi apps defined in yaml :pypi:`pytest-yaml-yoyo` - *last release*: May 20, 2023, + *last release*: May 22, 2023, *status*: N/A, *requires*: pytest (>=7.2.0) From dd667336ce0b55b430452e78cd57ba2c0a769066 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 16 Apr 2023 18:51:49 +0300 Subject: [PATCH 07/14] nodes: apply same traceback filtering for chained exceptions as main exception Fix #1904. --- changelog/1904.bugfix.rst | 1 + src/_pytest/_code/code.py | 22 +++++++++++++----- src/_pytest/nodes.py | 7 ++++-- testing/code/test_excinfo.py | 45 ++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 changelog/1904.bugfix.rst diff --git a/changelog/1904.bugfix.rst b/changelog/1904.bugfix.rst new file mode 100644 index 000000000..3e1a29215 --- /dev/null +++ b/changelog/1904.bugfix.rst @@ -0,0 +1 @@ +Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message). diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 5148dcdbe..9b051332b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -635,7 +635,9 @@ class ExceptionInfo(Generic[E]): showlocals: bool = False, style: "_TracebackStyle" = "long", abspath: bool = False, - tbfilter: bool = True, + tbfilter: Union[ + bool, Callable[["ExceptionInfo[BaseException]"], Traceback] + ] = True, funcargs: bool = False, truncate_locals: bool = True, chain: bool = True, @@ -652,9 +654,15 @@ class ExceptionInfo(Generic[E]): :param bool abspath: If paths should be changed to absolute or left unchanged. - :param bool tbfilter: - Hide entries that contain a local variable ``__tracebackhide__==True``. - Ignored if ``style=="native"``. + :param tbfilter: + A filter for traceback entries. + + * If false, don't hide any entries. + * If true, hide internal entries and entries that contain a local + variable ``__tracebackhide__ = True``. + * If a callable, delegates the filtering to the callable. + + Ignored if ``style`` is ``"native"``. :param bool funcargs: Show fixtures ("funcargs" for legacy purposes) per traceback entry. @@ -719,7 +727,7 @@ class FormattedExcinfo: showlocals: bool = False style: "_TracebackStyle" = "long" abspath: bool = True - tbfilter: bool = True + tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True funcargs: bool = False truncate_locals: bool = True chain: bool = True @@ -881,7 +889,9 @@ class FormattedExcinfo: def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback - if self.tbfilter: + if callable(self.tbfilter): + traceback = self.tbfilter(excinfo) + elif self.tbfilter: traceback = traceback.filter(excinfo) if isinstance(excinfo.value, RecursionError): diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 1a1a47a28..dbd6b0a42 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -450,10 +450,13 @@ class Node(metaclass=NodeMeta): style = "value" if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() + + tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] if self.config.getoption("fulltrace", False): style = "long" + tbfilter = False else: - excinfo.traceback = self._traceback_filter(excinfo) + tbfilter = self._traceback_filter if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? @@ -484,7 +487,7 @@ class Node(metaclass=NodeMeta): abspath=abspath, showlocals=self.config.getoption("showlocals", False), style=style, - tbfilter=False, # pruned already, or in --fulltrace mode. + tbfilter=tbfilter, truncate_locals=truncate_locals, ) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index bda6cd4cd..88aa5f0f1 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1603,3 +1603,48 @@ def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None: result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"]) if tbstyle not in ("line", "native"): result.stdout.fnmatch_lines(["All traceback entries are hidden.*"]) + + +def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None: + """Hidden entries of chained exceptions are not shown (#1904).""" + p = pytester.makepyfile( + """ + def g1(): + __tracebackhide__ = True + str.does_not_exist + + def f3(): + __tracebackhide__ = True + 1 / 0 + + def f2(): + try: + f3() + except Exception: + g1() + + def f1(): + __tracebackhide__ = True + f2() + + def test(): + f1() + """ + ) + result = pytester.runpytest(str(p), "--tb=short") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*.py:11: in f2", + " f3()", + "E ZeroDivisionError: division by zero", + "", + "During handling of the above exception, another exception occurred:", + "*.py:20: in test", + " f1()", + "*.py:13: in f2", + " g1()", + "E AttributeError:*'does_not_exist'", + ], + consecutive=True, + ) From ec752537ea01e46ab51bd2787786ce9a5db99597 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 07:55:13 +0200 Subject: [PATCH 08/14] build(deps): Bump pytest-cov in /testing/plugins_integration (#11051) Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.0.0 to 4.1.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index bcc3ad987..b5a6c6544 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -2,7 +2,7 @@ anyio[curio,trio]==3.6.2 django==4.2.1 pytest-asyncio==0.21.0 pytest-bdd==6.1.1 -pytest-cov==4.0.0 +pytest-cov==4.1.0 pytest-django==4.5.2 pytest-flakes==4.0.5 pytest-html==3.2.0 From fbfd4b50050080413c8faca5368b9cb9b1ac9313 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 07:55:42 +0200 Subject: [PATCH 09/14] build(deps): Bump anyio[curio,trio] in /testing/plugins_integration (#11050) Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 3.6.2 to 3.7.0. - [Changelog](https://github.com/agronholm/anyio/blob/3.7.0/docs/versionhistory.rst) - [Commits](https://github.com/agronholm/anyio/compare/3.6.2...3.7.0) --- updated-dependencies: - dependency-name: anyio[curio,trio] dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index b5a6c6544..0d30ab96f 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,4 +1,4 @@ -anyio[curio,trio]==3.6.2 +anyio[curio,trio]==3.7.0 django==4.2.1 pytest-asyncio==0.21.0 pytest-bdd==6.1.1 From fc538c5766a1c67bfcd704288279ceac5e20070a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 29 May 2023 22:38:36 +0300 Subject: [PATCH 10/14] cacheprovider: fix file-skipping feature for files in packages `--lf` has a feature where if a certain `Module` (python file) does not contain any failed tests, it is skipped entirely at the collector level instead of being collected and each item skipped individually. When this happens the collection summary looks like this: run-last-failure: rerun previous 1 failure (skipped 1 file) However, this feature didn't work for `Module`s inside of `Package`s, only for those directly beneath the `Session`. Fix #11054. --- changelog/11054.bugfix.rst | 1 + src/_pytest/cacheprovider.py | 2 +- testing/test_cacheprovider.py | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 changelog/11054.bugfix.rst diff --git a/changelog/11054.bugfix.rst b/changelog/11054.bugfix.rst new file mode 100644 index 000000000..a8ee04fe3 --- /dev/null +++ b/changelog/11054.bugfix.rst @@ -0,0 +1 @@ +Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files). diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 89a4a55f8..84ca2c688 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -219,7 +219,7 @@ class LFPluginCollWrapper: @hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector: nodes.Collector): - if isinstance(collector, Session): + if isinstance(collector, (Session, Package)): out = yield res: CollectReport = out.get_result() diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index ee2fe1845..7c6606e2b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -420,7 +420,13 @@ class TestLastFailed: result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 failed in*"]) - def test_terminal_report_lastfailed(self, pytester: Pytester) -> None: + @pytest.mark.parametrize("parent", ("session", "package")) + def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None: + if parent == "package": + pytester.makepyfile( + __init__="", + ) + test_a = pytester.makepyfile( test_a=""" def test_a1(): pass From 7c231baa6400a653eb322705ffffb6385dead3dc Mon Sep 17 00:00:00 2001 From: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> Date: Tue, 30 May 2023 06:06:13 -0400 Subject: [PATCH 11/14] Add warning when testpaths is set but paths are not found by glob (#11044) Closes #11013 --------- Co-authored-by: Ran Benita --- AUTHORS | 1 + changelog/11013.improvement.rst | 1 + src/_pytest/config/__init__.py | 9 +++++++++ testing/test_warnings.py | 14 ++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 changelog/11013.improvement.rst diff --git a/AUTHORS b/AUTHORS index 309088707..77d647126 100644 --- a/AUTHORS +++ b/AUTHORS @@ -197,6 +197,7 @@ Justice Ndou Justyna Janczyszyn Kale Kundert Kamran Ahmad +Kenny Y Karl O. Pinc Karthikeyan Singaravelan Katarzyna Jachim diff --git a/changelog/11013.improvement.rst b/changelog/11013.improvement.rst new file mode 100644 index 000000000..fe3ece93c --- /dev/null +++ b/changelog/11013.improvement.rst @@ -0,0 +1 @@ +Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6df06f7b2..23b17b9a6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1382,6 +1382,15 @@ class Config: args = [] for path in testpaths: args.extend(sorted(glob.iglob(path, recursive=True))) + if testpaths and not args: + warning_text = ( + "No files were found in testpaths; " + "consider removing or adjusting your testpaths configuration. " + "Searching recursively from the current directory instead." + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3 + ) if not args: source = Config.ArgsSource.INCOVATION_DIR args = [str(self.invocation_params.dir)] diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 7b716bb45..a1ecba247 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -777,6 +777,20 @@ class TestStackLevel: ) +def test_warning_on_testpaths_not_found(pytester: Pytester) -> None: + # Check for warning when testpaths set, but not found by glob + pytester.makeini( + """ + [pytest] + testpaths = absent + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + ["*ConfigWarning: No files were found in testpaths*", "*1 warning*"] + ) + + def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None: # Some platforms (notably PyPy) don't have tracemalloc. # We choose to explicitly not skip this in case tracemalloc is not From 4da9026766b3b6586a12a815a9713457c6567ec1 Mon Sep 17 00:00:00 2001 From: theirix Date: Tue, 30 May 2023 15:35:33 +0300 Subject: [PATCH 12/14] Handle microseconds with custom logging.Formatter (#11047) Added handling of %f directive to print microseconds in log format options, such as log-date-format. It is impossible to do with a standard logging.Formatter because it uses time.strftime which doesn't know about milliseconds and %f. In this PR I added a custom Formatter which converts LogRecord to a datetime.datetime object and formats it with %f flag. This behaviour is enabled only if a microsecond flag is specified in a format string. Also added a few tests to check the standard and changed behavior. Closes #10991 --- AUTHORS | 1 + changelog/10991.improvement.rst | 1 + src/_pytest/logging.py | 28 ++++++++- testing/logging/test_reporting.py | 97 +++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 changelog/10991.improvement.rst diff --git a/AUTHORS b/AUTHORS index 77d647126..42936552f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -131,6 +131,7 @@ Eric Siegerman Erik Aronesty Erik M. Bray Evan Kepner +Evgeny Seliverstov Fabien Zarifian Fabio Zadrozny Felix HofstÀtter diff --git a/changelog/10991.improvement.rst b/changelog/10991.improvement.rst new file mode 100644 index 000000000..768c08e55 --- /dev/null +++ b/changelog/10991.improvement.rst @@ -0,0 +1 @@ +Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 95774dd14..6381e86b0 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -5,7 +5,11 @@ import os import re from contextlib import contextmanager from contextlib import nullcontext +from datetime import datetime +from datetime import timedelta +from datetime import timezone from io import StringIO +from logging import LogRecord from pathlib import Path from typing import AbstractSet from typing import Dict @@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str: return _ANSI_ESCAPE_SEQ.sub("", text) -class ColoredLevelFormatter(logging.Formatter): +class DatetimeFormatter(logging.Formatter): + """A logging formatter which formats record with + :func:`datetime.datetime.strftime` formatter instead of + :func:`time.strftime` in case of microseconds in format string. + """ + + def formatTime(self, record: LogRecord, datefmt=None) -> str: + if datefmt and "%f" in datefmt: + ct = self.converter(record.created) + tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone) + # Construct `datetime.datetime` object from `struct_time` + # and msecs information from `record` + dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz) + return dt.strftime(datefmt) + # Use `logging.Formatter` for non-microsecond formats + return super().formatTime(record, datefmt) + + +class ColoredLevelFormatter(DatetimeFormatter): """A logging formatter which colorizes the %(levelname)..s part of the log format passed to __init__.""" @@ -625,7 +647,7 @@ class LoggingPlugin: config, "log_file_date_format", "log_date_format" ) - log_file_formatter = logging.Formatter( + log_file_formatter = DatetimeFormatter( log_file_format, datefmt=log_file_date_format ) self.log_file_handler.setFormatter(log_file_formatter) @@ -669,7 +691,7 @@ class LoggingPlugin: create_terminal_writer(self._config), log_format, log_date_format ) else: - formatter = logging.Formatter(log_format, log_date_format) + formatter = DatetimeFormatter(log_format, log_date_format) formatter._style = PercentStyleMultiline( formatter._style._fmt, auto_indent=auto_indent diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index ae2f53277..14b77236a 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1234,3 +1234,100 @@ def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None: "WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed." ) assert not result.stderr.lines + + +def test_without_date_format_log(pytester: Pytester) -> None: + """Check that date is not printed by default.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines( + ["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"] + ) + + +def test_date_format_log(pytester: Pytester) -> None: + """Check that log_date_format affects output.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + pytester.makeini( + """ + [pytest] + log_format=%(asctime)s; %(levelname)s; %(message)s + log_date_format=%Y-%m-%d %H:%M:%S + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"]) + + +def test_date_format_percentf_log(pytester: Pytester) -> None: + """Make sure that microseconds are printed in log.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + pytester.makeini( + """ + [pytest] + log_format=%(asctime)s; %(levelname)s; %(message)s + log_date_format=%Y-%m-%d %H:%M:%S.%f + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"]) + + +def test_date_format_percentf_tz_log(pytester: Pytester) -> None: + """Make sure that timezone and microseconds are properly formatted together.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + pytester.makeini( + """ + [pytest] + log_format=%(asctime)s; %(levelname)s; %(message)s + log_date_format=%Y-%m-%d %H:%M:%S.%f%z + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines( + [r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"] + ) From 9e1add75f771904ac124bbd0e313a77febe62ce4 Mon Sep 17 00:00:00 2001 From: Alessio Izzo Date: Tue, 30 May 2023 16:59:24 +0200 Subject: [PATCH 13/14] Fix warlus operator behavior when called by a function (#11041) In #10758 we introduced the support for the use of the walrus operator in the test cases. There was a case which was not handled that caused a bug report #11028. This PR aims to fix the issue and also to improve how the walrus operator is handled in the AssertionRewriter class. Closes #11028 --- changelog/11028.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 25 +++++++-- testing/test_assertrewrite.py | 90 ++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 changelog/11028.bugfix.rst diff --git a/changelog/11028.bugfix.rst b/changelog/11028.bugfix.rst new file mode 100644 index 000000000..9efc04ba6 --- /dev/null +++ b/changelog/11028.bugfix.rst @@ -0,0 +1 @@ +Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 8b1823470..ab8169da2 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -996,7 +996,9 @@ class AssertionRewriter(ast.NodeVisitor): ] ): pytest_temp = self.variable() - self.variables_overwrite[v.left.target.id] = pytest_temp + self.variables_overwrite[ + v.left.target.id + ] = v.left # type:ignore[assignment] v.left.target.id = pytest_temp self.push_format_context() res, expl = self.visit(v) @@ -1037,10 +1039,19 @@ class AssertionRewriter(ast.NodeVisitor): new_args = [] new_kwargs = [] for arg in call.args: + if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite: + arg = self.variables_overwrite[arg.id] # type:ignore[assignment] res, expl = self.visit(arg) arg_expls.append(expl) new_args.append(res) for keyword in call.keywords: + if ( + isinstance(keyword.value, ast.Name) + and keyword.value.id in self.variables_overwrite + ): + keyword.value = self.variables_overwrite[ + keyword.value.id + ] # type:ignore[assignment] res, expl = self.visit(keyword.value) new_kwargs.append(ast.keyword(keyword.arg, res)) if keyword.arg: @@ -1075,7 +1086,13 @@ class AssertionRewriter(ast.NodeVisitor): self.push_format_context() # We first check if we have overwritten a variable in the previous assert if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite: - comp.left.id = self.variables_overwrite[comp.left.id] + comp.left = self.variables_overwrite[ + comp.left.id + ] # type:ignore[assignment] + if isinstance(comp.left, namedExpr): + self.variables_overwrite[ + comp.left.target.id + ] = comp.left # type:ignore[assignment] left_res, left_expl = self.visit(comp.left) if isinstance(comp.left, (ast.Compare, ast.BoolOp)): left_expl = f"({left_expl})" @@ -1093,7 +1110,9 @@ class AssertionRewriter(ast.NodeVisitor): and next_operand.target.id == left_res.id ): next_operand.target.id = self.variable() - self.variables_overwrite[left_res.id] = next_operand.target.id + self.variables_overwrite[ + left_res.id + ] = next_operand # type:ignore[assignment] next_res, next_expl = self.visit(next_operand) if isinstance(next_operand, (ast.Compare, ast.BoolOp)): next_expl = f"({next_expl})" diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8d9441403..245241af2 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1436,6 +1436,96 @@ class TestIssue10743: assert result.ret == 0 +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="walrus operator not available in py<38" +) +class TestIssue11028: + def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_in_string(): + assert (obj := "foo") in obj + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_in_operand_json_dumps( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + import json + + def test_json_encoder(): + assert (obj := "foo") in json.dumps(obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_equals_operand_function( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def f(a): + return a + + def test_call_other_function_arg(): + assert (obj := "foo") == f(obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_equals_operand_function_keyword_arg( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def f(a='test'): + return a + + def test_call_other_function_k_arg(): + assert (obj := "foo") == f(a=obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_equals_operand_function_arg_as_function( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def f(a='test'): + return a + + def test_function_of_function(): + assert (obj := "foo") == f(f(obj)) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_gt_operand_function( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def add_one(a): + return a + 1 + + def test_gt(): + assert (obj := 4) > add_one(obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"]) + + @pytest.mark.skipif( sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems" ) From 4a1bba25b9ad23dccc00c695ffb4050b6ddb4189 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 27 May 2023 18:42:37 +0300 Subject: [PATCH 14/14] config: fallback confcutdir to rootpath if inipath is not set Currently, if `--confcutdir` is not set, `inipath.parent` is used, and if `initpath` is not set, then `confcutdir` is None, which means there is no cutoff. Having no cutoff is not great, it means we potentially start probing stuff all the way up to the filesystem root directory. So let's add another fallback, to `rootpath`, which is always something reasonable. --- changelog/11043.improvement.rst | 3 +++ src/_pytest/config/__init__.py | 7 +++++-- testing/test_config.py | 17 +++++++++++++++++ testing/test_conftest.py | 8 +++++++- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 changelog/11043.improvement.rst diff --git a/changelog/11043.improvement.rst b/changelog/11043.improvement.rst new file mode 100644 index 000000000..1fe0361d7 --- /dev/null +++ b/changelog/11043.improvement.rst @@ -0,0 +1,3 @@ +When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`. +Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem. +If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6df06f7b2..c62be6135 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1261,8 +1261,11 @@ class Config: _pytest.deprecated.STRICT_OPTION, stacklevel=2 ) - if self.known_args_namespace.confcutdir is None and self.inipath is not None: - confcutdir = str(self.inipath.parent) + if self.known_args_namespace.confcutdir is None: + if self.inipath is not None: + confcutdir = str(self.inipath.parent) + else: + confcutdir = str(self.rootpath) self.known_args_namespace.confcutdir = confcutdir try: self.hook.pytest_load_initial_conftests( diff --git a/testing/test_config.py b/testing/test_config.py index 1291e85f9..cdeb67ace 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -179,6 +179,23 @@ class TestParseIni: assert result.ret != 0 result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*") + def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None: + # If --confcutdir is not specified, and there is no configfile, default + # to the roothpath. + sub = pytester.mkdir("sub") + os.chdir(sub) + config = pytester.parseconfigure() + assert config.pluginmanager._confcutdir == sub + + def test_confcutdir_default_with_configfile(self, pytester: Pytester) -> None: + # If --confcutdir is not specified, and there is a configfile, default + # to the configfile's directory. + pytester.makeini("[pytest]") + sub = pytester.mkdir("sub") + os.chdir(sub) + config = pytester.parseconfigure() + assert config.pluginmanager._confcutdir == pytester.path + @pytest.mark.xfail(reason="probably not needed") def test_confcutdir(self, pytester: Pytester) -> None: sub = pytester.mkdir("sub") diff --git a/testing/test_conftest.py b/testing/test_conftest.py index d6abca536..c64bd11d4 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -594,7 +594,13 @@ class TestConftestVisibility: print("pytestarg : %s" % testarg) print("expected pass : %s" % expect_ntests_passed) os.chdir(dirs[chdir]) - reprec = pytester.inline_run(testarg, "-q", "--traceconfig") + reprec = pytester.inline_run( + testarg, + "-q", + "--traceconfig", + "--confcutdir", + pytester.path, + ) reprec.assertoutcome(passed=expect_ntests_passed)