Skip to content

Commit bace14d

Browse files
chris-eiblStanFromIreland
authored andcommitted
pythonGH-132439: Fix REPL swallowing characters entered with AltGr on cmd.exe (pythonGH-132440)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
1 parent 5359cb0 commit bace14d

File tree

3 files changed

+234
-9
lines changed

3 files changed

+234
-9
lines changed

Lib/_pyrepl/windows_console.py

Lines 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

Lines 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__":
Lines 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy