From bde963553ee9a7d241a50be2f84b6f20ff09ba83 Mon Sep 17 00:00:00 2001 From: Mick Phillips Date: Wed, 15 Jul 2026 17:10:04 -0700 Subject: [PATCH 1/2] Fix traceback in threads when SetTrace used. Testing === The following script returns normamly, where previously it encountered: `ValueError: call stack is not deep enough`. ``` import sys import threading import typing def tracer(frame, event, arg): return tracer def do_check(): isinstance(None, typing.Iterable) sys.settrace(tracer) t = threading.Thread(target=do_check) t.start() t.join() ``` --- src/core/IronPython/Runtime/PythonContext.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/IronPython/Runtime/PythonContext.cs b/src/core/IronPython/Runtime/PythonContext.cs index 32907cec8..ef3dc9f79 100644 --- a/src/core/IronPython/Runtime/PythonContext.cs +++ b/src/core/IronPython/Runtime/PythonContext.cs @@ -3848,9 +3848,9 @@ public PythonTracebackListenersDispatcher(PythonContext parent) { void Debugging.ITraceCallback.OnTraceEvent(Debugging.TraceEventKind kind, string name, string sourceFileName, SourceSpan sourceSpan, Func> scopeCallback, object payload, object customPayload) { var listener = _parent._tracebackListeners.Value; - if (listener == null && _parent.PythonOptions.Tracing) { - // If tracing without sys.set_trace() is enabled, we need to register a dummy traceback listener, - // because of the FunctionStack handling done there. + if (listener == null && (_parent.PythonOptions.Tracing || _parent._tracebackListenersCount > 0)) { + // If tracing is enabled (globally or via SetTrace on any thread), we need to register a dummy + // traceback listener for this thread, because of the FunctionStack handling done there. _parent._tracebackListeners.Value = listener = new PythonTracebackListener(_parent, null); } From 2d099d3a2383c7f384ce7580101db21f1957f436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Tue, 29 Sep 2026 21:17:23 -0400 Subject: [PATCH 2/2] Fix regression --- src/core/IronPython/Runtime/PythonContext.cs | 13 ++-- .../Runtime/PythonTracebackListener.cs | 8 ++- .../Cases/IronPythonCasesManifest.ini | 3 + .../test_trace_listener_count.py | 72 +++++++++++++++++++ 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tests/suite/modules/system_related/test_trace_listener_count.py diff --git a/src/core/IronPython/Runtime/PythonContext.cs b/src/core/IronPython/Runtime/PythonContext.cs index ef3dc9f79..c4e4d5e94 100644 --- a/src/core/IronPython/Runtime/PythonContext.cs +++ b/src/core/IronPython/Runtime/PythonContext.cs @@ -3851,7 +3851,7 @@ void Debugging.ITraceCallback.OnTraceEvent(Debugging.TraceEventKind kind, string if (listener == null && (_parent.PythonOptions.Tracing || _parent._tracebackListenersCount > 0)) { // If tracing is enabled (globally or via SetTrace on any thread), we need to register a dummy // traceback listener for this thread, because of the FunctionStack handling done there. - _parent._tracebackListeners.Value = listener = new PythonTracebackListener(_parent, null); + _parent._tracebackListeners.Value = listener = new PythonTracebackListener(_parent, null, isDummyListener: true); } listener?.OnTraceEvent(kind, name, sourceFileName, sourceSpan, scopeCallback, payload, customPayload); @@ -3881,8 +3881,8 @@ internal void SetTrace(object o) { // If CurrentPythonFrame is not null then we're currently inside a traceback, and // enabling trace while inside a traceback is only allowed through sys.call_tracing() var pyThread = PythonOps.GetFunctionStackNoCreate(); - if (pyThread == null || (oldTraceListener == null || !oldTraceListener.InTraceBack)) { - _tracebackListeners.Value = newTraceListener = new PythonTracebackListener(this, o); + if (pyThread == null || oldTraceListener == null || !oldTraceListener.InTraceBack) { + _tracebackListeners.Value = newTraceListener = new PythonTracebackListener(this, o, isDummyListener: false); } } @@ -3890,8 +3890,11 @@ internal void SetTrace(object o) { lock (_codeUpdateLock) { var oldEnableTracing = EnableTracing; - if ((oldTraceListener != null) != (newTraceListener != null)) { - _tracebackListenersCount += (newTraceListener != null) ? 1 : -1; + var oldCounted = oldTraceListener?.IsDummyListener == false; + var newCounted = newTraceListener?.IsDummyListener == false; + + if (oldCounted != newCounted) { + _tracebackListenersCount += newCounted ? 1 : -1; } if (EnableTracing != oldEnableTracing) { diff --git a/src/core/IronPython/Runtime/PythonTracebackListener.cs b/src/core/IronPython/Runtime/PythonTracebackListener.cs index ab96e1027..e8d6b0055 100644 --- a/src/core/IronPython/Runtime/PythonTracebackListener.cs +++ b/src/core/IronPython/Runtime/PythonTracebackListener.cs @@ -21,13 +21,14 @@ internal sealed class PythonTracebackListener : Debugging.ITraceCallback { private TracebackDelegate _traceDispatch; private bool _inTraceBack; private bool _exceptionThrown; - + #if PROFILE_SUPPORT private bool _profile; #endif - internal PythonTracebackListener(PythonContext pythonContext, object traceObject) { + internal PythonTracebackListener(PythonContext pythonContext, object traceObject, bool isDummyListener) { _pythonContext = pythonContext; + IsDummyListener = isDummyListener; if (traceObject != null) { _traceObject = traceObject; @@ -63,6 +64,9 @@ internal bool ExceptionThrown { } } + // dummy listener used to maintain the frame stack + internal bool IsDummyListener { get; } + #if PROFILE_SUPPORT internal void SetProfile(TracebackDelegate traceDispatch) { _traceDispatch = traceDispatch; diff --git a/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini b/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini index 8a4beeb00..3086dccaf 100644 --- a/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini +++ b/tests/IronPython.Tests/Cases/IronPythonCasesManifest.ini @@ -139,3 +139,6 @@ NotParallelSafe=true # Uses fixed file, directory, and environment variable name [IronPython.modules.system_related.test_sys_getframe] IsolationLevel=PROCESS # https://github.com/IronLanguages/ironpython3/issues/489 FullFrames=true + +[IronPython.modules.system_related.test_trace_listener_count] +IsolationLevel=PROCESS diff --git a/tests/suite/modules/system_related/test_trace_listener_count.py b/tests/suite/modules/system_related/test_trace_listener_count.py new file mode 100644 index 000000000..81429a8ac --- /dev/null +++ b/tests/suite/modules/system_related/test_trace_listener_count.py @@ -0,0 +1,72 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +import sys +import threading +import typing + +from iptest import IronPythonTestCase, run_test + +class TraceListenerCountTest(IronPythonTestCase): + def test_typing_lookup_on_thread_while_main_thread_traced(self): + # https://github.com/IronLanguages/ironpython3/issues/2072 + results = [] + + def tracer(frame, event, arg): + return tracer + + def do_check(): + results.append(isinstance(None, typing.Iterable)) + + thread = threading.Thread(target=do_check) + sys.settrace(tracer) + try: + thread.start() + thread.join(5) + self.assertFalse(thread.is_alive()) + finally: + sys.settrace(None) + + self.assertEqual(results, [False]) + + def test_dummy_listener_replaced_by_thread_trace(self): + # https://github.com/IronLanguages/ironpython3/pull/2075 + worker_trace_set = threading.Event() + allow_probe = threading.Event() + probe_events = [] + + def main_trace(frame, event, arg): + return main_trace + + def worker_trace(frame, event, arg): + if frame.f_code.co_name == 'probe': + probe_events.append(event) + return worker_trace + + def worker(): + try: + sys.settrace(worker_trace) + worker_trace_set.set() + allow_probe.wait() + + namespace = {} + exec('def probe():\n pass', namespace) + namespace['probe']() + finally: + sys.settrace(None) + + thread = threading.Thread(target=worker) + sys.settrace(main_trace) + try: + thread.start() + self.assertTrue(worker_trace_set.wait(5)) + finally: + sys.settrace(None) + allow_probe.set() + thread.join(5) + self.assertFalse(thread.is_alive()) + + self.assertIn('call', probe_events) + +run_test(__name__)