[Python-checkins] cpython (3.3): #18116: backport fix to 3.3 since real-world failure mode demonstrated.

r.david.murray python-checkins at python.org
Fri Dec 27 17:48:25 CET 2013


http://hg.python.org/cpython/rev/100f632d4306
changeset:   88206:100f632d4306
branch:      3.3
parent:      88201:7615c009e925
user:        R David Murray <rdmurray at bitdance.com>
date:        Fri Dec 27 11:24:32 2013 -0500
summary:
  #18116: backport fix to 3.3 since real-world failure mode demonstrated.

In issue 20074 it was pointed out that getpass would fail with a traceback if
stdin was, for example /dev/null, which is a non-unlikely scenario.

Also backported the tests from issue 17484 as modified by issue 18116.

(What I really did was copy getpass.py and test_getpass.py from their
state on tip as of 17bd04fbf3d3).

files:
  Lib/getpass.py           |   95 +++++++++-------
  Lib/test/test_getpass.py |  155 +++++++++++++++++++++++++++
  Lib/test/test_sundry.py  |    1 -
  Misc/NEWS                |    5 +
  4 files changed, 212 insertions(+), 44 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,52 +42,57 @@
 
     Always restores terminal settings before returning.
     """
-    fd = None
-    tty = 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 EnvironmentError as e:
-        # If that fails, see if stdin can be controlled.
+    passwd = None
+    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
new file mode 100644
--- /dev/null
+++ b/Lib/test/test_getpass.py
@@ -0,0 +1,155 @@
+import getpass
+import os
+import unittest
+from io import BytesIO, StringIO
+from unittest import mock
+from test import support
+
+try:
+    import termios
+except ImportError:
+    termios = None
+try:
+    import pwd
+except ImportError:
+    pwd = None
+
+ at mock.patch('os.environ')
+class GetpassGetuserTest(unittest.TestCase):
+
+    def test_username_takes_username_from_env(self, environ):
+        expected_name = 'some_name'
+        environ.get.return_value = expected_name
+        self.assertEqual(expected_name, getpass.getuser())
+
+    def test_username_priorities_of_env_values(self, environ):
+        environ.get.return_value = None
+        try:
+            getpass.getuser()
+        except ImportError: # in case there's no pwd module
+            pass
+        self.assertEqual(
+            environ.get.call_args_list,
+            [mock.call(x) for x in ('LOGNAME', 'USER', 'LNAME', 'USERNAME')])
+
+    def test_username_falls_back_to_pwd(self, environ):
+        expected_name = 'some_name'
+        environ.get.return_value = None
+        if pwd:
+            with mock.patch('os.getuid') as uid, \
+                    mock.patch('pwd.getpwuid') as getpw:
+                uid.return_value = 42
+                getpw.return_value = [expected_name]
+                self.assertEqual(expected_name,
+                                 getpass.getuser())
+                getpw.assert_called_once_with(42)
+        else:
+            self.assertRaises(ImportError, getpass.getuser)
+
+
+class GetpassRawinputTest(unittest.TestCase):
+
+    def test_flushes_stream_after_prompt(self):
+        # see issue 1703
+        stream = mock.Mock(spec=StringIO)
+        input = StringIO('input_string')
+        getpass._raw_input('some_prompt', stream, input=input)
+        stream.flush.assert_called_once_with()
+
+    def test_uses_stderr_as_default(self):
+        input = StringIO('input_string')
+        prompt = 'some_prompt'
+        with mock.patch('sys.stderr') as stderr:
+            getpass._raw_input(prompt, input=input)
+            stderr.write.assert_called_once_with(prompt)
+
+    @mock.patch('sys.stdin')
+    def test_uses_stdin_as_default_input(self, mock_input):
+        mock_input.readline.return_value = 'input_string'
+        getpass._raw_input(stream=StringIO())
+        mock_input.readline.assert_called_once_with()
+
+    def test_raises_on_empty_input(self):
+        input = StringIO('')
+        self.assertRaises(EOFError, getpass._raw_input, input=input)
+
+    def test_trims_trailing_newline(self):
+        input = StringIO('test\n')
+        self.assertEqual('test', getpass._raw_input(input=input))
+
+
+# Some of these tests are a bit white-box.  The functional requirement is that
+# the password input be taken directly from the tty, and that it not be echoed
+# on the screen, unless we are falling back to stderr/stdin.
+
+# Some of these might run on platforms without termios, but play it safe.
+ at unittest.skipUnless(termios, 'tests require system with termios')
+class UnixGetpassTest(unittest.TestCase):
+
+    def test_uses_tty_directly(self):
+        with mock.patch('os.open') as open, \
+                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.
+            open.return_value = None
+            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('io.FileIO'), \
+                mock.patch('io.TextIOWrapper'), \
+                mock.patch('termios.tcgetattr') as tcgetattr, \
+                mock.patch('termios.tcsetattr') as tcsetattr:
+            open.return_value = 3
+            fake_attrs = [255, 255, 255, 255, 255]
+            tcgetattr.return_value = list(fake_attrs)
+            getpass.unix_getpass()
+            tcsetattr.assert_called_with(3, mock.ANY, fake_attrs)
+
+    def test_falls_back_to_fallback_if_termios_raises(self):
+        with mock.patch('os.open') as open, \
+                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
+            fileio.return_value = BytesIO()
+            tcsetattr.side_effect = termios.error
+            getpass.unix_getpass()
+            fallback.assert_called_once_with('Password: ',
+                                             textio.return_value)
+
+    def test_flushes_stream_after_input(self):
+        # issue 7208
+        with mock.patch('os.open') as open, \
+                mock.patch('io.FileIO'), \
+                mock.patch('io.TextIOWrapper'), \
+                mock.patch('termios.tcgetattr'), \
+                mock.patch('termios.tcsetattr'):
+            open.return_value = 3
+            mock_stream = mock.Mock(spec=StringIO)
+            getpass.unix_getpass(stream=mock_stream)
+            mock_stream.flush.assert_called_with()
+
+    def test_falls_back_to_stdin(self):
+        with mock.patch('os.open') as os_open, \
+                mock.patch('sys.stdin', spec=StringIO) as stdin:
+            os_open.side_effect = IOError
+            stdin.fileno.side_effect = AttributeError
+            with support.captured_stderr() as stderr:
+                with self.assertWarns(getpass.GetPassWarning):
+                    getpass.unix_getpass()
+            stdin.readline.assert_called_once_with()
+            self.assertIn('Warning', stderr.getvalue())
+            self.assertIn('Password:', stderr.getvalue())
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Lib/test/test_sundry.py b/Lib/test/test_sundry.py
--- a/Lib/test/test_sundry.py
+++ b/Lib/test/test_sundry.py
@@ -41,7 +41,6 @@
 
             import encodings
             import formatter
-            import getpass
             import html.entities
             import imghdr
             import keyword
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -29,6 +29,11 @@
 Library
 -------
 
+- Issue #18116: getpass was always getting an error when testing /dev/tty,
+  and thus was always falling back to stdin, and would then raise an exception
+  if stdin could not be used (such as /dev/null).  It also leaked an open file.
+  All of these issues are now fixed.
+
 - Issue #20027: Fixed locale aliases for devanagari locales.
 
 - Issue #20067: Tkinter variables now work when wantobjects is false.

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


More information about the Python-checkins mailing list