Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 07f416a

Browse filesBrowse files
GH-132439: Fix REPL swallowing characters entered with AltGr on cmd.exe (GH-132440)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
1 parent b6c2ef0 commit 07f416a
Copy full SHA for 07f416a

File tree

Expand file treeCollapse file tree

3 files changed

+234
-9
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+234
-9
lines changed

‎Lib/_pyrepl/windows_console.py

Copy file name to clipboardExpand all lines: Lib/_pyrepl/windows_console.py
+12-8Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ def get_event(self, block: bool = True) -> Event | None:
464464

465465
if key == "\r":
466466
# Make enter unix-like
467-
return Event(evt="key", data="\n", raw=b"\n")
467+
return Event(evt="key", data="\n")
468468
elif key_event.wVirtualKeyCode == 8:
469469
# Turn backspace directly into the command
470470
key = "backspace"
@@ -476,9 +476,9 @@ def get_event(self, block: bool = True) -> Event | None:
476476
key = f"ctrl {key}"
477477
elif key_event.dwControlKeyState & ALT_ACTIVE:
478478
# queue the key, return the meta command
479-
self.event_queue.insert(Event(evt="key", data=key, raw=key))
479+
self.event_queue.insert(Event(evt="key", data=key))
480480
return Event(evt="key", data="\033") # keymap.py uses this for meta
481-
return Event(evt="key", data=key, raw=key)
481+
return Event(evt="key", data=key)
482482
if block:
483483
continue
484484

@@ -490,11 +490,15 @@ def get_event(self, block: bool = True) -> Event | None:
490490
continue
491491

492492
if key_event.dwControlKeyState & ALT_ACTIVE:
493-
# queue the key, return the meta command
494-
self.event_queue.insert(Event(evt="key", data=key, raw=raw_key))
495-
return Event(evt="key", data="\033") # keymap.py uses this for meta
496-
497-
return Event(evt="key", data=key, raw=raw_key)
493+
# Do not swallow characters that have been entered via AltGr:
494+
# Windows internally converts AltGr to CTRL+ALT, see
495+
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
496+
if not key_event.dwControlKeyState & CTRL_ACTIVE:
497+
# queue the key, return the meta command
498+
self.event_queue.insert(Event(evt="key", data=key))
499+
return Event(evt="key", data="\033") # keymap.py uses this for meta
500+
501+
return Event(evt="key", data=key)
498502
return self.event_queue.get()
499503

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

‎Lib/test/test_pyrepl/test_windows_console.py

Copy file name to clipboardExpand all lines: Lib/test/test_pyrepl/test_windows_console.py
+220-1Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
MOVE_DOWN,
2525
ERASE_IN_LINE,
2626
)
27+
import _pyrepl.windows_console as wc
2728
except ImportError:
2829
pass
2930

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

356575

357576
if __name__ == "__main__":
+2Lines changed: 2 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.