mirror of
https://github.com/python/cpython.git
synced 2025-08-31 14:07:50 +00:00
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>
This commit is contained in:
parent
b6c2ef0c7a
commit
07f416a3f0
3 changed files with 233 additions and 8 deletions
|
@ -464,7 +464,7 @@ class WindowsConsole(Console):
|
|||
|
||||
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 @@ class WindowsConsole(Console):
|
|||
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 @@ class WindowsConsole(Console):
|
|||
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
|
||||
# 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, raw=raw_key)
|
||||
return Event(evt="key", data=key)
|
||||
return self.event_queue.get()
|
||||
|
||||
def push_char(self, char: int | bytes) -> None:
|
||||
|
|
|
@ -24,6 +24,7 @@ try:
|
|||
MOVE_DOWN,
|
||||
ERASE_IN_LINE,
|
||||
)
|
||||
import _pyrepl.windows_console as wc
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
@ -350,8 +351,226 @@ class WindowsConsoleTests(TestCase):
|
|||
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__":
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Fix ``PyREPL`` on Windows: characters entered via AltGr are swallowed.
|
||||
Patch by Chris Eibl.
|
Loading…
Add table
Add a link
Reference in a new issue