Skip to content

Commit e4d6848

Browse files
ambvchris-eiblStanFromIreland
authored
[3.13] GH-132439: Fix REPL swallowing characters entered with AltGr on cmd.exe (GH-132440) (GH-133460)
(cherry picked from commit 07f416a) Co-authored-by: Chris Eibl <[email protected]> Co-authored-by: Stan Ulbrych <[email protected]>
1 parent a2bf7a0 commit e4d6848

File tree

3 files changed

+234
-9
lines changed

3 files changed

+234
-9
lines changed

Lib/_pyrepl/windows_console.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ def get_event(self, block: bool = True) -> Event | None:
448448

449449
if key == "\r":
450450
# Make enter unix-like
451-
return Event(evt="key", data="\n", raw=b"\n")
451+
return Event(evt="key", data="\n")
452452
elif key_event.wVirtualKeyCode == 8:
453453
# Turn backspace directly into the command
454454
key = "backspace"
@@ -460,9 +460,9 @@ def get_event(self, block: bool = True) -> Event | None:
460460
key = f"ctrl {key}"
461461
elif key_event.dwControlKeyState & ALT_ACTIVE:
462462
# queue the key, return the meta command
463-
self.event_queue.insert(Event(evt="key", data=key, raw=key))
463+
self.event_queue.insert(Event(evt="key", data=key))
464464
return Event(evt="key", data="\033") # keymap.py uses this for meta
465-
return Event(evt="key", data=key, raw=key)
465+
return Event(evt="key", data=key)
466466
if block:
467467
continue
468468

@@ -473,11 +473,15 @@ def get_event(self, block: bool = True) -> Event | None:
473473
continue
474474

475475
if key_event.dwControlKeyState & ALT_ACTIVE:
476-
# queue the key, return the meta command
477-
self.event_queue.insert(Event(evt="key", data=key, raw=raw_key))
478-
return Event(evt="key", data="\033") # keymap.py uses this for meta
479-
480-
return Event(evt="key", data=key, raw=raw_key)
476+
# Do not swallow characters that have been entered via AltGr:
477+
# Windows internally converts AltGr to CTRL+ALT, see
478+
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
479+
if not key_event.dwControlKeyState & CTRL_ACTIVE:
480+
# queue the key, return the meta command
481+
self.event_queue.insert(Event(evt="key", data=key))
482+
return Event(evt="key", data="\033") # keymap.py uses this for meta
483+
484+
return Event(evt="key", data=key)
481485
return self.event_queue.get()
482486

483487
def push_char(self, char: int | bytes) -> None:

Lib/test/test_pyrepl/test_windows_console.py

+220-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
MOVE_DOWN,
2424
ERASE_IN_LINE,
2525
)
26+
import _pyrepl.windows_console as wc
2627
except ImportError:
2728
pass
2829

@@ -340,8 +341,226 @@ def test_multiline_ctrl_z(self):
340341
Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
341342
],
342343
)
343-
reader, _ = self.handle_events_narrow(events)
344+
reader, con = self.handle_events_narrow(events)
344345
self.assertEqual(reader.cxy, (2, 3))
346+
con.restore()
347+
348+
349+
class WindowsConsoleGetEventTests(TestCase):
350+
# Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
351+
VK_BACK = 0x08
352+
VK_RETURN = 0x0D
353+
VK_LEFT = 0x25
354+
VK_7 = 0x37
355+
VK_M = 0x4D
356+
# Used for miscellaneous characters; it can vary by keyboard.
357+
# For the US standard keyboard, the '" key.
358+
# For the German keyboard, the Ä key.
359+
VK_OEM_7 = 0xDE
360+
361+
# State of control keys: https://learn.microsoft.com/en-us/windows/console/key-event-record-str
362+
RIGHT_ALT_PRESSED = 0x0001
363+
RIGHT_CTRL_PRESSED = 0x0004
364+
LEFT_ALT_PRESSED = 0x0002
365+
LEFT_CTRL_PRESSED = 0x0008
366+
ENHANCED_KEY = 0x0100
367+
SHIFT_PRESSED = 0x0010
368+
369+
370+
def get_event(self, input_records, **kwargs) -> Console:
371+
self.console = WindowsConsole(encoding='utf-8')
372+
self.mock = MagicMock(side_effect=input_records)
373+
self.console._read_input = self.mock
374+
self.console._WindowsConsole__vt_support = kwargs.get("vt_support",
375+
False)
376+
event = self.console.get_event(block=False)
377+
return event
378+
379+
def get_input_record(self, unicode_char, vcode=0, control=0):
380+
return wc.INPUT_RECORD(
381+
wc.KEY_EVENT,
382+
wc.ConsoleEvent(KeyEvent=
383+
wc.KeyEvent(
384+
bKeyDown=True,
385+
wRepeatCount=1,
386+
wVirtualKeyCode=vcode,
387+
wVirtualScanCode=0, # not used
388+
uChar=wc.Char(unicode_char),
389+
dwControlKeyState=control
390+
)))
391+
392+
def test_EmptyBuffer(self):
393+
self.assertEqual(self.get_event([None]), None)
394+
self.assertEqual(self.mock.call_count, 1)
395+
396+
def test_WINDOW_BUFFER_SIZE_EVENT(self):
397+
ir = wc.INPUT_RECORD(
398+
wc.WINDOW_BUFFER_SIZE_EVENT,
399+
wc.ConsoleEvent(WindowsBufferSizeEvent=
400+
wc.WindowsBufferSizeEvent(
401+
wc._COORD(0, 0))))
402+
self.assertEqual(self.get_event([ir]), Event("resize", ""))
403+
self.assertEqual(self.mock.call_count, 1)
404+
405+
def test_KEY_EVENT_up_ignored(self):
406+
ir = wc.INPUT_RECORD(
407+
wc.KEY_EVENT,
408+
wc.ConsoleEvent(KeyEvent=
409+
wc.KeyEvent(bKeyDown=False)))
410+
self.assertEqual(self.get_event([ir]), None)
411+
self.assertEqual(self.mock.call_count, 1)
412+
413+
def test_unhandled_events(self):
414+
for event in (wc.FOCUS_EVENT, wc.MENU_EVENT, wc.MOUSE_EVENT):
415+
ir = wc.INPUT_RECORD(
416+
event,
417+
# fake data, nothing is read except bKeyDown
418+
wc.ConsoleEvent(KeyEvent=
419+
wc.KeyEvent(bKeyDown=False)))
420+
self.assertEqual(self.get_event([ir]), None)
421+
self.assertEqual(self.mock.call_count, 1)
422+
423+
def test_enter(self):
424+
ir = self.get_input_record("\r", self.VK_RETURN)
425+
self.assertEqual(self.get_event([ir]), Event("key", "\n"))
426+
self.assertEqual(self.mock.call_count, 1)
427+
428+
def test_backspace(self):
429+
ir = self.get_input_record("\x08", self.VK_BACK)
430+
self.assertEqual(
431+
self.get_event([ir]), Event("key", "backspace"))
432+
self.assertEqual(self.mock.call_count, 1)
433+
434+
def test_m(self):
435+
ir = self.get_input_record("m", self.VK_M)
436+
self.assertEqual(self.get_event([ir]), Event("key", "m"))
437+
self.assertEqual(self.mock.call_count, 1)
438+
439+
def test_M(self):
440+
ir = self.get_input_record("M", self.VK_M, self.SHIFT_PRESSED)
441+
self.assertEqual(self.get_event([ir]), Event("key", "M"))
442+
self.assertEqual(self.mock.call_count, 1)
443+
444+
def test_left(self):
445+
# VK_LEFT is sent as ENHANCED_KEY
446+
ir = self.get_input_record("\x00", self.VK_LEFT, self.ENHANCED_KEY)
447+
self.assertEqual(self.get_event([ir]), Event("key", "left"))
448+
self.assertEqual(self.mock.call_count, 1)
449+
450+
def test_left_RIGHT_CTRL_PRESSED(self):
451+
ir = self.get_input_record(
452+
"\x00", self.VK_LEFT, self.RIGHT_CTRL_PRESSED | self.ENHANCED_KEY)
453+
self.assertEqual(
454+
self.get_event([ir]), Event("key", "ctrl left"))
455+
self.assertEqual(self.mock.call_count, 1)
456+
457+
def test_left_LEFT_CTRL_PRESSED(self):
458+
ir = self.get_input_record(
459+
"\x00", self.VK_LEFT, self.LEFT_CTRL_PRESSED | self.ENHANCED_KEY)
460+
self.assertEqual(
461+
self.get_event([ir]), Event("key", "ctrl left"))
462+
self.assertEqual(self.mock.call_count, 1)
463+
464+
def test_left_RIGHT_ALT_PRESSED(self):
465+
ir = self.get_input_record(
466+
"\x00", self.VK_LEFT, self.RIGHT_ALT_PRESSED | self.ENHANCED_KEY)
467+
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
468+
self.assertEqual(
469+
self.console.get_event(), Event("key", "left"))
470+
# self.mock is not called again, since the second time we read from the
471+
# command queue
472+
self.assertEqual(self.mock.call_count, 1)
473+
474+
def test_left_LEFT_ALT_PRESSED(self):
475+
ir = self.get_input_record(
476+
"\x00", self.VK_LEFT, self.LEFT_ALT_PRESSED | self.ENHANCED_KEY)
477+
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
478+
self.assertEqual(
479+
self.console.get_event(), Event("key", "left"))
480+
self.assertEqual(self.mock.call_count, 1)
481+
482+
def test_m_LEFT_ALT_PRESSED_and_LEFT_CTRL_PRESSED(self):
483+
# For the shift keys, Windows does not send anything when
484+
# ALT and CTRL are both pressed, so let's test with VK_M.
485+
# get_event() receives this input, but does not
486+
# generate an event.
487+
# This is for e.g. an English keyboard layout, for a
488+
# German layout this returns `µ`, see test_AltGr_m.
489+
ir = self.get_input_record(
490+
"\x00", self.VK_M, self.LEFT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
491+
self.assertEqual(self.get_event([ir]), None)
492+
self.assertEqual(self.mock.call_count, 1)
493+
494+
def test_m_LEFT_ALT_PRESSED(self):
495+
ir = self.get_input_record(
496+
"m", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED)
497+
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
498+
self.assertEqual(self.console.get_event(), Event("key", "m"))
499+
self.assertEqual(self.mock.call_count, 1)
500+
501+
def test_m_RIGHT_ALT_PRESSED(self):
502+
ir = self.get_input_record(
503+
"m", vcode=self.VK_M, control=self.RIGHT_ALT_PRESSED)
504+
self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
505+
self.assertEqual(self.console.get_event(), Event("key", "m"))
506+
self.assertEqual(self.mock.call_count, 1)
507+
508+
def test_AltGr_7(self):
509+
# E.g. on a German keyboard layout, '{' is entered via
510+
# AltGr + 7, where AltGr is the right Alt key on the keyboard.
511+
# In this case, Windows automatically sets
512+
# RIGHT_ALT_PRESSED = 0x0001 + LEFT_CTRL_PRESSED = 0x0008
513+
# This can also be entered like
514+
# LeftAlt + LeftCtrl + 7 or
515+
# LeftAlt + RightCtrl + 7
516+
# See https://learn.microsoft.com/en-us/windows/console/key-event-record-str
517+
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
518+
ir = self.get_input_record(
519+
"{", vcode=self.VK_7,
520+
control=self.RIGHT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
521+
self.assertEqual(self.get_event([ir]), Event("key", "{"))
522+
self.assertEqual(self.mock.call_count, 1)
523+
524+
def test_AltGr_m(self):
525+
# E.g. on a German keyboard layout, this yields 'µ'
526+
# Let's use LEFT_ALT_PRESSED and RIGHT_CTRL_PRESSED this
527+
# time, to cover that, too. See above in test_AltGr_7.
528+
ir = self.get_input_record(
529+
"µ", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED | self.RIGHT_CTRL_PRESSED)
530+
self.assertEqual(self.get_event([ir]), Event("key", "µ"))
531+
self.assertEqual(self.mock.call_count, 1)
532+
533+
def test_umlaut_a_german(self):
534+
ir = self.get_input_record("ä", self.VK_OEM_7)
535+
self.assertEqual(self.get_event([ir]), Event("key", "ä"))
536+
self.assertEqual(self.mock.call_count, 1)
537+
538+
# virtual terminal tests
539+
# Note: wVirtualKeyCode, wVirtualScanCode and dwControlKeyState
540+
# are always zero in this case.
541+
# "\r" and backspace are handled specially, everything else
542+
# is handled in "elif self.__vt_support:" in WindowsConsole.get_event().
543+
# Hence, only one regular key ("m") and a terminal sequence
544+
# are sufficient to test here, the real tests happen in test_eventqueue
545+
# and test_keymap.
546+
547+
def test_enter_vt(self):
548+
ir = self.get_input_record("\r")
549+
self.assertEqual(self.get_event([ir], vt_support=True),
550+
Event("key", "\n"))
551+
self.assertEqual(self.mock.call_count, 1)
552+
553+
def test_backspace_vt(self):
554+
ir = self.get_input_record("\x7f")
555+
self.assertEqual(self.get_event([ir], vt_support=True),
556+
Event("key", "backspace", b"\x7f"))
557+
self.assertEqual(self.mock.call_count, 1)
558+
559+
def test_up_vt(self):
560+
irs = [self.get_input_record(x) for x in "\x1b[A"]
561+
self.assertEqual(self.get_event(irs, vt_support=True),
562+
Event(evt='key', data='up', raw=bytearray(b'\x1b[A')))
563+
self.assertEqual(self.mock.call_count, 3)
345564

346565

347566
if __name__ == "__main__":
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix ``PyREPL`` on Windows: characters entered via AltGr are swallowed.
2+
Patch by Chris Eibl.

0 commit comments

Comments
 (0)