summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/getpass.py99
-rw-r--r--Lib/test/test_getpass.py155
-rw-r--r--Lib/test/test_sundry.py1
-rw-r--r--Misc/NEWS5
4 files changed, 214 insertions, 46 deletions
diff --git a/Lib/getpass.py b/Lib/getpass.py
index 0044742..53c38b8 100644
--- a/Lib/getpass.py
+++ b/Lib/getpass.py
@@ -15,7 +15,11 @@ On the Mac EasyDialogs.AskPassword is used, if available.
# 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 @@ def unix_getpass(prompt='Password: ', stream=None):
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
-
- 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
+ # 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:
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)
-
- stream.write('\n')
- return passwd
+ 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
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
index 0000000..1731bd4
--- /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
+
+@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.
+@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
index e08cf01..c6cca26 100644
--- a/Lib/test/test_sundry.py
+++ b/Lib/test/test_sundry.py
@@ -41,7 +41,6 @@ class TestUntestedModules(unittest.TestCase):
import encodings
import formatter
- import getpass
import html.entities
import imghdr
import keyword
diff --git a/Misc/NEWS b/Misc/NEWS
index 85b5354..bc0e0b4 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -29,6 +29,11 @@ Core and Builtins
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.