[Python-checkins] cpython: #18116: getpass no longer always falls back to stdin.

r.david.murray python-checkins at python.org
Wed Jul 10 23:10:44 CEST 2013


http://hg.python.org/cpython/rev/70f55dc9d43f
changeset:   84535:70f55dc9d43f
parent:      84533:38b42ffdf86b
user:        R David Murray <rdmurray at bitdance.com>
date:        Wed Jul 10 17:02:24 2013 -0400
summary:
  #18116: getpass no longer always falls back to stdin.

Also fixes a resource warning that occurred when the fallback is taken.

Patch by Serhiy Storchaka.

(We couldn't figure out how to write tests for this.)

files:
  Lib/getpass.py           |  94 +++++++++++++++------------
  Lib/test/test_getpass.py |  20 +++--
  Misc/NEWS                |   4 +
  3 files changed, 68 insertions(+), 50 deletions(-)


diff --git a/Lib/getpass.py b/Lib/getpass.py
--- a/Lib/getpass.py
+++ b/Lib/getpass.py
@@ -15,7 +15,11 @@
 #          Guido van Rossum (Windows support and cleanup)
 #          Gregory P. Smith (tty support & GetPassWarning)
 
-import os, sys, warnings
+import contextlib
+import io
+import os
+import sys
+import warnings
 
 __all__ = ["getpass","getuser","GetPassWarning"]
 
@@ -38,53 +42,57 @@
 
     Always restores terminal settings before returning.
     """
-    fd = None
-    tty = None
     passwd = None
-    try:
-        # Always try reading and writing directly on the tty first.
-        fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
-        tty = os.fdopen(fd, 'w+', 1)
-        input = tty
-        if not stream:
-            stream = tty
-    except OSError as e:
-        # If that fails, see if stdin can be controlled.
+    with contextlib.ExitStack() as stack:
         try:
-            fd = sys.stdin.fileno()
-        except (AttributeError, ValueError):
-            passwd = fallback_getpass(prompt, stream)
-        input = sys.stdin
-        if not stream:
-            stream = sys.stderr
+            # Always try reading and writing directly on the tty first.
+            fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
+            tty = io.FileIO(fd, 'w+')
+            stack.enter_context(tty)
+            input = io.TextIOWrapper(tty)
+            stack.enter_context(input)
+            if not stream:
+                stream = input
+        except OSError as e:
+            # If that fails, see if stdin can be controlled.
+            stack.close()
+            try:
+                fd = sys.stdin.fileno()
+            except (AttributeError, ValueError):
+                fd = None
+                passwd = fallback_getpass(prompt, stream)
+            input = sys.stdin
+            if not stream:
+                stream = sys.stderr
 
-    if fd is not None:
-        passwd = None
-        try:
-            old = termios.tcgetattr(fd)     # a copy to save
-            new = old[:]
-            new[3] &= ~termios.ECHO  # 3 == 'lflags'
-            tcsetattr_flags = termios.TCSAFLUSH
-            if hasattr(termios, 'TCSASOFT'):
-                tcsetattr_flags |= termios.TCSASOFT
+        if fd is not None:
             try:
-                termios.tcsetattr(fd, tcsetattr_flags, new)
-                passwd = _raw_input(prompt, stream, input=input)
-            finally:
-                termios.tcsetattr(fd, tcsetattr_flags, old)
-                stream.flush()  # issue7208
-        except termios.error:
-            if passwd is not None:
-                # _raw_input succeeded.  The final tcsetattr failed.  Reraise
-                # instead of leaving the terminal in an unknown state.
-                raise
-            # We can't control the tty or stdin.  Give up and use normal IO.
-            # fallback_getpass() raises an appropriate warning.
-            del input, tty  # clean up unused file objects before blocking
-            passwd = fallback_getpass(prompt, stream)
+                old = termios.tcgetattr(fd)     # a copy to save
+                new = old[:]
+                new[3] &= ~termios.ECHO  # 3 == 'lflags'
+                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)
+                finally:
+                    termios.tcsetattr(fd, tcsetattr_flags, old)
+                    stream.flush()  # issue7208
+            except termios.error:
+                if passwd is not None:
+                    # _raw_input succeeded.  The final tcsetattr failed.  Reraise
+                    # instead of leaving the terminal in an unknown state.
+                    raise
+                # We can't control the tty or stdin.  Give up and use normal IO.
+                # fallback_getpass() raises an appropriate warning.
+                if stream is not input:
+                    # clean up unused file objects before blocking
+                    stack.close()
+                passwd = fallback_getpass(prompt, stream)
 
-    stream.write('\n')
-    return passwd
+        stream.write('\n')
+        return passwd
 
 
 def win_getpass(prompt='Password: ', stream=None):
diff --git a/Lib/test/test_getpass.py b/Lib/test/test_getpass.py
--- a/Lib/test/test_getpass.py
+++ b/Lib/test/test_getpass.py
@@ -1,7 +1,7 @@
 import getpass
 import os
 import unittest
-from io import StringIO
+from io import BytesIO, StringIO
 from unittest import mock
 from test import support
 
@@ -88,7 +88,8 @@
 
     def test_uses_tty_directly(self):
         with mock.patch('os.open') as open, \
-                mock.patch('os.fdopen'):
+                mock.patch('io.FileIO') as fileio, \
+                mock.patch('io.TextIOWrapper') as textio:
             # By setting open's return value to None the implementation will
             # skip code we don't care about in this test.  We can mock this out
             # fully if an alternate implementation works differently.
@@ -96,10 +97,13 @@
             getpass.unix_getpass()
             open.assert_called_once_with('/dev/tty',
                                          os.O_RDWR | os.O_NOCTTY)
+            fileio.assert_called_once_with(open.return_value, 'w+')
+            textio.assert_called_once_with(fileio.return_value)
 
     def test_resets_termios(self):
         with mock.patch('os.open') as open, \
-                mock.patch('os.fdopen'), \
+                mock.patch('io.FileIO'), \
+                mock.patch('io.TextIOWrapper'), \
                 mock.patch('termios.tcgetattr') as tcgetattr, \
                 mock.patch('termios.tcsetattr') as tcsetattr:
             open.return_value = 3
@@ -110,21 +114,23 @@
 
     def test_falls_back_to_fallback_if_termios_raises(self):
         with mock.patch('os.open') as open, \
-                mock.patch('os.fdopen') as fdopen, \
+                mock.patch('io.FileIO') as fileio, \
+                mock.patch('io.TextIOWrapper') as textio, \
                 mock.patch('termios.tcgetattr'), \
                 mock.patch('termios.tcsetattr') as tcsetattr, \
                 mock.patch('getpass.fallback_getpass') as fallback:
             open.return_value = 3
-            fdopen.return_value = StringIO()
+            fileio.return_value = BytesIO()
             tcsetattr.side_effect = termios.error
             getpass.unix_getpass()
             fallback.assert_called_once_with('Password: ',
-                                             fdopen.return_value)
+                                             textio.return_value)
 
     def test_flushes_stream_after_input(self):
         # issue 7208
         with mock.patch('os.open') as open, \
-                mock.patch('os.fdopen'), \
+                mock.patch('io.FileIO'), \
+                mock.patch('io.TextIOWrapper'), \
                 mock.patch('termios.tcgetattr'), \
                 mock.patch('termios.tcsetattr'):
             open.return_value = 3
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -142,6 +142,10 @@
 Library
 -------
 
+- Issue #18116: getpass was always getting an error when testing /dev/tty,
+  and thus was always falling back to stdin.  It also leaked an open file
+  when it did so.  Both of these issues are now fixed.
+
 - Issue #17198: Fix a NameError in the dbm module.  Patch by Valentina
   Mukhamedzhanova.
 

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list