Skip to content

Commit 3662540

Browse files
chris-eiblStanFromIreland
authored andcommitted
[3.13] pythonGH-132439: Fix REPL swallowing characters entered with AltGr on cmd.exe (pythonGH-132440)
(cherry picked from commit 07f416a) Co-authored-by: Chris Eibl <138194463+chris-eibl@users.noreply.github.com> Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
1 parent a2bf7a0 commit 3662540

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
@@ -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

Lines changed: 220 additions & 1 deletion
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__":
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