From d2b3f7411c16feb00392a7290b33d631ff4841fa Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 24 Feb 2025 07:22:01 +0400 Subject: [PATCH 01/22] Add optional argument `mask` for `getpass.getpass` --- Doc/library/getpass.rst | 7 ++- Doc/whatsnew/3.14.rst | 8 ++++ Lib/getpass.py | 45 ++++++++++++++++--- ...5-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst | 2 + 4 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index 3b5296f9ec66fa..327a23a39b12be 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -16,7 +16,7 @@ The :mod:`getpass` module provides two functions: -.. function:: getpass(prompt='Password: ', stream=None) +.. function:: getpass(prompt='Password: ', stream=None, mask=None) Prompt the user for a password without echoing. The user is prompted using the string *prompt*, which defaults to ``'Password: '``. On Unix, the @@ -25,6 +25,11 @@ The :mod:`getpass` module provides two functions: (:file:`/dev/tty`) or if that is unavailable to ``sys.stderr`` (this argument is ignored on Windows). + The *mask* argument controls how user input is displayed while typing. If + *mask* is ``None`` (default), input remains hidden. If *mask* is a string, + each typed character is replaced with the given string. For example, + ``mask='*'`` 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 issuing a :exc:`GetPassWarning`. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 1cd8da46a2bb7e..8c109204ca1618 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -558,6 +558,14 @@ getopt * Add support for returning intermixed options and non-option arguments in order. (Contributed by Serhiy Storchaka in :gh:`126390`.) + +getpass +------- + +* Add optional argument *mask* for :meth:`getpass.getpass`. + (Contributed by Semyon Moroz in :gh:`77065`.) + + http ---- diff --git a/Lib/getpass.py b/Lib/getpass.py index bd0097ced94c5e..75bcbd5f6cd275 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -1,6 +1,7 @@ """Utilities to get a password and/or the current user name. -getpass(prompt[, stream]) - Prompt for a password, with echo turned off. +getpass(prompt[, stream[, mask]]) - Prompt for a password, with echo +turned off. getuser() - Get the user name from the environment or password database. GetPassWarning - This UserWarning is issued when getpass() cannot prevent @@ -25,13 +26,14 @@ class GetPassWarning(UserWarning): pass -def unix_getpass(prompt='Password: ', stream=None): +def unix_getpass(prompt='Password: ', stream=None, mask=None): """Prompt for a password, with echo turned off. Args: 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. + mask: A string used to mask input (e.g., '*'). If None, input is hidden. Returns: The seKr3t input. Raises: @@ -40,7 +42,7 @@ def unix_getpass(prompt='Password: ', stream=None): Always restores terminal settings before returning. """ - passwd = None + passwd = "" with contextlib.ExitStack() as stack: try: # Always try reading and writing directly on the tty first. @@ -68,17 +70,41 @@ def unix_getpass(prompt='Password: ', stream=None): old = termios.tcgetattr(fd) # a copy to save new = old[:] new[3] &= ~termios.ECHO # 3 == 'lflags' + if mask: + new[3] &= ~termios.ICANON tcsetattr_flags = termios.TCSAFLUSH if hasattr(termios, 'TCSASOFT'): tcsetattr_flags |= termios.TCSASOFT try: termios.tcsetattr(fd, tcsetattr_flags, new) - passwd = _raw_input(prompt, stream, input=input) + if not mask: + passwd = _raw_input(prompt, stream, input=input) + stream.write('\n') + return passwd + + stream.write(prompt) + stream.flush() + while True: + char = input.read(1) + if char == '\n' or char == '\r': + break + if char == '\x03': + raise KeyboardInterrupt + if char == '\x7f' or char == '\b': + if mask and passwd: + stream.write("\b \b" * len(mask)) + stream.flush() + passwd = passwd[:-1] + else: + passwd += char + if mask: + stream.write(mask) + stream.flush() finally: termios.tcsetattr(fd, tcsetattr_flags, old) stream.flush() # issue7208 except termios.error: - if passwd is not None: + if passwd: # _raw_input succeeded. The final tcsetattr failed. Reraise # instead of leaving the terminal in an unknown state. raise @@ -93,7 +119,7 @@ def unix_getpass(prompt='Password: ', stream=None): return passwd -def win_getpass(prompt='Password: ', stream=None): +def win_getpass(prompt='Password: ', stream=None, mask=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) @@ -108,9 +134,16 @@ def win_getpass(prompt='Password: ', stream=None): if c == '\003': raise KeyboardInterrupt if c == '\b': + if mask and pw: + for _ in range(len(mask)): + msvcrt.putwch('\b') + msvcrt.putwch(' ') + msvcrt.putwch('\b') pw = pw[:-1] else: pw = pw + c + if mask: + msvcrt.putwch(mask) msvcrt.putwch('\r') msvcrt.putwch('\n') return pw diff --git a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst new file mode 100644 index 00000000000000..487424297b44fa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst @@ -0,0 +1,2 @@ +Added optional argument *mask* for :meth:`getpass.getpass`. Patch by +Semyon Moroz. From 413a9ff09539ece2d1013b06cbe6cdf845cd6fc2 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 1 Mar 2025 17:32:18 +0000 Subject: [PATCH 02/22] Update Lib/getpass.py 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 75bcbd5f6cd275..624fe436e28ee3 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -135,7 +135,7 @@ def win_getpass(prompt='Password: ', stream=None, mask=None): raise KeyboardInterrupt if c == '\b': if mask and pw: - for _ in range(len(mask)): + for _ in mask: msvcrt.putwch('\b') msvcrt.putwch(' ') msvcrt.putwch('\b') From 9ac2ae2bcae79da6fced1b8a18a224b2ea3eb973 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 1 Mar 2025 21:49:12 +0400 Subject: [PATCH 03/22] Revert passwd empty string --- Lib/getpass.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 624fe436e28ee3..27540e9f2d62d2 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -42,7 +42,7 @@ def unix_getpass(prompt='Password: ', stream=None, mask=None): Always restores terminal settings before returning. """ - passwd = "" + passwd = None with contextlib.ExitStack() as stack: try: # Always try reading and writing directly on the tty first. @@ -82,6 +82,7 @@ def unix_getpass(prompt='Password: ', stream=None, mask=None): stream.write('\n') return passwd + passwd = "" stream.write(prompt) stream.flush() while True: @@ -104,7 +105,7 @@ def unix_getpass(prompt='Password: ', stream=None, mask=None): termios.tcsetattr(fd, tcsetattr_flags, old) stream.flush() # issue7208 except termios.error: - if passwd: + if passwd is not None: # _raw_input succeeded. The final tcsetattr failed. Reraise # instead of leaving the terminal in an unknown state. raise From 0ae2b819f22bb62d4ead4ab4d93af56aa4fe0a28 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 1 Mar 2025 22:13:25 +0400 Subject: [PATCH 04/22] Rename argument --- Doc/library/getpass.rst | 11 ++++--- Doc/whatsnew/3.14.rst | 4 ++- Lib/getpass.py | 31 ++++++++++--------- ...5-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst | 4 +-- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index 327a23a39b12be..5f09a52e884bc9 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -16,7 +16,7 @@ The :mod:`getpass` module provides two functions: -.. function:: getpass(prompt='Password: ', stream=None, mask=None) +.. function:: getpass(prompt='Password: ', stream=None, *, echochar=None) Prompt the user for a password without echoing. The user is prompted using the string *prompt*, which defaults to ``'Password: '``. On Unix, the @@ -25,10 +25,11 @@ The :mod:`getpass` module provides two functions: (:file:`/dev/tty`) or if that is unavailable to ``sys.stderr`` (this argument is ignored on Windows). - The *mask* argument controls how user input is displayed while typing. If - *mask* is ``None`` (default), input remains hidden. If *mask* is a string, - each typed character is replaced with the given string. For example, - ``mask='*'`` will display asterisks instead of the actual input. + The *echochar* argument controls how user input is displayed while typing. + If *echochar* is ``None`` (default), input remains hidden. If *echochar* is + a string, each typed character is replaced with the given string. + For example, ``echochar='*'`` 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/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 8c109204ca1618..41dde56b1c9e36 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -562,7 +562,9 @@ getopt getpass ------- -* Add optional argument *mask* for :meth:`getpass.getpass`. +* Support keyboard feedback by :func:`getpass.getpass` via the keyword-only + optional argument ``echochar``. Placeholder characters are rendered whenever + a character is entered, and removed when a character is deleted. (Contributed by Semyon Moroz in :gh:`77065`.) diff --git a/Lib/getpass.py b/Lib/getpass.py index 27540e9f2d62d2..61ca29b04cc69e 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -1,7 +1,7 @@ """Utilities to get a password and/or the current user name. -getpass(prompt[, stream[, mask]]) - Prompt for a password, with echo -turned off. +getpass(prompt[, stream[, echochar]]) - Prompt for a password, with echo +turned off and optional keyboard feedback. getuser() - Get the user name from the environment or password database. GetPassWarning - This UserWarning is issued when getpass() cannot prevent @@ -26,14 +26,15 @@ class GetPassWarning(UserWarning): pass -def unix_getpass(prompt='Password: ', stream=None, mask=None): +def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): """Prompt for a password, with echo turned off. Args: 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. - mask: A string used to mask input (e.g., '*'). If None, input is hidden. + echochar: A string used to mask input (e.g., '*'). If None, input is + hidden. Returns: The seKr3t input. Raises: @@ -70,14 +71,14 @@ def unix_getpass(prompt='Password: ', stream=None, mask=None): old = termios.tcgetattr(fd) # a copy to save new = old[:] new[3] &= ~termios.ECHO # 3 == 'lflags' - if mask: + if echochar: new[3] &= ~termios.ICANON tcsetattr_flags = termios.TCSAFLUSH if hasattr(termios, 'TCSASOFT'): tcsetattr_flags |= termios.TCSASOFT try: termios.tcsetattr(fd, tcsetattr_flags, new) - if not mask: + if not echochar: passwd = _raw_input(prompt, stream, input=input) stream.write('\n') return passwd @@ -92,14 +93,14 @@ def unix_getpass(prompt='Password: ', stream=None, mask=None): if char == '\x03': raise KeyboardInterrupt if char == '\x7f' or char == '\b': - if mask and passwd: - stream.write("\b \b" * len(mask)) + if echochar and passwd: + stream.write("\b \b" * len(echochar)) stream.flush() passwd = passwd[:-1] else: passwd += char - if mask: - stream.write(mask) + if echochar: + stream.write(echochar) stream.flush() finally: termios.tcsetattr(fd, tcsetattr_flags, old) @@ -120,7 +121,7 @@ def unix_getpass(prompt='Password: ', stream=None, mask=None): return passwd -def win_getpass(prompt='Password: ', stream=None, mask=None): +def win_getpass(prompt='Password: ', stream=None, *, echochar=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) @@ -135,16 +136,16 @@ def win_getpass(prompt='Password: ', stream=None, mask=None): if c == '\003': raise KeyboardInterrupt if c == '\b': - if mask and pw: - for _ in mask: + if echochar and pw: + for _ in echochar: msvcrt.putwch('\b') msvcrt.putwch(' ') msvcrt.putwch('\b') pw = pw[:-1] else: pw = pw + c - if mask: - msvcrt.putwch(mask) + if echochar: + msvcrt.putwch(echochar) msvcrt.putwch('\r') msvcrt.putwch('\n') return pw diff --git a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst index 487424297b44fa..0fa39ab4257698 100644 --- a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst +++ b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst @@ -1,2 +1,2 @@ -Added optional argument *mask* for :meth:`getpass.getpass`. Patch by -Semyon Moroz. +Add keyword-only optional argument *mask* for :meth:`getpass.getpass`. +Patch by Semyon Moroz. From da00d07d6288f02b84a33b4ae2e53f29f7666df6 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 1 Mar 2025 22:18:05 +0400 Subject: [PATCH 05/22] rename argument in news entry --- .../next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst index 0fa39ab4257698..8e433bd844985f 100644 --- a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst +++ b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst @@ -1,2 +1,2 @@ -Add keyword-only optional argument *mask* for :meth:`getpass.getpass`. +Add keyword-only optional argument *echochar* for :meth:`getpass.getpass`. Patch by Semyon Moroz. From 6c71075efa05ce75453e4d47a2765fb73cd72a9b Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 1 Mar 2025 23:16:28 +0400 Subject: [PATCH 06/22] accept suggestions --- Doc/library/getpass.rst | 3 +++ Lib/getpass.py | 49 ++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index 5f09a52e884bc9..8fdcca79b2e2e6 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -39,6 +39,9 @@ The :mod:`getpass` module provides two functions: If you call getpass from within IDLE, the input may be done in the terminal you launched IDLE from rather than the idle window itself. + .. versionchanged:: next + Added the *echochar* parameter for keyboard feedback. + .. exception:: GetPassWarning A :exc:`UserWarning` subclass issued when password input may be echoed. diff --git a/Lib/getpass.py b/Lib/getpass.py index 61ca29b04cc69e..5f55a74337dc7f 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -34,7 +34,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): stream: A writable file object to display the prompt. Defaults to the tty. If no tty is available defaults to sys.stderr. echochar: A string used to mask input (e.g., '*'). If None, input is - hidden. + hidden. Returns: The seKr3t input. Raises: @@ -83,25 +83,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): stream.write('\n') return passwd - passwd = "" - stream.write(prompt) - stream.flush() - while True: - char = input.read(1) - if char == '\n' or char == '\r': - break - if char == '\x03': - raise KeyboardInterrupt - if char == '\x7f' or char == '\b': - if echochar and passwd: - stream.write("\b \b" * len(echochar)) - stream.flush() - passwd = passwd[:-1] - else: - passwd += char - if echochar: - stream.write(echochar) - stream.flush() + passwd = _echochar_input(prompt, stream, input, echochar) finally: termios.tcsetattr(fd, tcsetattr_flags, old) stream.flush() # issue7208 @@ -186,6 +168,33 @@ def _raw_input(prompt="", stream=None, input=None): return line +def _echochar_input(prompt="", stream=None, input=None, echochar=""): + if not stream: + stream = sys.stderr + if not input: + input = sys.stdin + prompt = str(prompt) + stream.write(prompt) + stream.flush() + passwd = "" + while True: + char = input.read(1) + if char == '\n' or char == '\r': + break + if char == '\x03': + raise KeyboardInterrupt + if char == '\x7f' or char == '\b': + if passwd: + stream.write("\b \b" * len(echochar)) + stream.flush() + passwd = passwd[:-1] + else: + passwd += char + stream.write(echochar) + stream.flush() + return passwd + + def getuser(): """Get the username from the environment or password database. From 946d718eff36ea6976b3c1a02680d13b796e3444 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 1 Mar 2025 23:28:42 +0400 Subject: [PATCH 07/22] Rename _input_with_echochar function --- Lib/getpass.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 5f55a74337dc7f..b153db143df0ac 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -83,7 +83,8 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): stream.write('\n') return passwd - passwd = _echochar_input(prompt, stream, input, echochar) + passwd = _input_with_echochar(prompt, stream, input, + echochar) finally: termios.tcsetattr(fd, tcsetattr_flags, old) stream.flush() # issue7208 @@ -168,7 +169,7 @@ def _raw_input(prompt="", stream=None, input=None): return line -def _echochar_input(prompt="", stream=None, input=None, echochar=""): +def _input_with_echochar(prompt="", stream=None, input=None, echochar=""): if not stream: stream = sys.stderr if not input: From e2351abd7228f8573d33a1341d394340a9217192 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Thu, 13 Mar 2025 21:42:48 +0400 Subject: [PATCH 08/22] add suggestions --- Lib/getpass.py | 2 +- .../Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index b153db143df0ac..48b017fadb7129 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -169,7 +169,7 @@ def _raw_input(prompt="", stream=None, input=None): return line -def _input_with_echochar(prompt="", stream=None, input=None, echochar=""): +def _input_with_echochar(prompt, stream, input, echochar): if not stream: stream = sys.stderr if not input: diff --git a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst index 8e433bd844985f..d38a8c868e9407 100644 --- a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst +++ b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst @@ -1,2 +1,2 @@ -Add keyword-only optional argument *echochar* for :meth:`getpass.getpass`. -Patch by Semyon Moroz. +Add keyword-only optional argument *echochar* for :meth:`getpass.getpass` +for optional visual keyboard feedback support. Patch by Semyon Moroz. From 75e37bc971af922dffb860b178f2b96a26bcba2e Mon Sep 17 00:00:00 2001 From: donBarbos Date: Thu, 13 Mar 2025 23:10:22 +0400 Subject: [PATCH 09/22] Add echochar check for ASCII --- Lib/getpass.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 48b017fadb7129..7777d21e352824 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -43,6 +43,10 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): Always restores terminal settings before returning. """ + if echochar and not echochar.isascii(): + return ValueError(f"Invalid echochar: {echochar}. " + "ASCII character expected.") + passwd = None with contextlib.ExitStack() as stack: try: @@ -108,6 +112,9 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) + if echochar and not echochar.isascii(): + return ValueError(f"Invalid echochar: {echochar}. " + "ASCII character expected.") for c in prompt: msvcrt.putwch(c) @@ -120,10 +127,9 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): raise KeyboardInterrupt if c == '\b': if echochar and pw: - for _ in echochar: - msvcrt.putwch('\b') - msvcrt.putwch(' ') - msvcrt.putwch('\b') + msvcrt.putwch('\b') + msvcrt.putwch(' ') + msvcrt.putwch('\b') pw = pw[:-1] else: pw = pw + c @@ -186,7 +192,7 @@ def _input_with_echochar(prompt, stream, input, echochar): raise KeyboardInterrupt if char == '\x7f' or char == '\b': if passwd: - stream.write("\b \b" * len(echochar)) + stream.write("\b \b") stream.flush() passwd = passwd[:-1] else: From cd08e6859e5572391a0915c063a2ee041a4c0a98 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Thu, 13 Mar 2025 23:49:50 +0400 Subject: [PATCH 10/22] echochar must be ascii --- Doc/library/getpass.rst | 6 +++--- Lib/getpass.py | 33 ++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index 8fdcca79b2e2e6..d1bd95caa5c67e 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -27,9 +27,9 @@ The :mod:`getpass` module provides two functions: The *echochar* argument controls how user input is displayed while typing. If *echochar* is ``None`` (default), input remains hidden. If *echochar* is - a string, each typed character is replaced with the given string. - For example, ``echochar='*'`` will display asterisks instead of the actual - input. + a string, each typed character is replaced with the given string. But this + string must be ASCII character. For example, ``echochar='*'`` 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 7777d21e352824..4174ce21d1f027 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -43,9 +43,8 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): Always restores terminal settings before returning. """ - if echochar and not echochar.isascii(): - return ValueError(f"Invalid echochar: {echochar}. " - "ASCII character expected.") + if _is_ascii(echochar): + return ValueError(f"'echochar' must be ASCII, got: {echochar!r}") passwd = None with contextlib.ExitStack() as stack: @@ -82,13 +81,11 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): tcsetattr_flags |= termios.TCSASOFT try: termios.tcsetattr(fd, tcsetattr_flags, new) - if not echochar: + if echochar: + passwd = _input_with_echochar(prompt, stream, input, + echochar) + else: passwd = _raw_input(prompt, stream, input=input) - stream.write('\n') - return passwd - - passwd = _input_with_echochar(prompt, stream, input, - echochar) finally: termios.tcsetattr(fd, tcsetattr_flags, old) stream.flush() # issue7208 @@ -112,9 +109,8 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) - if echochar and not echochar.isascii(): - return ValueError(f"Invalid echochar: {echochar}. " - "ASCII character expected.") + if _is_ascii(echochar): + return ValueError(f"'echochar' must be ASCII, got: {echochar!r}") for c in prompt: msvcrt.putwch(c) @@ -127,9 +123,9 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): raise KeyboardInterrupt if c == '\b': if echochar and pw: - msvcrt.putwch('\b') - msvcrt.putwch(' ') - msvcrt.putwch('\b') + msvcrt.putch('\b') + msvcrt.putch(' ') + msvcrt.putch('\b') pw = pw[:-1] else: pw = pw + c @@ -150,6 +146,13 @@ def fallback_getpass(prompt='Password: ', stream=None): return _raw_input(prompt, stream) +def _is_ascii(echochar): + # ASCII excluding control characters + if echochar and not (32 <= ord(echochar) <= 127): + return False + return True + + def _raw_input(prompt="", stream=None, input=None): # This doesn't save the string in the GNU readline history. if not stream: From bcdf95a1090de77d079e72a1dcdfa1e0dade7201 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Thu, 13 Mar 2025 23:53:55 +0400 Subject: [PATCH 11/22] fix check rule --- Lib/getpass.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 4174ce21d1f027..0994cb44ff0009 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -43,7 +43,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): Always restores terminal settings before returning. """ - if _is_ascii(echochar): + if not _is_ascii(echochar): return ValueError(f"'echochar' must be ASCII, got: {echochar!r}") passwd = None @@ -109,7 +109,7 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) - if _is_ascii(echochar): + if not _is_ascii(echochar): return ValueError(f"'echochar' must be ASCII, got: {echochar!r}") for c in prompt: @@ -148,9 +148,9 @@ def fallback_getpass(prompt='Password: ', stream=None): def _is_ascii(echochar): # ASCII excluding control characters - if echochar and not (32 <= ord(echochar) <= 127): - return False - return True + if echochar and (32 <= ord(echochar) <= 127): + return True + return False def _raw_input(prompt="", stream=None, input=None): From 501d7041e1e7db3c2a8f742c5a6192e88e2150dd Mon Sep 17 00:00:00 2001 From: donBarbos Date: Fri, 14 Mar 2025 10:02:34 +0400 Subject: [PATCH 12/22] Update checks rules --- Doc/library/getpass.rst | 6 +++--- Lib/getpass.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index d1bd95caa5c67e..df99238f9af8e8 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -26,9 +26,9 @@ The :mod:`getpass` module provides two functions: argument is ignored on Windows). The *echochar* argument controls how user input is displayed while typing. - If *echochar* is ``None`` (default), input remains hidden. If *echochar* is - a string, each typed character is replaced with the given string. But this - string must be ASCII character. For example, ``echochar='*'`` will display + If *echochar* is ``None`` (default), input remains hidden. Otherwise, + *echochar* must be a printable ASCII string and each typed character + is replaced by the former. For example, ``echochar='*'`` will display asterisks instead of the actual input. If echo free input is unavailable getpass() falls back to printing diff --git a/Lib/getpass.py b/Lib/getpass.py index 0994cb44ff0009..4102855d474ab8 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -43,8 +43,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): Always restores terminal settings before returning. """ - if not _is_ascii(echochar): - return ValueError(f"'echochar' must be ASCII, got: {echochar!r}") + _check_echochar(echochar) passwd = None with contextlib.ExitStack() as stack: @@ -109,8 +108,7 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) - if not _is_ascii(echochar): - return ValueError(f"'echochar' must be ASCII, got: {echochar!r}") + _check_echochar(echochar) for c in prompt: msvcrt.putwch(c) @@ -146,11 +144,12 @@ def fallback_getpass(prompt='Password: ', stream=None): return _raw_input(prompt, stream) -def _is_ascii(echochar): +def _check_echochar(echochar): # ASCII excluding control characters - if echochar and (32 <= ord(echochar) <= 127): - return True - return False + if echochar and not (len(echochar) == 1 and + echochar.isprintable() and + echochar.isascii()): + raise ValueError(f"'echochar' must be ASCII, got: {echochar!r}") def _raw_input(prompt="", stream=None, input=None): From b6b822f8bce290f3548568b7cbcacf70510af000 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Fri, 14 Mar 2025 10:16:53 +0400 Subject: [PATCH 13/22] remove len check --- Lib/getpass.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 4102855d474ab8..cce26dee913ccd 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -146,9 +146,7 @@ def fallback_getpass(prompt='Password: ', stream=None): def _check_echochar(echochar): # ASCII excluding control characters - if echochar and not (len(echochar) == 1 and - echochar.isprintable() and - echochar.isascii()): + if echochar and not (echochar.isprintable() and echochar.isascii()): raise ValueError(f"'echochar' must be ASCII, got: {echochar!r}") From 977389a582d499e2e8b07b56dd3f055c222ded57 Mon Sep 17 00:00:00 2001 From: Semyon Moroz Date: Sun, 23 Mar 2025 15:40:25 +0000 Subject: [PATCH 14/22] Update 3.14.rst --- Doc/whatsnew/3.14.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 76f50c16d693d8..a0f0736294f81c 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -609,7 +609,7 @@ getopt graphlib -------- +-------- * Allow :meth:`graphlib.TopologicalSorter.prepare` to be called more than once as long as sorting has not started. @@ -617,7 +617,7 @@ graphlib getpass --------- +------- * Support keyboard feedback by :func:`getpass.getpass` via the keyword-only optional argument ``echochar``. Placeholder characters are rendered whenever From 59da53cb76981766ed6897171c459520d4d4a437 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 26 Mar 2025 02:57:41 +0400 Subject: [PATCH 15/22] Add test --- Lib/test/test_getpass.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 80dda2caaa3331..2ab62767dc701a 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -161,6 +161,32 @@ def test_falls_back_to_stdin(self): self.assertIn('Warning', stderr.getvalue()) self.assertIn('Password:', stderr.getvalue()) + def test_echochar_replaces_input_with_asterisks(self): + mock_result = '*************' + with mock.patch('os.open') as os_open, \ + mock.patch('io.FileIO'), \ + mock.patch('io.TextIOWrapper') as textio, \ + mock.patch('termios.tcgetattr'), \ + mock.patch('termios.tcsetattr'), \ + mock.patch('getpass._input_with_echochar') as mock_input: + os_open.return_value = 3 + mock_input.return_value = mock_result + + result = getpass.unix_getpass(echochar='*') + mock_input.assert_called_once_with('Password: ', textio(), textio(), '*') + self.assertEqual(result, mock_result) + + def test_input_with_echochar(self): + passwd = 'my1pa$$word!' + mock_input = StringIO(f'{passwd}\n') + mock_output = StringIO() + with mock.patch('sys.stdin', mock_input), \ + mock.patch('sys.stdout', mock_output): + result = getpass._input_with_echochar('Password: ', mock_output, + mock_input, '*') + self.assertEqual(result, passwd) + self.assertEqual('Password: ************', mock_output.getvalue()) + if __name__ == "__main__": unittest.main() From 3c027d4b8865a90e103c4ac30eef38492b287919 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sun, 30 Mar 2025 15:37:25 +0400 Subject: [PATCH 16/22] Add handle of new cases --- Doc/whatsnew/3.14.rst | 16 ++++++++-------- Lib/getpass.py | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index a0f0736294f81c..77204075843f67 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -608,14 +608,6 @@ getopt (Contributed by Serhiy Storchaka in :gh:`126390`.) -graphlib --------- - -* Allow :meth:`graphlib.TopologicalSorter.prepare` to be called more than once - as long as sorting has not started. - (Contributed by Daniel Pope in :gh:`130914`) - - getpass ------- @@ -625,6 +617,14 @@ getpass (Contributed by Semyon Moroz in :gh:`77065`.) +graphlib +-------- + +* Allow :meth:`graphlib.TopologicalSorter.prepare` to be called more than once + as long as sorting has not started. + (Contributed by Daniel Pope in :gh:`130914`) + + http ---- diff --git a/Lib/getpass.py b/Lib/getpass.py index cce26dee913ccd..4e96be762bf039 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -147,7 +147,8 @@ def fallback_getpass(prompt='Password: ', stream=None): def _check_echochar(echochar): # ASCII excluding control characters if echochar and not (echochar.isprintable() and echochar.isascii()): - raise ValueError(f"'echochar' must be ASCII, got: {echochar!r}") + raise ValueError("'echochar' must be a printable ASCII string, " + f"got: {echochar!r}") def _raw_input(prompt="", stream=None, input=None): @@ -184,21 +185,30 @@ def _input_with_echochar(prompt, stream, input, echochar): stream.write(prompt) stream.flush() passwd = "" + eof_pressed = False while True: char = input.read(1) if char == '\n' or char == '\r': break - if char == '\x03': + elif char == '\x03': raise KeyboardInterrupt - if char == '\x7f' or char == '\b': + elif char == '\x7f' or char == '\b': if passwd: stream.write("\b \b") stream.flush() passwd = passwd[:-1] + elif char == '\x04': + if eof_pressed: + break + else: + eof_pressed = True + elif char == '\x00': + continue else: passwd += char stream.write(echochar) stream.flush() + eof_pressed = False return passwd From 81e71a9aa3040aaa24c7b4f4592a8020bccac6c5 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sun, 30 Mar 2025 17:39:28 +0400 Subject: [PATCH 17/22] Add test case with control chars --- Lib/test/test_getpass.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 2ab62767dc701a..b90fd34a0156d1 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -176,7 +176,7 @@ def test_echochar_replaces_input_with_asterisks(self): mock_input.assert_called_once_with('Password: ', textio(), textio(), '*') self.assertEqual(result, mock_result) - def test_input_with_echochar(self): + def test_input_with_control_characters(self): passwd = 'my1pa$$word!' mock_input = StringIO(f'{passwd}\n') mock_output = StringIO() @@ -187,6 +187,18 @@ def test_input_with_echochar(self): self.assertEqual(result, passwd) self.assertEqual('Password: ************', mock_output.getvalue()) + def test_control_chars_with_echochar(self): + passwd = 'pass\twd\b' + expect_result = 'pass\tw' + mock_input = StringIO(f'{passwd}\n') + mock_output = StringIO() + with mock.patch('sys.stdin', mock_input), \ + mock.patch('sys.stdout', mock_output): + result = getpass._input_with_echochar('Password: ', mock_output, + mock_input, '*') + self.assertEqual(result, expect_result) + self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) + if __name__ == "__main__": unittest.main() From ba236eeee04a5fb4305b2f90a1ac5e13f8187259 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sun, 30 Mar 2025 17:47:24 +0400 Subject: [PATCH 18/22] Back old name of test function --- 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 b90fd34a0156d1..1a8454477dbb6e 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -176,7 +176,7 @@ def test_echochar_replaces_input_with_asterisks(self): mock_input.assert_called_once_with('Password: ', textio(), textio(), '*') self.assertEqual(result, mock_result) - def test_input_with_control_characters(self): + def test_input_with_echochar(self): passwd = 'my1pa$$word!' mock_input = StringIO(f'{passwd}\n') mock_output = StringIO() From c608b7a17b18cee012b3c2757727c6b4f9afafeb Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 5 May 2025 08:49:41 +0400 Subject: [PATCH 19/22] Update --- Lib/getpass.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/Lib/getpass.py b/Lib/getpass.py index 4e96be762bf039..047f733b770e92 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -80,11 +80,9 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): tcsetattr_flags |= termios.TCSASOFT try: termios.tcsetattr(fd, tcsetattr_flags, new) - if echochar: - passwd = _input_with_echochar(prompt, stream, input, - echochar) - else: - passwd = _raw_input(prompt, stream, input=input) + passwd = _raw_input(prompt, stream, input=input, + echochar=echochar) + finally: termios.tcsetattr(fd, tcsetattr_flags, old) stream.flush() # issue7208 @@ -151,7 +149,7 @@ def _check_echochar(echochar): f"got: {echochar!r}") -def _raw_input(prompt="", stream=None, input=None): +def _raw_input(prompt="", stream=None, input=None, echochar=None): # This doesn't save the string in the GNU readline history. if not stream: stream = sys.stderr @@ -168,6 +166,8 @@ def _raw_input(prompt="", stream=None, input=None): stream.write(prompt) stream.flush() # NOTE: The Python C API calls flockfile() (and unlock) during readline. + if echochar: + return _readline_with_echochar(stream, input, echochar) line = input.readline() if not line: raise EOFError @@ -176,14 +176,7 @@ def _raw_input(prompt="", stream=None, input=None): return line -def _input_with_echochar(prompt, stream, input, echochar): - if not stream: - stream = sys.stderr - if not input: - input = sys.stdin - prompt = str(prompt) - stream.write(prompt) - stream.flush() +def _readline_with_echochar(stream, input, echochar): passwd = "" eof_pressed = False while True: From 17a5891acb1baf2bd150b3898133e6c186e446c0 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 5 May 2025 08:57:32 +0400 Subject: [PATCH 20/22] Add update tests --- Lib/test/test_getpass.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 1a8454477dbb6e..2583f7b79662ba 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -168,22 +168,23 @@ def test_echochar_replaces_input_with_asterisks(self): mock.patch('io.TextIOWrapper') as textio, \ mock.patch('termios.tcgetattr'), \ mock.patch('termios.tcsetattr'), \ - mock.patch('getpass._input_with_echochar') as mock_input: + mock.patch('getpass._raw_input') as mock_input: os_open.return_value = 3 mock_input.return_value = mock_result result = getpass.unix_getpass(echochar='*') - mock_input.assert_called_once_with('Password: ', textio(), textio(), '*') + mock_input.assert_called_once_with('Password: ', textio(), + input=textio(), echochar='*') self.assertEqual(result, mock_result) - def test_input_with_echochar(self): + def test_raw_input_with_echochar(self): passwd = 'my1pa$$word!' mock_input = StringIO(f'{passwd}\n') mock_output = StringIO() with mock.patch('sys.stdin', mock_input), \ mock.patch('sys.stdout', mock_output): - result = getpass._input_with_echochar('Password: ', mock_output, - mock_input, '*') + result = getpass._raw_input('Password: ', mock_output, mock_input, + '*') self.assertEqual(result, passwd) self.assertEqual('Password: ************', mock_output.getvalue()) @@ -194,8 +195,8 @@ def test_control_chars_with_echochar(self): mock_output = StringIO() with mock.patch('sys.stdin', mock_input), \ mock.patch('sys.stdout', mock_output): - result = getpass._input_with_echochar('Password: ', mock_output, - mock_input, '*') + result = getpass._raw_input('Password: ', mock_output, mock_input, + '*') self.assertEqual(result, expect_result) self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) From 08e8686863812198765d6137d681ae5842d3bbef Mon Sep 17 00:00:00 2001 From: donBarbos Date: Tue, 6 May 2025 14:53:25 +0400 Subject: [PATCH 21/22] Accept suggestions --- Doc/library/getpass.rst | 12 +++--- Doc/whatsnew/3.14.rst | 2 +- Lib/getpass.py | 40 +++++++++---------- Lib/test/test_getpass.py | 4 +- ...5-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Doc/library/getpass.rst b/Doc/library/getpass.rst index df99238f9af8e8..38b78dc3299a3e 100644 --- a/Doc/library/getpass.rst +++ b/Doc/library/getpass.rst @@ -16,7 +16,7 @@ The :mod:`getpass` module provides two functions: -.. function:: getpass(prompt='Password: ', stream=None, *, echochar=None) +.. function:: getpass(prompt='Password: ', stream=None, *, echo_char=None) Prompt the user for a password without echoing. The user is prompted using the string *prompt*, which defaults to ``'Password: '``. On Unix, the @@ -25,10 +25,10 @@ The :mod:`getpass` module provides two functions: (:file:`/dev/tty`) or if that is unavailable to ``sys.stderr`` (this argument is ignored on Windows). - The *echochar* argument controls how user input is displayed while typing. - If *echochar* is ``None`` (default), input remains hidden. Otherwise, - *echochar* must be a printable ASCII string and each typed character - is replaced by the former. For example, ``echochar='*'`` will display + 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. If echo free input is unavailable getpass() falls back to printing @@ -40,7 +40,7 @@ The :mod:`getpass` module provides two functions: terminal you launched IDLE from rather than the idle window itself. .. versionchanged:: next - Added the *echochar* parameter for keyboard feedback. + Added the *echo_char* parameter for keyboard feedback. .. exception:: GetPassWarning diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 77204075843f67..973f00b9e70e9d 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -612,7 +612,7 @@ getpass ------- * Support keyboard feedback by :func:`getpass.getpass` via the keyword-only - optional argument ``echochar``. Placeholder characters are rendered whenever + optional argument ``echo_char``. Placeholder characters are rendered whenever a character is entered, and removed when a character is deleted. (Contributed by Semyon Moroz in :gh:`77065`.) diff --git a/Lib/getpass.py b/Lib/getpass.py index 047f733b770e92..f571425e54178a 100644 --- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -1,6 +1,6 @@ """Utilities to get a password and/or the current user name. -getpass(prompt[, stream[, echochar]]) - Prompt for a password, with echo +getpass(prompt[, stream[, echo_char]]) - Prompt for a password, with echo turned off and optional keyboard feedback. getuser() - Get the user name from the environment or password database. @@ -26,14 +26,14 @@ class GetPassWarning(UserWarning): pass -def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): +def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None): """Prompt for a password, with echo turned off. Args: 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. - echochar: A string used to mask input (e.g., '*'). If None, input is + echo_char: A string used to mask input (e.g., '*'). If None, input is hidden. Returns: The seKr3t input. @@ -43,7 +43,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): Always restores terminal settings before returning. """ - _check_echochar(echochar) + _check_echo_char(echo_char) passwd = None with contextlib.ExitStack() as stack: @@ -73,7 +73,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): old = termios.tcgetattr(fd) # a copy to save new = old[:] new[3] &= ~termios.ECHO # 3 == 'lflags' - if echochar: + if echo_char: new[3] &= ~termios.ICANON tcsetattr_flags = termios.TCSAFLUSH if hasattr(termios, 'TCSASOFT'): @@ -81,7 +81,7 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): try: termios.tcsetattr(fd, tcsetattr_flags, new) passwd = _raw_input(prompt, stream, input=input, - echochar=echochar) + echo_char=echo_char) finally: termios.tcsetattr(fd, tcsetattr_flags, old) @@ -102,11 +102,11 @@ def unix_getpass(prompt='Password: ', stream=None, *, echochar=None): return passwd -def win_getpass(prompt='Password: ', stream=None, *, echochar=None): +def win_getpass(prompt='Password: ', stream=None, *, echo_char=None): """Prompt for password with echo off, using Windows getwch().""" if sys.stdin is not sys.__stdin__: return fallback_getpass(prompt, stream) - _check_echochar(echochar) + _check_echo_char(echo_char) for c in prompt: msvcrt.putwch(c) @@ -118,15 +118,15 @@ def win_getpass(prompt='Password: ', stream=None, *, echochar=None): if c == '\003': raise KeyboardInterrupt if c == '\b': - if echochar and pw: + if echo_char and pw: msvcrt.putch('\b') msvcrt.putch(' ') msvcrt.putch('\b') pw = pw[:-1] else: pw = pw + c - if echochar: - msvcrt.putwch(echochar) + if echo_char: + msvcrt.putwch(echo_char) msvcrt.putwch('\r') msvcrt.putwch('\n') return pw @@ -142,14 +142,14 @@ def fallback_getpass(prompt='Password: ', stream=None): return _raw_input(prompt, stream) -def _check_echochar(echochar): +def _check_echo_char(echo_char): # ASCII excluding control characters - if echochar and not (echochar.isprintable() and echochar.isascii()): - raise ValueError("'echochar' must be a printable ASCII string, " - f"got: {echochar!r}") + 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}") -def _raw_input(prompt="", stream=None, input=None, echochar=None): +def _raw_input(prompt="", stream=None, input=None, echo_char=None): # This doesn't save the string in the GNU readline history. if not stream: stream = sys.stderr @@ -166,8 +166,8 @@ def _raw_input(prompt="", stream=None, input=None, echochar=None): stream.write(prompt) stream.flush() # NOTE: The Python C API calls flockfile() (and unlock) during readline. - if echochar: - return _readline_with_echochar(stream, input, echochar) + if echo_char: + return _readline_with_echo_char(stream, input, echo_char) line = input.readline() if not line: raise EOFError @@ -176,7 +176,7 @@ def _raw_input(prompt="", stream=None, input=None, echochar=None): return line -def _readline_with_echochar(stream, input, echochar): +def _readline_with_echo_char(stream, input, echo_char): passwd = "" eof_pressed = False while True: @@ -199,7 +199,7 @@ def _readline_with_echochar(stream, input, echochar): continue else: passwd += char - stream.write(echochar) + stream.write(echo_char) stream.flush() eof_pressed = False return passwd diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index 2583f7b79662ba..da9fc53326b592 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -172,9 +172,9 @@ def test_echochar_replaces_input_with_asterisks(self): os_open.return_value = 3 mock_input.return_value = mock_result - result = getpass.unix_getpass(echochar='*') + result = getpass.unix_getpass(echo_char='*') mock_input.assert_called_once_with('Password: ', textio(), - input=textio(), echochar='*') + input=textio(), echo_char='*') self.assertEqual(result, mock_result) def test_raw_input_with_echochar(self): diff --git a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst index d38a8c868e9407..65d87e9d727a2c 100644 --- a/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst +++ b/Misc/NEWS.d/next/Library/2025-02-24-07-08-11.gh-issue-77065.8uW0Wf.rst @@ -1,2 +1,2 @@ -Add keyword-only optional argument *echochar* for :meth:`getpass.getpass` +Add keyword-only optional argument *echo_char* for :meth:`getpass.getpass` for optional visual keyboard feedback support. Patch by Semyon Moroz. From 4319f23937c0d2857c5e096de8d32cc2dcc4b735 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Tue, 6 May 2025 15:11:52 +0400 Subject: [PATCH 22/22] Rename --- Lib/test/test_getpass.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py index da9fc53326b592..ab36535a1cfa8a 100644 --- a/Lib/test/test_getpass.py +++ b/Lib/test/test_getpass.py @@ -161,7 +161,7 @@ def test_falls_back_to_stdin(self): self.assertIn('Warning', stderr.getvalue()) self.assertIn('Password:', stderr.getvalue()) - def test_echochar_replaces_input_with_asterisks(self): + def test_echo_char_replaces_input_with_asterisks(self): mock_result = '*************' with mock.patch('os.open') as os_open, \ mock.patch('io.FileIO'), \ @@ -177,7 +177,7 @@ def test_echochar_replaces_input_with_asterisks(self): input=textio(), echo_char='*') self.assertEqual(result, mock_result) - def test_raw_input_with_echochar(self): + def test_raw_input_with_echo_char(self): passwd = 'my1pa$$word!' mock_input = StringIO(f'{passwd}\n') mock_output = StringIO() @@ -188,7 +188,7 @@ def test_raw_input_with_echochar(self): self.assertEqual(result, passwd) self.assertEqual('Password: ************', mock_output.getvalue()) - def test_control_chars_with_echochar(self): + def test_control_chars_with_echo_char(self): passwd = 'pass\twd\b' expect_result = 'pass\tw' mock_input = StringIO(f'{passwd}\n')