diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index af9c9e9f39d9a6..a0c0c6dee2d513 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -27,9 +27,9 @@ The :mod:`getpass` module provides two functions: The *echo_char* argument controls how user input is displayed while typing. If *echo_char* is ``None`` (default), input remains hidden. Otherwise, - *echo_char* must be a printable ASCII string and each typed character - is replaced by it. For example, ``echo_char='*'`` will display - asterisks instead of the actual input. + *echo_char* must be a single printable ASCII character and each + typed character is replaced by it. For example, ``echo_char='*'`` will + display asterisks instead of the actual input. If echo free input is unavailable getpass() falls back to printing a warning message to *stream* and reading from ``sys.stdin`` and diff --git a/Lib/getpass.py b/Lib/getpass.py index 1dd40e25e09068..3d9bb1f0d146a4 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -33,8 +33,8 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None): prompt: Written on stream to ask for the input. Default: 'Password: ' stream: A writable file object to display the prompt. Defaults to the tty. If no tty is available defaults to sys.stderr. - echo_char: A string used to mask input (e.g., '*'). If None, input is - hidden. + echo_char: A single ASCII character to mask input (e.g., '*'). + If None, input is hidden. Returns: The seKr3t input. Raises: @@ -144,10 +144,19 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None): def _check_echo_char(echo_char): - # ASCII excluding control characters - if echo_char and not (echo_char.isprintable() and echo_char.isascii()): - raise ValueError("'echo_char' must be a printable ASCII string, " - f"got: {echo_char!r}") + # Single-character ASCII excluding control characters + if echo_char is None: + return + if not isinstance(echo_char, str): + raise TypeError("'echo_char' must be a str or None, not " + f"{type(echo_char).__name__}") + if not ( + len(echo_char) == 1 + and echo_char.isprintable() + and echo_char.isascii() + ): + raise ValueError("'echo_char' must be a single printable ASCII " + f"character, got: {echo_char!r}") def _raw_input(prompt="", stream=None, input=None, echo_char=None): diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index ab36535a1cfa8a..9c3def2c3be59b 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -201,5 +201,41 @@ def test_control_chars_with_echo_char(self): self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) +class GetpassEchoCharTest(unittest.TestCase): + + def test_accept_none(self): + getpass._check_echo_char(None) + + @support.subTests('echo_char', ["*", "A", " "]) + def test_accept_single_printable_ascii(self, echo_char): + getpass._check_echo_char(echo_char) + + def test_reject_empty_string(self): + self.assertRaises(ValueError, getpass.getpass, echo_char="") + + @support.subTests('echo_char', ["***", "AA", "aA*!"]) + def test_reject_multi_character_strings(self, echo_char): + self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) + + @support.subTests('echo_char', [ + '\N{LATIN CAPITAL LETTER AE}', # non-ASCII single character + '\N{HEAVY BLACK HEART}', # non-ASCII multibyte character + ]) + def test_reject_non_ascii(self, echo_char): + self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) + + @support.subTests('echo_char', [ + ch for ch in map(chr, range(0, 128)) + if not ch.isprintable() + ]) + def test_reject_non_printable_characters(self, echo_char): + self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) + + # TypeError Rejection + @support.subTests('echo_char', [b"*", 0, 0.0, [], {}]) + def test_reject_non_string(self, echo_char): + self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/ACKS b/Misc/ACKS index 83bc62726eecc9..ee3d66a04f0217 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -903,6 +903,7 @@ Jim Jewett Pedro Diaz Jimenez Orjan Johansen Fredrik Johansson +Benjamin K. Johnson Gregory K. Johnson Kent Johnson Michael Johnson diff --git a/Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst b/Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst new file mode 100644 index 00000000000000..75151ea86373d4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst @@ -0,0 +1,2 @@ +Raise :exc:`ValueError` when a multi-character string is passed to the +*echo_char* parameter of :func:`getpass.getpass`. Patch by Benjamin Johnson.