cpython: 100f632d4306 (original) (raw)
Mercurial > cpython
changeset 88206:100f632d4306 3.3
#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). [#18116]
R David Murray rdmurray@bitdance.com | |
---|---|
date | Fri, 27 Dec 2013 11:24:32 -0500 |
parents | 7615c009e925 |
children | 29a5a5b39dd6 a00842b783cf |
files | Lib/getpass.py Lib/test/test_getpass.py Lib/test/test_sundry.py Misc/NEWS |
diffstat | 4 files changed, 213 insertions(+), 45 deletions(-)[+] [-] Lib/getpass.py 97 Lib/test/test_getpass.py 155 Lib/test/test_sundry.py 1 Misc/NEWS 5 |
line wrap: on
line diff
--- a/Lib/getpass.py +++ b/Lib/getpass.py @@ -15,7 +15,11 @@ On the Mac EasyDialogs.AskPassword is us
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: ', st Always restores terminal settings before returning. """
- fd = None
- tty = None
- try:
# Always try reading and writing directly on the tty first.[](#l1.23)
fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)[](#l1.24)
tty = os.fdopen(fd, 'w+', 1)[](#l1.25)
input = tty[](#l1.26)
if not stream:[](#l1.27)
stream = tty[](#l1.28)
- except EnvironmentError as e:
# If that fails, see if stdin can be controlled.[](#l1.30)
try:[](#l1.31)
fd = sys.stdin.fileno()[](#l1.32)
except (AttributeError, ValueError):[](#l1.33)
passwd = fallback_getpass(prompt, stream)[](#l1.34)
input = sys.stdin[](#l1.35)
if not stream:[](#l1.36)
stream = sys.stderr[](#l1.37)
old = termios.tcgetattr(fd) # a copy to save[](#l1.44)
new = old[:][](#l1.45)
new[3] &= ~termios.ECHO # 3 == 'lflags'[](#l1.46)
tcsetattr_flags = termios.TCSAFLUSH[](#l1.47)
if hasattr(termios, 'TCSASOFT'):[](#l1.48)
tcsetattr_flags |= termios.TCSASOFT[](#l1.49)
# Always try reading and writing directly on the tty first.[](#l1.50)
fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)[](#l1.51)
tty = io.FileIO(fd, 'w+')[](#l1.52)
stack.enter_context(tty)[](#l1.53)
input = io.TextIOWrapper(tty)[](#l1.54)
stack.enter_context(input)[](#l1.55)
if not stream:[](#l1.56)
stream = input[](#l1.57)
except OSError as e:[](#l1.58)
# If that fails, see if stdin can be controlled.[](#l1.59)
stack.close()[](#l1.60)
try:[](#l1.61)
fd = sys.stdin.fileno()[](#l1.62)
except (AttributeError, ValueError):[](#l1.63)
fd = None[](#l1.64)
passwd = fallback_getpass(prompt, stream)[](#l1.65)
input = sys.stdin[](#l1.66)
if not stream:[](#l1.67)
stream = sys.stderr[](#l1.68)
if fd is not None:[](#l1.70) try:[](#l1.71)
termios.tcsetattr(fd, tcsetattr_flags, new)[](#l1.72)
passwd = _raw_input(prompt, stream, input=input)[](#l1.73)
finally:[](#l1.74)
termios.tcsetattr(fd, tcsetattr_flags, old)[](#l1.75)
stream.flush() # issue7208[](#l1.76)
except termios.error:[](#l1.77)
if passwd is not None:[](#l1.78)
# _raw_input succeeded. The final tcsetattr failed. Reraise[](#l1.79)
# instead of leaving the terminal in an unknown state.[](#l1.80)
raise[](#l1.81)
# We can't control the tty or stdin. Give up and use normal IO.[](#l1.82)
# fallback_getpass() raises an appropriate warning.[](#l1.83)
del input, tty # clean up unused file objects before blocking[](#l1.84)
passwd = fallback_getpass(prompt, stream)[](#l1.85)
old = termios.tcgetattr(fd) # a copy to save[](#l1.86)
new = old[:][](#l1.87)
new[3] &= ~termios.ECHO # 3 == 'lflags'[](#l1.88)
tcsetattr_flags = termios.TCSAFLUSH[](#l1.89)
if hasattr(termios, 'TCSASOFT'):[](#l1.90)
tcsetattr_flags |= termios.TCSASOFT[](#l1.91)
try:[](#l1.92)
termios.tcsetattr(fd, tcsetattr_flags, new)[](#l1.93)
passwd = _raw_input(prompt, stream, input=input)[](#l1.94)
finally:[](#l1.95)
termios.tcsetattr(fd, tcsetattr_flags, old)[](#l1.96)
stream.flush() # issue7208[](#l1.97)
except termios.error:[](#l1.98)
if passwd is not None:[](#l1.99)
# _raw_input succeeded. The final tcsetattr failed. Reraise[](#l1.100)
# instead of leaving the terminal in an unknown state.[](#l1.101)
raise[](#l1.102)
# We can't control the tty or stdin. Give up and use normal IO.[](#l1.103)
# fallback_getpass() raises an appropriate warning.[](#l1.104)
if stream is not input:[](#l1.105)
# clean up unused file objects before blocking[](#l1.106)
stack.close()[](#l1.107)
passwd = fallback_getpass(prompt, stream)[](#l1.108)
stream.write('\n')[](#l1.112)
return passwd[](#l1.113)
def win_getpass(prompt='Password: ', stream=None):
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:
+ +@mock.patch('os.environ') +class GetpassGetuserTest(unittest.TestCase): +
- def test_username_takes_username_from_env(self, environ):
expected_name = 'some_name'[](#l2.25)
environ.get.return_value = expected_name[](#l2.26)
self.assertEqual(expected_name, getpass.getuser())[](#l2.27)
- def test_username_priorities_of_env_values(self, environ):
environ.get.return_value = None[](#l2.30)
try:[](#l2.31)
getpass.getuser()[](#l2.32)
except ImportError: # in case there's no pwd module[](#l2.33)
pass[](#l2.34)
self.assertEqual([](#l2.35)
environ.get.call_args_list,[](#l2.36)
[mock.call(x) for x in ('LOGNAME', 'USER', 'LNAME', 'USERNAME')])[](#l2.37)
- def test_username_falls_back_to_pwd(self, environ):
expected_name = 'some_name'[](#l2.40)
environ.get.return_value = None[](#l2.41)
if pwd:[](#l2.42)
with mock.patch('os.getuid') as uid, \[](#l2.43)
mock.patch('pwd.getpwuid') as getpw:[](#l2.44)
uid.return_value = 42[](#l2.45)
getpw.return_value = [expected_name][](#l2.46)
self.assertEqual(expected_name,[](#l2.47)
getpass.getuser())[](#l2.48)
getpw.assert_called_once_with(42)[](#l2.49)
else:[](#l2.50)
self.assertRaises(ImportError, getpass.getuser)[](#l2.51)
+ + +class GetpassRawinputTest(unittest.TestCase): +
- def test_flushes_stream_after_prompt(self):
# see issue 1703[](#l2.57)
stream = mock.Mock(spec=StringIO)[](#l2.58)
input = StringIO('input_string')[](#l2.59)
getpass._raw_input('some_prompt', stream, input=input)[](#l2.60)
stream.flush.assert_called_once_with()[](#l2.61)
- def test_uses_stderr_as_default(self):
input = StringIO('input_string')[](#l2.64)
prompt = 'some_prompt'[](#l2.65)
with mock.patch('sys.stderr') as stderr:[](#l2.66)
getpass._raw_input(prompt, input=input)[](#l2.67)
stderr.write.assert_called_once_with(prompt)[](#l2.68)
- @mock.patch('sys.stdin')
- def test_uses_stdin_as_default_input(self, mock_input):
mock_input.readline.return_value = 'input_string'[](#l2.72)
getpass._raw_input(stream=StringIO())[](#l2.73)
mock_input.readline.assert_called_once_with()[](#l2.74)
- def test_raises_on_empty_input(self):
input = StringIO('')[](#l2.77)
self.assertRaises(EOFError, getpass._raw_input, input=input)[](#l2.78)
- def test_trims_trailing_newline(self):
input = StringIO('test\n')[](#l2.81)
self.assertEqual('test', getpass._raw_input(input=input))[](#l2.82)
+ + +# 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, \[](#l2.94)
mock.patch('io.FileIO') as fileio, \[](#l2.95)
mock.patch('io.TextIOWrapper') as textio:[](#l2.96)
# By setting open's return value to None the implementation will[](#l2.97)
# skip code we don't care about in this test. We can mock this out[](#l2.98)
# fully if an alternate implementation works differently.[](#l2.99)
open.return_value = None[](#l2.100)
getpass.unix_getpass()[](#l2.101)
open.assert_called_once_with('/dev/tty',[](#l2.102)
os.O_RDWR | os.O_NOCTTY)[](#l2.103)
fileio.assert_called_once_with(open.return_value, 'w+')[](#l2.104)
textio.assert_called_once_with(fileio.return_value)[](#l2.105)
- def test_resets_termios(self):
with mock.patch('os.open') as open, \[](#l2.108)
mock.patch('io.FileIO'), \[](#l2.109)
mock.patch('io.TextIOWrapper'), \[](#l2.110)
mock.patch('termios.tcgetattr') as tcgetattr, \[](#l2.111)
mock.patch('termios.tcsetattr') as tcsetattr:[](#l2.112)
open.return_value = 3[](#l2.113)
fake_attrs = [255, 255, 255, 255, 255][](#l2.114)
tcgetattr.return_value = list(fake_attrs)[](#l2.115)
getpass.unix_getpass()[](#l2.116)
tcsetattr.assert_called_with(3, mock.ANY, fake_attrs)[](#l2.117)
- def test_falls_back_to_fallback_if_termios_raises(self):
with mock.patch('os.open') as open, \[](#l2.120)
mock.patch('io.FileIO') as fileio, \[](#l2.121)
mock.patch('io.TextIOWrapper') as textio, \[](#l2.122)
mock.patch('termios.tcgetattr'), \[](#l2.123)
mock.patch('termios.tcsetattr') as tcsetattr, \[](#l2.124)
mock.patch('getpass.fallback_getpass') as fallback:[](#l2.125)
open.return_value = 3[](#l2.126)
fileio.return_value = BytesIO()[](#l2.127)
tcsetattr.side_effect = termios.error[](#l2.128)
getpass.unix_getpass()[](#l2.129)
fallback.assert_called_once_with('Password: ',[](#l2.130)
textio.return_value)[](#l2.131)
- def test_flushes_stream_after_input(self):
# issue 7208[](#l2.134)
with mock.patch('os.open') as open, \[](#l2.135)
mock.patch('io.FileIO'), \[](#l2.136)
mock.patch('io.TextIOWrapper'), \[](#l2.137)
mock.patch('termios.tcgetattr'), \[](#l2.138)
mock.patch('termios.tcsetattr'):[](#l2.139)
open.return_value = 3[](#l2.140)
mock_stream = mock.Mock(spec=StringIO)[](#l2.141)
getpass.unix_getpass(stream=mock_stream)[](#l2.142)
mock_stream.flush.assert_called_with()[](#l2.143)
- def test_falls_back_to_stdin(self):
with mock.patch('os.open') as os_open, \[](#l2.146)
mock.patch('sys.stdin', spec=StringIO) as stdin:[](#l2.147)
os_open.side_effect = IOError[](#l2.148)
stdin.fileno.side_effect = AttributeError[](#l2.149)
with support.captured_stderr() as stderr:[](#l2.150)
with self.assertWarns(getpass.GetPassWarning):[](#l2.151)
getpass.unix_getpass()[](#l2.152)
stdin.readline.assert_called_once_with()[](#l2.153)
self.assertIn('Warning', stderr.getvalue())[](#l2.154)
self.assertIn('Password:', stderr.getvalue())[](#l2.155)
--- a/Lib/test/test_sundry.py +++ b/Lib/test/test_sundry.py @@ -41,7 +41,6 @@ class TestUntestedModules(unittest.TestC import encodings import formatter
import getpass[](#l3.7) import html.entities[](#l3.8) import imghdr[](#l3.9) import keyword[](#l3.10)
--- 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,