From 7d412d9ae32921df22d6adb445199b1943dee1b9 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Sat, 6 Sep 2025 11:40:43 -0600 Subject: [PATCH 01/26] Accept only single character with getpass.getpass(echo_char) (see: gh-138514) --- Doc/library/getpass.rst | 6 +++--- Lib/getpass.py | 19 +++++++++++++------ Misc/ACKS | 1 + ...-09-06-11-26-21.gh-issue-138514.66ltOb.rst | 2 ++ 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index 0fb0fc88683c03..e4c8adec06522f 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-character, printable ASCII string 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..26df55baf5b7af 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -11,6 +11,9 @@ """ +# fmt: off +# isort: skip_file + # Authors: Piers Lauder (original) # Guido van Rossum (Windows support and cleanup) # Gregory P. Smith (tty support & GetPassWarning) @@ -33,8 +36,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-character string used to mask input (e.g., '*'). + If None, input is hidden. Returns: The seKr3t input. Raises: @@ -144,10 +147,14 @@ 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 and not ( + len(echo_char) == 1 + and echo_char.isprintable() + and echo_char.isascii() + ): + raise ValueError("'echo_char' must be a single-character, printable " + f"ASCII string, got: {echo_char!r}") def _raw_input(prompt="", stream=None, input=None, echo_char=None): diff --git a/Misc/ACKS b/Misc/ACKS index 37b7988606fa99..c54a27bbc8eb0b 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -906,6 +906,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..66e8a521cf1013 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst @@ -0,0 +1,2 @@ +Modify :func:`getpass.getpass` to accept only a single character. Patch by +Benjamin Johnson. From a99e90003c5d1ae37fbf5aff9b9dbf8f2e413829 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Sat, 6 Sep 2025 15:13:30 -0600 Subject: [PATCH 02/26] accept suggestion to reword blurb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- .../next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 66e8a521cf1013..b07c6ab3562550 100644 --- 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 @@ -1,2 +1,2 @@ -Modify :func:`getpass.getpass` to accept only a single character. Patch by +Force :func:`getpass.getpass` to accept only a single character. Patch by Benjamin Johnson. From bbd741ae9ae59576fe6c6e94dc6a6cc3d0cf4d60 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 20:24:52 -0400 Subject: [PATCH 03/26] reword echo_char description per suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/getpass.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index e4c8adec06522f..f2f514121cb69e 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -27,7 +27,7 @@ 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 single-character, printable ASCII string and each + *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. From 4338c9ff86429105a019ef0ba5ed005410911024 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 20:25:18 -0400 Subject: [PATCH 04/26] reword echo_char error message per suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/getpass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 26df55baf5b7af..54f8c957a138b6 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -153,8 +153,8 @@ def _check_echo_char(echo_char): and echo_char.isprintable() and echo_char.isascii() ): - raise ValueError("'echo_char' must be a single-character, printable " - f"ASCII string, got: {echo_char!r}") + 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): From 37a0a7293ac28e38899f30270545666919f8c9ea Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 20:25:44 -0400 Subject: [PATCH 05/26] reword getpass echo_char news per suggestion Co-authored-by: Brian Schubert --- .../Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index b07c6ab3562550..75151ea86373d4 100644 --- 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 @@ -1,2 +1,2 @@ -Force :func:`getpass.getpass` to accept only a single character. Patch by -Benjamin Johnson. +Raise :exc:`ValueError` when a multi-character string is passed to the +*echo_char* parameter of :func:`getpass.getpass`. Patch by Benjamin Johnson. From b063e33afbead1745874930644cf7b45458701ad Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 20:25:59 -0400 Subject: [PATCH 06/26] fix getpass echo_char spacing per suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/getpass.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 54f8c957a138b6..58c77b48e035fc 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -149,10 +149,10 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None): def _check_echo_char(echo_char): # Single-character ASCII excluding control characters if echo_char and not ( - len(echo_char) == 1 - and echo_char.isprintable() - and echo_char.isascii() - ): + 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}") From fcda047f5de20bc7d9eb27986fd26d099f63925a Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 21:13:09 -0400 Subject: [PATCH 07/26] remove --- Lib/getpass.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 58c77b48e035fc..1a3fdd0caaf066 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -11,9 +11,6 @@ """ -# fmt: off -# isort: skip_file - # Authors: Piers Lauder (original) # Guido van Rossum (Windows support and cleanup) # Gregory P. Smith (tty support & GetPassWarning) From 4eb57011f555a91eb176d4c5fe3552b3b4d66e6b Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 21:47:10 -0400 Subject: [PATCH 08/26] modify --- Lib/getpass.py | 7 ++++++- Lib/test/test_getpass.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 1a3fdd0caaf066..cb09cb61d61320 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -145,7 +145,12 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None): def _check_echo_char(echo_char): # Single-character ASCII excluding control characters - if echo_char and not ( + if echo_char is None: + return + if not isinstance(echo_char, str): + raise TypeError("'echo_char' must be type 'str' or 'None', got: " + f"{echo_char!r} (type: {type(echo_char).__name__})") + if not ( len(echo_char) == 1 and echo_char.isprintable() and echo_char.isascii() diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index ab36535a1cfa8a..42de12c1361691 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -200,6 +200,38 @@ def test_control_chars_with_echo_char(self): self.assertEqual(result, expect_result) self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) +class GetpassEchoCharValidationTest(unittest.TestCase): + def test_accepts_none(self): + getpass._check_echo_char(None) + + def test_accepts_single_printable_ascii(self): + for ch in ["*", "A", " "]: + getpass._check_echo_char(ch) + + def test_rejects_multi_character_strings(self): + for s in ["***", "AA", "aA*!"]: + with self.assertRaises(ValueError): + getpass._check_echo_char(s) + + def test_rejects_non_ascii(self): + for s in ["Æ", "❤️", "🐍"]: + with self.assertRaises(ValueError): + getpass._check_echo_char(s) + + def test_rejects_control_characters(self): + for ch in ["\n", "\t", "\r", "\x00", "\x7f", "\x07"]: + with self.assertRaises(ValueError): + getpass._check_echo_char(ch) + + def test_rejects_non_string(self): + for item in [b"*", 0]: + with self.assertRaises(TypeError): + getpass._check_echo_char(item) + + def test_rejects_empty_string(self): + for item in [""]: + with self.assertRaises(ValueError): + getpass._check_echo_char(item) if __name__ == "__main__": unittest.main() From e8d08f8115865b3fa15e720bc65ddca9a2440c97 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 22:32:24 -0400 Subject: [PATCH 09/26] modify TypeError message --- Lib/getpass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index cb09cb61d61320..07f6e75b028825 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -148,8 +148,8 @@ def _check_echo_char(echo_char): if echo_char is None: return if not isinstance(echo_char, str): - raise TypeError("'echo_char' must be type 'str' or 'None', got: " - f"{echo_char!r} (type: {type(echo_char).__name__})") + raise TypeError("'echo_char' must be str or None, not " + f"{type(echo_char).__name__}") if not ( len(echo_char) == 1 and echo_char.isprintable() From 749cc7d961910baf1106a36531100bdef0b99f10 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 22:33:16 -0400 Subject: [PATCH 10/26] modify TypeError message again --- Lib/getpass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 07f6e75b028825..16c1db6b4383d9 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -148,7 +148,7 @@ def _check_echo_char(echo_char): if echo_char is None: return if not isinstance(echo_char, str): - raise TypeError("'echo_char' must be str or None, not " + raise TypeError("'echo_char' must be type str or None, not " f"{type(echo_char).__name__}") if not ( len(echo_char) == 1 From a6aed9356168a32ca5bfebce64420a9642069ca1 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Tue, 9 Sep 2025 22:33:49 -0400 Subject: [PATCH 11/26] add comma to ValueError message --- Lib/getpass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 16c1db6b4383d9..cb39b3f5d639cc 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -156,7 +156,7 @@ def _check_echo_char(echo_char): and echo_char.isascii() ): raise ValueError("'echo_char' must be a single printable ASCII " - f"character got: {echo_char!r}") + f"character, got: {echo_char!r}") def _raw_input(prompt="", stream=None, input=None, echo_char=None): From 7253600d767b7da9bfc8c0f0b692140ee567088f Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 16:53:05 -0400 Subject: [PATCH 12/26] accept suggestion from picnixz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/getpass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index cb39b3f5d639cc..c833841901fc3f 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -33,7 +33,7 @@ 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 single-character string used to mask input (e.g., '*'). + echo_char: A single ASCII character to mask input (e.g., '*'). If None, input is hidden. Returns: The seKr3t input. From cf0cc7368740992eb73bf77166e4a78f38c01aab Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 16:53:26 -0400 Subject: [PATCH 13/26] accept language suggestion from picnixz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/getpass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index c833841901fc3f..3d9bb1f0d146a4 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -148,7 +148,7 @@ def _check_echo_char(echo_char): if echo_char is None: return if not isinstance(echo_char, str): - raise TypeError("'echo_char' must be type str or None, not " + raise TypeError("'echo_char' must be a str or None, not " f"{type(echo_char).__name__}") if not ( len(echo_char) == 1 From 58f86cdeb60cb6f7550aee5b5136b52fcf494e58 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 16:54:00 -0400 Subject: [PATCH 14/26] accept spacing suggestion from picnixz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_getpass.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 42de12c1361691..9a6cae2380b6c6 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -200,6 +200,7 @@ def test_control_chars_with_echo_char(self): self.assertEqual(result, expect_result) self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) + class GetpassEchoCharValidationTest(unittest.TestCase): def test_accepts_none(self): getpass._check_echo_char(None) From 6bb99d3fb4ece19bbdef97af61c2c731237a1833 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 16:54:36 -0400 Subject: [PATCH 15/26] accept leftover artifact removal suggestion from picnixz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_getpass.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 9a6cae2380b6c6..f6ced7600f21e6 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -230,9 +230,7 @@ def test_rejects_non_string(self): getpass._check_echo_char(item) def test_rejects_empty_string(self): - for item in [""]: - with self.assertRaises(ValueError): - getpass._check_echo_char(item) + self.assertRaises(ValueError, getpass.getpass, echo_char="") if __name__ == "__main__": unittest.main() From fde711ff958a5b9d70e0c470ed8ffa5d3cd838c1 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 17:17:09 -0400 Subject: [PATCH 16/26] add new line per suggestion --- Lib/test/test_getpass.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index f6ced7600f21e6..afb1fd988e7b12 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -232,5 +232,6 @@ def test_rejects_non_string(self): def test_rejects_empty_string(self): self.assertRaises(ValueError, getpass.getpass, echo_char="") + if __name__ == "__main__": unittest.main() From acc7bca86fa2bb9716279ae0d3ad2b8ae34e6baa Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 18:05:17 -0400 Subject: [PATCH 17/26] use subTest for getpass echo_char tests --- Lib/test/test_getpass.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index afb1fd988e7b12..6ee4abb1914dcc 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -201,36 +201,43 @@ def test_control_chars_with_echo_char(self): self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) -class GetpassEchoCharValidationTest(unittest.TestCase): +class GetpassEchoCharTest(unittest.TestCase): + # Successful Validation Cases def test_accepts_none(self): getpass._check_echo_char(None) def test_accepts_single_printable_ascii(self): for ch in ["*", "A", " "]: - getpass._check_echo_char(ch) + with self.subTest(echo_char=ch): + getpass._check_echo_char(ch) + + # Rejected `echo_char` Cases + def test_rejects_empty_string(self): + self.assertRaises(ValueError, getpass.getpass, echo_char="") def test_rejects_multi_character_strings(self): for s in ["***", "AA", "aA*!"]: - with self.assertRaises(ValueError): - getpass._check_echo_char(s) + with self.subTest(echo_char=s): + with self.assertRaises(ValueError): + getpass.getpass(echo_char=s) def test_rejects_non_ascii(self): - for s in ["Æ", "❤️", "🐍"]: - with self.assertRaises(ValueError): - getpass._check_echo_char(s) + for ch in ["Æ", "❤️", "🐍"]: + with self.subTest(echo_char=ch): + with self.assertRaises(ValueError): + getpass.getpass(echo_char=ch) def test_rejects_control_characters(self): for ch in ["\n", "\t", "\r", "\x00", "\x7f", "\x07"]: - with self.assertRaises(ValueError): - getpass._check_echo_char(ch) + with self.subTest(echo_char=ch): + with self.assertRaises(ValueError): + getpass.getpass(echo_char=ch) def test_rejects_non_string(self): - for item in [b"*", 0]: - with self.assertRaises(TypeError): - getpass._check_echo_char(item) - - def test_rejects_empty_string(self): - self.assertRaises(ValueError, getpass.getpass, echo_char="") + for item in [b"*", 0, 0.0, [], {}]: + with self.subTest(echo_char=item): + with self.assertRaises(TypeError): + getpass.getpass(echo_char=item) if __name__ == "__main__": From 461fd5b970367b9bde21964a89a393859ea42f66 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 18:06:30 -0400 Subject: [PATCH 18/26] switch to one-liner style --- Lib/test/test_getpass.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 6ee4abb1914dcc..31536ae116c957 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -218,26 +218,22 @@ def test_rejects_empty_string(self): def test_rejects_multi_character_strings(self): for s in ["***", "AA", "aA*!"]: with self.subTest(echo_char=s): - with self.assertRaises(ValueError): - getpass.getpass(echo_char=s) + self.assertRaises(ValueError, getpass.getpass, echo_char=s) def test_rejects_non_ascii(self): for ch in ["Æ", "❤️", "🐍"]: with self.subTest(echo_char=ch): - with self.assertRaises(ValueError): - getpass.getpass(echo_char=ch) + self.assertRaises(ValueError, getpass.getpass, echo_char=ch) def test_rejects_control_characters(self): for ch in ["\n", "\t", "\r", "\x00", "\x7f", "\x07"]: with self.subTest(echo_char=ch): - with self.assertRaises(ValueError): - getpass.getpass(echo_char=ch) + self.assertRaises(ValueError, getpass.getpass, echo_char=ch) def test_rejects_non_string(self): for item in [b"*", 0, 0.0, [], {}]: with self.subTest(echo_char=item): - with self.assertRaises(TypeError): - getpass.getpass(echo_char=item) + self.assertRaises(TypeError, getpass.getpass, echo_char=item) if __name__ == "__main__": From 301ef46a9df0e42213b4dcf3451d34aa23480fd8 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 18:26:07 -0400 Subject: [PATCH 19/26] use @support.subTests syntax --- Lib/test/test_getpass.py | 44 +++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 31536ae116c957..f6d10878cfa402 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -202,38 +202,36 @@ def test_control_chars_with_echo_char(self): class GetpassEchoCharTest(unittest.TestCase): + # Tests for the `echo_char` parameter of `getpass.getpass()` # Successful Validation Cases def test_accepts_none(self): getpass._check_echo_char(None) - def test_accepts_single_printable_ascii(self): - for ch in ["*", "A", " "]: - with self.subTest(echo_char=ch): - getpass._check_echo_char(ch) + @support.subTests('echo_char', ["*", "A", " "]) + def test_accepts_single_printable_ascii(self, echo_char): + getpass._check_echo_char(echo_char) # Rejected `echo_char` Cases + # ValueError Rejection(s) def test_rejects_empty_string(self): self.assertRaises(ValueError, getpass.getpass, echo_char="") - def test_rejects_multi_character_strings(self): - for s in ["***", "AA", "aA*!"]: - with self.subTest(echo_char=s): - self.assertRaises(ValueError, getpass.getpass, echo_char=s) - - def test_rejects_non_ascii(self): - for ch in ["Æ", "❤️", "🐍"]: - with self.subTest(echo_char=ch): - self.assertRaises(ValueError, getpass.getpass, echo_char=ch) - - def test_rejects_control_characters(self): - for ch in ["\n", "\t", "\r", "\x00", "\x7f", "\x07"]: - with self.subTest(echo_char=ch): - self.assertRaises(ValueError, getpass.getpass, echo_char=ch) - - def test_rejects_non_string(self): - for item in [b"*", 0, 0.0, [], {}]: - with self.subTest(echo_char=item): - self.assertRaises(TypeError, getpass.getpass, echo_char=item) + @support.subTests('echo_char', ["***", "AA", "aA*!"]) + def test_rejects_multi_character_strings(self, echo_char): + self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) + + @support.subTests('echo_char', ["Æ", "❤️", "🐍"]) + def test_rejects_non_ascii(self, echo_char): + self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) + + @support.subTests('echo_char', ["\n", "\t", "\r", "\x00", "\x7f", "\x07"]) + def test_rejects_control_characters(self, echo_char): + self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) + + # TypeError Rejection(s) + @support.subTests('echo_char', [b"*", 0, 0.0, [], {}]) + def test_rejects_non_string(self, echo_char): + self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char) if __name__ == "__main__": From 6e148e58fd5718074d60362f28e1a88eb5ec3914 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 18:30:58 -0400 Subject: [PATCH 20/26] match style --- Lib/test/test_getpass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index f6d10878cfa402..4e114a2f2c96ef 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -203,7 +203,7 @@ def test_control_chars_with_echo_char(self): class GetpassEchoCharTest(unittest.TestCase): # Tests for the `echo_char` parameter of `getpass.getpass()` - # Successful Validation Cases + # Successful Validation Case(s) def test_accepts_none(self): getpass._check_echo_char(None) From caded776184fec339b3853db5daaf80ae96058da Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Wed, 10 Sep 2025 18:31:18 -0400 Subject: [PATCH 21/26] match style further --- Lib/test/test_getpass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 4e114a2f2c96ef..f7bab1ef95edef 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -211,7 +211,7 @@ def test_accepts_none(self): def test_accepts_single_printable_ascii(self, echo_char): getpass._check_echo_char(echo_char) - # Rejected `echo_char` Cases + # Rejected `echo_char` Case(s) # ValueError Rejection(s) def test_rejects_empty_string(self): self.assertRaises(ValueError, getpass.getpass, echo_char="") From 981d49de80c33ab4ef49a567586303c08ae14af0 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Thu, 11 Sep 2025 08:32:47 -0400 Subject: [PATCH 22/26] remove too-verbose comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_getpass.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index f7bab1ef95edef..589bc85e49db61 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -202,8 +202,7 @@ def test_control_chars_with_echo_char(self): class GetpassEchoCharTest(unittest.TestCase): - # Tests for the `echo_char` parameter of `getpass.getpass()` - # Successful Validation Case(s) + def test_accepts_none(self): getpass._check_echo_char(None) From 07534ce03f5de1fb2a40e4f39cb254843c55d249 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Thu, 11 Sep 2025 08:32:57 -0400 Subject: [PATCH 23/26] remove too-verbose comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_getpass.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 589bc85e49db61..8763c8cae47fce 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -210,8 +210,6 @@ def test_accepts_none(self): def test_accepts_single_printable_ascii(self, echo_char): getpass._check_echo_char(echo_char) - # Rejected `echo_char` Case(s) - # ValueError Rejection(s) def test_rejects_empty_string(self): self.assertRaises(ValueError, getpass.getpass, echo_char="") From 2203e03db2176dbcf8f47bea87e57363b7bec7a7 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Thu, 11 Sep 2025 08:34:17 -0400 Subject: [PATCH 24/26] simplify non-ascii tests using ascii MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_getpass.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 8763c8cae47fce..097f7404494648 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -217,7 +217,10 @@ def test_rejects_empty_string(self): def test_rejects_multi_character_strings(self, echo_char): self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) - @support.subTests('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_rejects_non_ascii(self, echo_char): self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) From 6485375ce96c2ea2455f9c829fc1f76ca68668c8 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Thu, 11 Sep 2025 08:35:03 -0400 Subject: [PATCH 25/26] test every non-printable ascii byte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_getpass.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 097f7404494648..39b477bf40396f 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -224,8 +224,11 @@ def test_rejects_multi_character_strings(self, echo_char): def test_rejects_non_ascii(self, echo_char): self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) - @support.subTests('echo_char', ["\n", "\t", "\r", "\x00", "\x7f", "\x07"]) - def test_rejects_control_characters(self, echo_char): + @support.subTests('echo_char', [ + ch for ch in map(chr, range(0, 128)) + if not ch.isprintable() + ]) + def test_rejects_non_printable_characters(self, echo_char): self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) # TypeError Rejection(s) From 3629a53a10776b5357c1778c72f6cf383608301b Mon Sep 17 00:00:00 2001 From: Benjamin Johnson Date: Thu, 11 Sep 2025 08:40:41 -0400 Subject: [PATCH 26/26] remove s characters --- Lib/test/test_getpass.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 39b477bf40396f..9c3def2c3be59b 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -203,37 +203,37 @@ def test_control_chars_with_echo_char(self): class GetpassEchoCharTest(unittest.TestCase): - def test_accepts_none(self): + def test_accept_none(self): getpass._check_echo_char(None) @support.subTests('echo_char', ["*", "A", " "]) - def test_accepts_single_printable_ascii(self, echo_char): + def test_accept_single_printable_ascii(self, echo_char): getpass._check_echo_char(echo_char) - def test_rejects_empty_string(self): + def test_reject_empty_string(self): self.assertRaises(ValueError, getpass.getpass, echo_char="") @support.subTests('echo_char', ["***", "AA", "aA*!"]) - def test_rejects_multi_character_strings(self, echo_char): + 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_rejects_non_ascii(self, echo_char): + '\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_rejects_non_printable_characters(self, echo_char): + def test_reject_non_printable_characters(self, echo_char): self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char) - # TypeError Rejection(s) + # TypeError Rejection @support.subTests('echo_char', [b"*", 0, 0.0, [], {}]) - def test_rejects_non_string(self, echo_char): + def test_reject_non_string(self, echo_char): self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char)