Skip to content

Commit b8609bd

Browse files
Address reviews
1 parent 31e35e4 commit b8609bd

File tree

1 file changed

+64
-92
lines changed

1 file changed

+64
-92
lines changed

Lib/getpass.py

Lines changed: 64 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -28,48 +28,36 @@ class GetPassWarning(UserWarning): pass
2828

2929
# Default POSIX control character mappings
3030
_POSIX_CTRL_CHARS = {
31-
'ERASE': b'\x7f', # DEL/Backspace
32-
'KILL': b'\x15', # Ctrl+U - kill line
33-
'WERASE': b'\x17', # Ctrl+W - erase word
34-
'LNEXT': b'\x16', # Ctrl+V - literal next
35-
'EOF': b'\x04', # Ctrl+D - EOF
36-
'INTR': b'\x03', # Ctrl+C - interrupt
37-
'SOH': b'\x01', # Ctrl+A - start of heading (beginning of line)
38-
'ENQ': b'\x05', # Ctrl+E - enquiry (end of line)
39-
'VT': b'\x0b', # Ctrl+K - vertical tab (kill forward)
31+
'ERASE': '\x7f', # DEL/Backspace
32+
'KILL': '\x15', # Ctrl+U - kill line
33+
'WERASE': '\x17', # Ctrl+W - erase word
34+
'LNEXT': '\x16', # Ctrl+V - literal next
35+
'EOF': '\x04', # Ctrl+D - EOF
36+
'INTR': '\x03', # Ctrl+C - interrupt
37+
'SOH': '\x01', # Ctrl+A - start of heading (beginning of line)
38+
'ENQ': '\x05', # Ctrl+E - enquiry (end of line)
39+
'VT': '\x0b', # Ctrl+K - vertical tab (kill forward)
4040
}
4141

4242

4343
def _get_terminal_ctrl_chars(fd):
4444
"""Extract control characters from terminal settings.
4545
46-
Returns a dict mapping control char names to their byte values.
46+
Returns a dict mapping control char names to their str values.
4747
Falls back to POSIX defaults if termios isn't available.
4848
"""
49+
res = _POSIX_CTRL_CHARS.copy()
4950
try:
5051
old = termios.tcgetattr(fd)
5152
cc = old[6] # Index 6 is the control characters array
52-
return {
53-
'ERASE': cc[termios.VERASE] if termios.VERASE < len(cc) else _POSIX_CTRL_CHARS['ERASE'],
54-
'KILL': cc[termios.VKILL] if termios.VKILL < len(cc) else _POSIX_CTRL_CHARS['KILL'],
55-
'WERASE': cc[termios.VWERASE] if termios.VWERASE < len(cc) else _POSIX_CTRL_CHARS['WERASE'],
56-
'LNEXT': cc[termios.VLNEXT] if termios.VLNEXT < len(cc) else _POSIX_CTRL_CHARS['LNEXT'],
57-
'EOF': cc[termios.VEOF] if termios.VEOF < len(cc) else _POSIX_CTRL_CHARS['EOF'],
58-
'INTR': cc[termios.VINTR] if termios.VINTR < len(cc) else _POSIX_CTRL_CHARS['INTR'],
59-
# Ctrl+A/E/K are not in termios, use POSIX defaults
60-
'SOH': _POSIX_CTRL_CHARS['SOH'],
61-
'ENQ': _POSIX_CTRL_CHARS['ENQ'],
62-
'VT': _POSIX_CTRL_CHARS['VT'],
63-
}
6453
except (termios.error, OSError):
65-
return _POSIX_CTRL_CHARS.copy()
66-
67-
68-
def _decode_ctrl_char(char_value):
69-
"""Convert a control character from bytes to str."""
70-
if isinstance(char_value, bytes):
71-
return char_value.decode('latin-1')
72-
return char_value
54+
return res
55+
# Ctrl+A/E/K are not in termios, use POSIX defaults
56+
for name in ('ERASE', 'KILL', 'WERASE', 'LNEXT', 'EOF', 'INTR'):
57+
cap = getattr(termios, f'V{name}')
58+
if cap < len(cc):
59+
res[name] = cc[cap].decode('latin-1')
60+
return res
7361

7462

7563
def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
@@ -248,78 +236,75 @@ def __init__(self, stream, echo_char, ctrl_chars):
248236
self.cursor_pos = 0
249237
self.eof_pressed = False
250238
self.literal_next = False
251-
self.ctrl = {name: _decode_ctrl_char(value)
252-
for name, value in ctrl_chars.items()}
239+
self.ctrl = ctrl_chars
240+
self._dispatch = {
241+
ctrl_chars['SOH']: self._handle_move_start, # Ctrl+A
242+
ctrl_chars['ENQ']: self._handle_move_end, # Ctrl+E
243+
ctrl_chars['VT']: self._handle_kill_forward, # Ctrl+K
244+
ctrl_chars['KILL']: self._handle_kill_line, # Ctrl+U
245+
ctrl_chars['WERASE']: self._handle_erase_word, # Ctrl+W
246+
ctrl_chars['ERASE']: self._handle_erase, # DEL
247+
'\b': self._handle_erase, # Backspace
248+
}
253249

254-
def refresh_display(self):
255-
"""Redraw the entire password line with asterisks."""
256-
self.stream.write('\r' + ' ' * (len(self.passwd) + 20) + '\r')
250+
def _refresh_display(self):
251+
"""Redraw the entire password line with *echo_char*."""
252+
self.stream.write('\r' + ' ' * len(self.passwd) + '\r')
257253
self.stream.write(self.echo_char * len(self.passwd))
258254
if self.cursor_pos < len(self.passwd):
259255
self.stream.write('\b' * (len(self.passwd) - self.cursor_pos))
260256
self.stream.flush()
261257

262-
def erase_chars(self, count):
263-
"""Erase count asterisks from display."""
258+
def _erase_chars(self, count):
259+
"""Erase count echo characters from display."""
264260
self.stream.write("\b \b" * count)
265261

266-
def insert_char(self, char):
262+
def _insert_char(self, char):
267263
"""Insert character at cursor position."""
268264
self.passwd = self.passwd[:self.cursor_pos] + char + self.passwd[self.cursor_pos:]
269265
self.cursor_pos += 1
270266
# Only refresh if inserting in middle
271267
if self.cursor_pos < len(self.passwd):
272-
self.refresh_display()
268+
self._refresh_display()
273269
else:
274270
self.stream.write(self.echo_char)
275271
self.stream.flush()
276272

277-
def handle_literal_next(self, char):
278-
"""Insert next character literally (Ctrl+V)."""
279-
self.insert_char(char)
280-
self.literal_next = False
281-
self.eof_pressed = False
282-
283-
def handle_move_start(self):
273+
def _handle_move_start(self):
284274
"""Move cursor to beginning (Ctrl+A)."""
285275
self.cursor_pos = 0
286-
self.eof_pressed = False
287276

288-
def handle_move_end(self):
277+
def _handle_move_end(self):
289278
"""Move cursor to end (Ctrl+E)."""
290279
self.cursor_pos = len(self.passwd)
291-
self.eof_pressed = False
292280

293-
def handle_erase(self):
281+
def _handle_erase(self):
294282
"""Delete character before cursor (Backspace/DEL)."""
295283
if self.cursor_pos > 0:
296284
self.passwd = self.passwd[:self.cursor_pos-1] + self.passwd[self.cursor_pos:]
297285
self.cursor_pos -= 1
298286
# Only refresh if deleting from middle
299287
if self.cursor_pos < len(self.passwd):
300-
self.refresh_display()
288+
self._refresh_display()
301289
else:
302290
self.stream.write("\b \b")
303291
self.stream.flush()
304-
self.eof_pressed = False
305292

306-
def handle_kill_line(self):
293+
def _handle_kill_line(self):
307294
"""Erase entire line (Ctrl+U)."""
308-
self.erase_chars(len(self.passwd))
295+
self._erase_chars(len(self.passwd))
309296
self.passwd = ""
310297
self.cursor_pos = 0
311298
self.stream.flush()
312-
self.eof_pressed = False
313299

314-
def handle_kill_forward(self):
300+
def _handle_kill_forward(self):
315301
"""Kill from cursor to end (Ctrl+K)."""
316302
chars_to_delete = len(self.passwd) - self.cursor_pos
317303
self.passwd = self.passwd[:self.cursor_pos]
318-
self.erase_chars(chars_to_delete)
304+
self._erase_chars(chars_to_delete)
319305
self.stream.flush()
320-
self.eof_pressed = False
321306

322-
def handle_erase_word(self):
307+
def _handle_erase_word(self):
323308
"""Erase previous word (Ctrl+W)."""
324309
old_cursor = self.cursor_pos
325310
# Skip trailing spaces
@@ -330,20 +315,16 @@ def handle_erase_word(self):
330315
self.cursor_pos -= 1
331316
# Remove the deleted portion
332317
self.passwd = self.passwd[:self.cursor_pos] + self.passwd[old_cursor:]
333-
self.refresh_display()
334-
self.eof_pressed = False
318+
self._refresh_display()
335319

336-
def build_dispatch_table(self):
337-
"""Build dispatch table mapping control chars to handlers."""
338-
return {
339-
self.ctrl['SOH']: self.handle_move_start, # Ctrl+A
340-
self.ctrl['ENQ']: self.handle_move_end, # Ctrl+E
341-
self.ctrl['VT']: self.handle_kill_forward, # Ctrl+K
342-
self.ctrl['KILL']: self.handle_kill_line, # Ctrl+U
343-
self.ctrl['WERASE']: self.handle_erase_word, # Ctrl+W
344-
self.ctrl['ERASE']: self.handle_erase, # DEL
345-
'\b': self.handle_erase, # Backspace
346-
}
320+
def handle(self, char):
321+
"""Handle a single character input. Returns True if handled."""
322+
self.eof_pressed = False
323+
handler = self._dispatch.get(char)
324+
if handler:
325+
handler()
326+
return True
327+
return False
347328

348329

349330
def _readline_with_echo_char(stream, input, echo_char, term_ctrl_chars=None):
@@ -352,43 +333,34 @@ def _readline_with_echo_char(stream, input, echo_char, term_ctrl_chars=None):
352333
term_ctrl_chars = _POSIX_CTRL_CHARS.copy()
353334

354335
editor = _PasswordLineEditor(stream, echo_char, term_ctrl_chars)
355-
dispatch = editor.build_dispatch_table()
356336

357337
while True:
358338
char = input.read(1)
359339

360340
# Check for line terminators
361341
if char in ('\n', '\r'):
362342
break
363-
364343
# Handle literal next mode FIRST (Ctrl+V quotes next char)
365-
if editor.literal_next:
366-
editor.handle_literal_next(char)
367-
continue
368-
344+
elif editor.literal_next:
345+
editor._insert_char(char)
346+
editor.literal_next = False
347+
editor.eof_pressed = False
369348
# Check if it's the LNEXT character
370-
if char == editor.ctrl['LNEXT']:
349+
elif char == editor.ctrl['LNEXT']:
371350
editor.literal_next = True
372351
editor.eof_pressed = False
373-
continue
374-
375352
# Check for special control characters
376-
if char == editor.ctrl['INTR']:
353+
elif char == editor.ctrl['INTR']:
377354
raise KeyboardInterrupt
378-
if char == editor.ctrl['EOF']:
355+
elif char == editor.ctrl['EOF']:
379356
if editor.eof_pressed:
380357
break
381358
editor.eof_pressed = True
382-
continue
383-
if char == '\x00':
384-
continue
385-
359+
elif char == '\x00':
360+
pass
386361
# Dispatch to handler or insert as normal character
387-
handler = dispatch.get(char)
388-
if handler:
389-
handler()
390-
else:
391-
editor.insert_char(char)
362+
elif not editor.handle(char):
363+
editor._insert_char(char)
392364
editor.eof_pressed = False
393365

394366
return editor.passwd

0 commit comments

Comments
 (0)