diff --git a/changelog/7742.bugfix.rst b/changelog/7742.bugfix.rst new file mode 100644 index 000000000..95277ee89 --- /dev/null +++ b/changelog/7742.bugfix.rst @@ -0,0 +1 @@ +Fix INTERNALERROR when accessing locals / globals with faulty ``exec``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 14a4681ab..a6a2c68d2 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -246,10 +246,20 @@ class TracebackEntry: Mostly for internal use. """ - f = self.frame - tbh = f.f_locals.get( - "__tracebackhide__", f.f_globals.get("__tracebackhide__", False) + tbh = ( + False ) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] + for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): + # in normal cases, f_locals and f_globals are dictionaries + # however via `exec(...)` / `eval(...)` they can be other types + # (even incorrect types!). + # as such, we suppress all exceptions while accessing __tracebackhide__ + try: + tbh = maybe_ns_dct["__tracebackhide__"] + except Exception: + pass + else: + break if tbh and callable(tbh): return tbh(None if self._excinfo is None else self._excinfo()) return tbh diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 8eab319ea..84d3b1d01 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1352,6 +1352,19 @@ raise ValueError() ) assert out == expected_out + def test_exec_type_error_filter(self, importasmod): + """See #7742""" + mod = importasmod( + """\ + def f(): + exec("a = 1", {}, []) + """ + ) + with pytest.raises(TypeError) as excinfo: + mod.f() + # previously crashed with `AttributeError: list has no attribute get` + excinfo.traceback.filter() + @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])