https://github.com/python/cpython/commit/07f416a3f063db6b91b8b99ff61a51b64b0503f1
commit: 07f416a3f063db6b91b8b99ff61a51b64b0503f1
branch: main
author: Chris Eibl <138194463+chris-e...@users.noreply.github.com>
committer: ambv <luk...@langa.pl>
date: 2025-05-05T18:45:45+02:00
summary:

GH-132439: Fix REPL swallowing characters entered with AltGr on cmd.exe 
(GH-132440)

Co-authored-by: Stan Ulbrych <89152624+stanfromirel...@users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2025-04-12-16-29-42.gh-issue-132439.3twrU6.rst
M Lib/_pyrepl/windows_console.py
M Lib/test/test_pyrepl/test_windows_console.py

diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py
index 587c6137f885c0..a294d627b0efb8 100644
--- a/Lib/_pyrepl/windows_console.py
+++ b/Lib/_pyrepl/windows_console.py
@@ -464,7 +464,7 @@ def get_event(self, block: bool = True) -> Event | None:
 
             if key == "\r":
                 # Make enter unix-like
-                return Event(evt="key", data="\n", raw=b"\n")
+                return Event(evt="key", data="\n")
             elif key_event.wVirtualKeyCode == 8:
                 # Turn backspace directly into the command
                 key = "backspace"
@@ -476,9 +476,9 @@ def get_event(self, block: bool = True) -> Event | None:
                         key = f"ctrl {key}"
                     elif key_event.dwControlKeyState & ALT_ACTIVE:
                         # queue the key, return the meta command
-                        self.event_queue.insert(Event(evt="key", data=key, 
raw=key))
+                        self.event_queue.insert(Event(evt="key", data=key))
                         return Event(evt="key", data="\033")  # keymap.py uses 
this for meta
-                    return Event(evt="key", data=key, raw=key)
+                    return Event(evt="key", data=key)
                 if block:
                     continue
 
@@ -490,11 +490,15 @@ def get_event(self, block: bool = True) -> Event | None:
                 continue
 
             if key_event.dwControlKeyState & ALT_ACTIVE:
-                # queue the key, return the meta command
-                self.event_queue.insert(Event(evt="key", data=key, 
raw=raw_key))
-                return Event(evt="key", data="\033")  # keymap.py uses this 
for meta
-
-            return Event(evt="key", data=key, raw=raw_key)
+                # Do not swallow characters that have been entered via AltGr:
+                # Windows internally converts AltGr to CTRL+ALT, see
+                # 
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
+                if not key_event.dwControlKeyState & CTRL_ACTIVE:
+                    # queue the key, return the meta command
+                    self.event_queue.insert(Event(evt="key", data=key))
+                    return Event(evt="key", data="\033")  # keymap.py uses 
this for meta
+
+            return Event(evt="key", data=key)
         return self.event_queue.get()
 
     def push_char(self, char: int | bytes) -> None:
diff --git a/Lib/test/test_pyrepl/test_windows_console.py 
b/Lib/test/test_pyrepl/test_windows_console.py
index e95fec46a851ee..ca90a7058149eb 100644
--- a/Lib/test/test_pyrepl/test_windows_console.py
+++ b/Lib/test/test_pyrepl/test_windows_console.py
@@ -24,6 +24,7 @@
         MOVE_DOWN,
         ERASE_IN_LINE,
     )
+    import _pyrepl.windows_console as wc
 except ImportError:
     pass
 
@@ -350,8 +351,226 @@ def test_multiline_ctrl_z(self):
                 Event(evt="key", data='\x1a', raw=bytearray(b'\x1a')),
             ],
         )
-        reader, _ = self.handle_events_narrow(events)
+        reader, con = self.handle_events_narrow(events)
         self.assertEqual(reader.cxy, (2, 3))
+        con.restore()
+
+
+class WindowsConsoleGetEventTests(TestCase):
+    # Virtual-Key Codes: 
https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
+    VK_BACK = 0x08
+    VK_RETURN = 0x0D
+    VK_LEFT = 0x25
+    VK_7 = 0x37
+    VK_M = 0x4D
+    # Used for miscellaneous characters; it can vary by keyboard.
+    # For the US standard keyboard, the '" key.
+    # For the German keyboard, the Ä key.
+    VK_OEM_7 = 0xDE
+
+    # State of control keys: 
https://learn.microsoft.com/en-us/windows/console/key-event-record-str
+    RIGHT_ALT_PRESSED = 0x0001
+    RIGHT_CTRL_PRESSED = 0x0004
+    LEFT_ALT_PRESSED = 0x0002
+    LEFT_CTRL_PRESSED = 0x0008
+    ENHANCED_KEY = 0x0100
+    SHIFT_PRESSED = 0x0010
+
+
+    def get_event(self, input_records, **kwargs) -> Console:
+        self.console = WindowsConsole(encoding='utf-8')
+        self.mock = MagicMock(side_effect=input_records)
+        self.console._read_input = self.mock
+        self.console._WindowsConsole__vt_support = kwargs.get("vt_support",
+                                                              False)
+        event = self.console.get_event(block=False)
+        return event
+
+    def get_input_record(self, unicode_char, vcode=0, control=0):
+        return wc.INPUT_RECORD(
+            wc.KEY_EVENT,
+            wc.ConsoleEvent(KeyEvent=
+                wc.KeyEvent(
+                    bKeyDown=True,
+                    wRepeatCount=1,
+                    wVirtualKeyCode=vcode,
+                    wVirtualScanCode=0, # not used
+                    uChar=wc.Char(unicode_char),
+                    dwControlKeyState=control
+                    )))
+
+    def test_EmptyBuffer(self):
+        self.assertEqual(self.get_event([None]), None)
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_WINDOW_BUFFER_SIZE_EVENT(self):
+        ir = wc.INPUT_RECORD(
+            wc.WINDOW_BUFFER_SIZE_EVENT,
+            wc.ConsoleEvent(WindowsBufferSizeEvent=
+                wc.WindowsBufferSizeEvent(
+                    wc._COORD(0, 0))))
+        self.assertEqual(self.get_event([ir]), Event("resize", ""))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_KEY_EVENT_up_ignored(self):
+        ir = wc.INPUT_RECORD(
+            wc.KEY_EVENT,
+            wc.ConsoleEvent(KeyEvent=
+                wc.KeyEvent(bKeyDown=False)))
+        self.assertEqual(self.get_event([ir]), None)
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_unhandled_events(self):
+        for event in (wc.FOCUS_EVENT, wc.MENU_EVENT, wc.MOUSE_EVENT):
+            ir = wc.INPUT_RECORD(
+                event,
+                # fake data, nothing is read except bKeyDown
+                wc.ConsoleEvent(KeyEvent=
+                    wc.KeyEvent(bKeyDown=False)))
+            self.assertEqual(self.get_event([ir]), None)
+            self.assertEqual(self.mock.call_count, 1)
+
+    def test_enter(self):
+        ir = self.get_input_record("\r", self.VK_RETURN)
+        self.assertEqual(self.get_event([ir]), Event("key", "\n"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_backspace(self):
+        ir = self.get_input_record("\x08", self.VK_BACK)
+        self.assertEqual(
+            self.get_event([ir]), Event("key", "backspace"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_m(self):
+        ir = self.get_input_record("m", self.VK_M)
+        self.assertEqual(self.get_event([ir]), Event("key", "m"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_M(self):
+        ir = self.get_input_record("M", self.VK_M, self.SHIFT_PRESSED)
+        self.assertEqual(self.get_event([ir]), Event("key", "M"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_left(self):
+        # VK_LEFT is sent as ENHANCED_KEY
+        ir = self.get_input_record("\x00", self.VK_LEFT, self.ENHANCED_KEY)
+        self.assertEqual(self.get_event([ir]), Event("key", "left"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_left_RIGHT_CTRL_PRESSED(self):
+        ir = self.get_input_record(
+            "\x00", self.VK_LEFT, self.RIGHT_CTRL_PRESSED | self.ENHANCED_KEY)
+        self.assertEqual(
+            self.get_event([ir]), Event("key", "ctrl left"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_left_LEFT_CTRL_PRESSED(self):
+        ir = self.get_input_record(
+            "\x00", self.VK_LEFT, self.LEFT_CTRL_PRESSED | self.ENHANCED_KEY)
+        self.assertEqual(
+            self.get_event([ir]), Event("key", "ctrl left"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_left_RIGHT_ALT_PRESSED(self):
+        ir = self.get_input_record(
+            "\x00", self.VK_LEFT, self.RIGHT_ALT_PRESSED | self.ENHANCED_KEY)
+        self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
+        self.assertEqual(
+            self.console.get_event(), Event("key", "left"))
+        # self.mock is not called again, since the second time we read from the
+        # command queue
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_left_LEFT_ALT_PRESSED(self):
+        ir = self.get_input_record(
+            "\x00", self.VK_LEFT, self.LEFT_ALT_PRESSED | self.ENHANCED_KEY)
+        self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
+        self.assertEqual(
+            self.console.get_event(), Event("key", "left"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_m_LEFT_ALT_PRESSED_and_LEFT_CTRL_PRESSED(self):
+        # For the shift keys, Windows does not send anything when
+        # ALT and CTRL are both pressed, so let's test with VK_M.
+        # get_event() receives this input, but does not
+        # generate an event.
+        # This is for e.g. an English keyboard layout, for a
+        # German layout this returns `µ`, see test_AltGr_m.
+        ir = self.get_input_record(
+            "\x00", self.VK_M, self.LEFT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
+        self.assertEqual(self.get_event([ir]), None)
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_m_LEFT_ALT_PRESSED(self):
+        ir = self.get_input_record(
+            "m", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED)
+        self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
+        self.assertEqual(self.console.get_event(), Event("key", "m"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_m_RIGHT_ALT_PRESSED(self):
+        ir = self.get_input_record(
+            "m", vcode=self.VK_M, control=self.RIGHT_ALT_PRESSED)
+        self.assertEqual(self.get_event([ir]), Event(evt="key", data="\033"))
+        self.assertEqual(self.console.get_event(), Event("key", "m"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_AltGr_7(self):
+        # E.g. on a German keyboard layout, '{' is entered via
+        # AltGr + 7, where AltGr is the right Alt key on the keyboard.
+        # In this case, Windows automatically sets
+        # RIGHT_ALT_PRESSED = 0x0001 + LEFT_CTRL_PRESSED = 0x0008
+        # This can also be entered like
+        # LeftAlt + LeftCtrl + 7 or
+        # LeftAlt + RightCtrl + 7
+        # See 
https://learn.microsoft.com/en-us/windows/console/key-event-record-str
+        # 
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
+        ir = self.get_input_record(
+            "{", vcode=self.VK_7,
+            control=self.RIGHT_ALT_PRESSED | self.LEFT_CTRL_PRESSED)
+        self.assertEqual(self.get_event([ir]), Event("key", "{"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_AltGr_m(self):
+        # E.g. on a German keyboard layout, this yields 'µ'
+        # Let's use LEFT_ALT_PRESSED and RIGHT_CTRL_PRESSED this
+        # time, to cover that, too. See above in test_AltGr_7.
+        ir = self.get_input_record(
+            "µ", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED | 
self.RIGHT_CTRL_PRESSED)
+        self.assertEqual(self.get_event([ir]), Event("key", "µ"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_umlaut_a_german(self):
+        ir = self.get_input_record("ä", self.VK_OEM_7)
+        self.assertEqual(self.get_event([ir]), Event("key", "ä"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    # virtual terminal tests
+    # Note: wVirtualKeyCode, wVirtualScanCode and dwControlKeyState
+    # are always zero in this case.
+    # "\r" and backspace are handled specially, everything else
+    # is handled in "elif self.__vt_support:" in WindowsConsole.get_event().
+    # Hence, only one regular key ("m") and a terminal sequence
+    # are sufficient to test here, the real tests happen in test_eventqueue
+    # and test_keymap.
+
+    def test_enter_vt(self):
+        ir = self.get_input_record("\r")
+        self.assertEqual(self.get_event([ir], vt_support=True),
+                         Event("key", "\n"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_backspace_vt(self):
+        ir = self.get_input_record("\x7f")
+        self.assertEqual(self.get_event([ir], vt_support=True),
+                         Event("key", "backspace", b"\x7f"))
+        self.assertEqual(self.mock.call_count, 1)
+
+    def test_up_vt(self):
+        irs = [self.get_input_record(x) for x in "\x1b[A"]
+        self.assertEqual(self.get_event(irs, vt_support=True),
+                         Event(evt='key', data='up', raw=bytearray(b'\x1b[A')))
+        self.assertEqual(self.mock.call_count, 3)
 
 
 if __name__ == "__main__":
diff --git 
a/Misc/NEWS.d/next/Library/2025-04-12-16-29-42.gh-issue-132439.3twrU6.rst 
b/Misc/NEWS.d/next/Library/2025-04-12-16-29-42.gh-issue-132439.3twrU6.rst
new file mode 100644
index 00000000000000..8d0778d8d3bc9a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-12-16-29-42.gh-issue-132439.3twrU6.rst
@@ -0,0 +1,2 @@
+Fix ``PyREPL`` on Windows: characters entered via AltGr are swallowed.
+Patch by Chris Eibl.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to