Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion Lib/idlelib/idle_test/test_multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from idlelib import multicall
import unittest
from test.support import requires
from test.support import requires, captured_stderr
from tkinter import Tk, Text


Expand Down Expand Up @@ -43,6 +43,36 @@ def test_yview(self):
mctext = self.mc(self.root)
self.assertIs(mctext.yview.__func__, Text.yview)

def test_valid_binding(self):
# A valid key binding must bind without a warning (cf. gh-55646).
mctext = self.mc(self.root)
with captured_stderr() as stderr:
mctext.event_add('<<test-good>>', '<Control-Key-Up>')
mctext.bind('<<test-good>>', lambda e: None)
self.assertEqual(stderr.getvalue(), '')

def test_invalid_triplet_binding(self):
# gh-55646: '<Control-Key-up>' parses into a triplet on every platform,
# so Tk rejects it in bind().
mctext = self.mc(self.root)
with captured_stderr() as stderr:
mctext.event_add('<<test-bad>>', '<Control-Key-up>') # Must not raise.
mctext.bind('<<test-bad>>', lambda e: None) # Must not raise.
warning = stderr.getvalue()
self.assertIn('invalid key binding', warning)
self.assertIn('test-bad', warning) # The offending action is named.

def test_invalid_nontriplet_binding(self):
# gh-55646: '<Foo-Key-Up>' has no valid modifier, so MultiCall does not
# parse it and falls back to Tk's event_add, which rejects it.
mctext = self.mc(self.root)
with captured_stderr() as stderr:
mctext.event_add('<<test-bad>>', '<Foo-Key-Up>') # Must not raise.
mctext.bind('<<test-bad>>', lambda e: None) # Must not raise.
warning = stderr.getvalue()
self.assertIn('invalid key binding', warning)
self.assertIn('test-bad', warning) # The offending action is named.

def test_event_delete_unbound_sequence(self):
# gh-89360: deleting a sequence that was not added to a virtual
# event is ignored instead of raising ValueError.
Expand Down
35 changes: 32 additions & 3 deletions Lib/idlelib/multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,17 @@ def _triplet_to_sequence(triplet):
else:
return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>'


def warn_bad_binding(virtual, sequence, err):
# gh-55646: warn instead of crashing on an invalid key binding.
action = virtual[2:-2] if virtual[:2] == '<<' and virtual[-2:] == '>>' \
else virtual
print(f'Warning: ignoring invalid key binding {sequence!r} '
f'for {action!r}: {err}. '
f'Please reconfigure it in the IDLE Settings dialog.',
file=sys.stderr)


_multicall_dict = {}
def MultiCallCreator(widget):
"""Return a MultiCall class which inherits its methods from the
Expand Down Expand Up @@ -343,8 +354,17 @@ def bind(self, sequence=None, func=None, add=None):
self.__binders[triplet[1]].unbind(triplet, ei[0])
ei[0] = func
if ei[0] is not None:
bad = []
for triplet in ei[1]:
self.__binders[triplet[1]].bind(triplet, func)
try:
self.__binders[triplet[1]].bind(triplet, func)
except tkinter.TclError as err:
warn_bad_binding(sequence,
_triplet_to_sequence(triplet),
err)
bad.append(triplet)
for triplet in bad: # Drop the invalid sequences.
ei[1].remove(triplet)
else:
self.__eventinfo[sequence] = [func, []]
return widget.bind(self, sequence, func, add)
Expand All @@ -371,10 +391,19 @@ def event_add(self, virtual, *sequences):
triplet = _parse_sequence(seq)
if triplet is None:
#print("Tkinter event_add(%s)" % seq, file=sys.__stderr__)
widget.event_add(self, virtual, seq)
try:
widget.event_add(self, virtual, seq)
except tkinter.TclError as err:
warn_bad_binding(virtual, seq, err)
continue # Drop the invalid sequence.
else:
if func is not None:
self.__binders[triplet[1]].bind(triplet, func)
try:
self.__binders[triplet[1]].bind(triplet, func)
except tkinter.TclError as err:
warn_bad_binding(virtual,
_triplet_to_sequence(triplet), err)
continue # Drop the invalid sequence.
triplets.append(triplet)

def event_delete(self, virtual, *sequences):
Expand Down
74 changes: 74 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,80 @@ def test_self_trace(self):
self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace")
self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py"))

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_empty_native_thread_stack(self):
_testcapi = import_module("_testcapi")
lock = threading.Lock()
lock.acquire()
# A built-in callback leaves the C thread's Python stack empty.
_testcapi.call_in_temporary_c_thread(lock.acquire, False)
try:
for cache_frames, native in ((False, False), (False, True),
(True, False), (True, True)):
with self.subTest(cache_frames=cache_frames, native=native):
unwinder = RemoteUnwinder(
os.getpid(), all_threads=True, cache_frames=cache_frames,
native=native,
)
_get_stack_trace_with_retry(
unwinder, condition=lambda trace: len(trace[0].threads) == 2,
)
threads = unwinder.get_stack_trace()[0].threads
native_stack, python_stack = sorted(
(thread.frame_info for thread in threads), key=len,
)
self.assertEqual(native_stack, [])
self.assertEqual(
python_stack[0].funcname,
"TestGetStackTrace.test_empty_native_thread_stack",
)
finally:
lock.release()
_testcapi.join_temporary_c_thread()

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_popping_python_frame_is_not_native(self):
script = """\
def leaf(depth):
if depth:
leaf(depth - 1)

while True:
leaf(300)
"""
with _managed_subprocess([sys.executable, "-c", script]) as process:
for _ in busy_retry(SHORT_TIMEOUT):
try:
unwinder = RemoteUnwinder(
process.pid, native=True, gc=False, cache_frames=False,
)
except RuntimeError:
continue
break
samples = 0
for _ in range(10_000):
try:
threads = unwinder.get_stack_trace()[0].threads
except TRANSIENT_ERRORS:
continue
if not threads:
continue
frames = threads[0].frame_info
names = [frame.funcname for frame in frames]
if "leaf" not in names:
continue
samples += 1
self.assertNotIn(("leaf", "<native>"), zip(names, names[1:]))
self.assertGreater(samples, 1000)

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix IDLE crash at startup when the user configuration contains an invalid key
binding, such as ``<Alt-Key-up>`` instead of ``<Alt-Key-Up>``. The invalid
binding is now ignored with a warning.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :mod:`profiling.sampling` failing when a native thread has an empty Python
stack.
22 changes: 13 additions & 9 deletions Modules/_remote_debugging/frames.c
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,23 @@ find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr)
* FRAME PARSING FUNCTIONS
* ============================================================================ */

enum { FRAME_PARSE_INTERPRETER = 2 };

int
is_frame_valid(
RemoteUnwinderObject *unwinder,
uintptr_t frame_addr,
uintptr_t code_object_addr
) {
if ((void*)code_object_addr == NULL) {
return 0;
return 0; // Frame being cleared
}

void* frame = (void*)frame_addr;

char owner = GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner);
if (owner == FRAME_OWNED_BY_INTERPRETER) {
return 0; // C frame or sentinel base frame
return FRAME_PARSE_INTERPRETER; // C frame or sentinel base frame
}

if (owner != FRAME_OWNED_BY_GENERATOR && owner != FRAME_OWNED_BY_THREAD) {
Expand Down Expand Up @@ -313,6 +315,7 @@ process_frame_chain(
ctx->last_frame_visited = 0;

while ((void*)frame_addr != NULL) {
int parse_result = 0;
PyObject *frame = NULL;
uintptr_t next_frame_addr = 0;
uintptr_t stackpointer = 0;
Expand All @@ -326,14 +329,15 @@ process_frame_chain(
assert(frame_count <= MAX_FRAMES);

if (ctx->chunks && ctx->chunks->count > 0) {
if (parse_frame_from_chunks(unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks) == 0) {
parse_result = parse_frame_from_chunks(
unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks);
if (parse_result == 0) {
goto parsed_frame;
}
PyErr_Clear();
}
{
uintptr_t address_of_code_object = 0;
int parse_result;
if (ctx->prefetch.frame && ctx->prefetch.frame_addr == frame_addr) {
parse_result = parse_frame_buffer(
unwinder, &frame, ctx->prefetch.frame,
Expand All @@ -358,19 +362,19 @@ process_frame_chain(
continue;
}

if (frame == NULL && PyList_GET_SIZE(ctx->frame_info) == 0) {
const char *e = "Failed to parse initial frame in chain";
PyErr_SetString(PyExc_RuntimeError, e);
return -1;
}
PyObject *extra_frame = NULL;
if (unwinder->gc && frame_addr == ctx->gc_frame) {
_Py_DECLARE_STR(gc, "<GC>");
extra_frame = &_Py_STR(gc);
}
// A leading frame without Python code marks no transition between
// Python frames: it is a frame being popped or C code the thread is
// returning into.
else if (unwinder->native &&
frame == NULL &&
parse_result == FRAME_PARSE_INTERPRETER &&
next_frame_addr &&
PyList_GET_SIZE(ctx->frame_info) > 0 &&
!(unwinder->gc && next_frame_addr == ctx->gc_frame))
{
_Py_DECLARE_STR(native, "<native>");
Expand Down
Loading