#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.)
This commit is contained in:
R David Murray 2013-07-10 17:02:24 -04:00
parent acb362e29f
commit 16dbbae298
3 changed files with 69 additions and 51 deletions

View File

@ -15,7 +15,11 @@ On the Mac EasyDialogs.AskPassword is used, if available.
# Guido van Rossum (Windows support and cleanup) # Guido van Rossum (Windows support and cleanup)
# Gregory P. Smith (tty support & GetPassWarning) # Gregory P. Smith (tty support & GetPassWarning)
import os, sys, warnings import contextlib
import io
import os
import sys
import warnings
__all__ = ["getpass","getuser","GetPassWarning"] __all__ = ["getpass","getuser","GetPassWarning"]
@ -38,53 +42,57 @@ def unix_getpass(prompt='Password: ', stream=None):
Always restores terminal settings before returning. Always restores terminal settings before returning.
""" """
fd = None
tty = None
passwd = None passwd = None
try: with contextlib.ExitStack() as stack:
# 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.
try: try:
fd = sys.stdin.fileno() # Always try reading and writing directly on the tty first.
except (AttributeError, ValueError): fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
passwd = fallback_getpass(prompt, stream) tty = io.FileIO(fd, 'w+')
input = sys.stdin stack.enter_context(tty)
if not stream: input = io.TextIOWrapper(tty)
stream = sys.stderr stack.enter_context(input)
if not stream:
if fd is not None: stream = input
passwd = None except OSError as e:
try: # If that fails, see if stdin can be controlled.
old = termios.tcgetattr(fd) # a copy to save stack.close()
new = old[:]
new[3] &= ~termios.ECHO # 3 == 'lflags'
tcsetattr_flags = termios.TCSAFLUSH
if hasattr(termios, 'TCSASOFT'):
tcsetattr_flags |= termios.TCSASOFT
try: try:
termios.tcsetattr(fd, tcsetattr_flags, new) fd = sys.stdin.fileno()
passwd = _raw_input(prompt, stream, input=input) except (AttributeError, ValueError):
finally: fd = None
termios.tcsetattr(fd, tcsetattr_flags, old) passwd = fallback_getpass(prompt, stream)
stream.flush() # issue7208 input = sys.stdin
except termios.error: if not stream:
if passwd is not None: stream = sys.stderr
# _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') if fd is not None:
return passwd 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
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): def win_getpass(prompt='Password: ', stream=None):

View File

@ -1,7 +1,7 @@
import getpass import getpass
import os import os
import unittest import unittest
from io import StringIO from io import BytesIO, StringIO
from unittest import mock from unittest import mock
from test import support from test import support
@ -88,7 +88,8 @@ class UnixGetpassTest(unittest.TestCase):
def test_uses_tty_directly(self): def test_uses_tty_directly(self):
with mock.patch('os.open') as open, \ 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 # 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 # skip code we don't care about in this test. We can mock this out
# fully if an alternate implementation works differently. # fully if an alternate implementation works differently.
@ -96,10 +97,13 @@ class UnixGetpassTest(unittest.TestCase):
getpass.unix_getpass() getpass.unix_getpass()
open.assert_called_once_with('/dev/tty', open.assert_called_once_with('/dev/tty',
os.O_RDWR | os.O_NOCTTY) 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): def test_resets_termios(self):
with mock.patch('os.open') as open, \ 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.tcgetattr') as tcgetattr, \
mock.patch('termios.tcsetattr') as tcsetattr: mock.patch('termios.tcsetattr') as tcsetattr:
open.return_value = 3 open.return_value = 3
@ -110,21 +114,23 @@ class UnixGetpassTest(unittest.TestCase):
def test_falls_back_to_fallback_if_termios_raises(self): def test_falls_back_to_fallback_if_termios_raises(self):
with mock.patch('os.open') as open, \ 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.tcgetattr'), \
mock.patch('termios.tcsetattr') as tcsetattr, \ mock.patch('termios.tcsetattr') as tcsetattr, \
mock.patch('getpass.fallback_getpass') as fallback: mock.patch('getpass.fallback_getpass') as fallback:
open.return_value = 3 open.return_value = 3
fdopen.return_value = StringIO() fileio.return_value = BytesIO()
tcsetattr.side_effect = termios.error tcsetattr.side_effect = termios.error
getpass.unix_getpass() getpass.unix_getpass()
fallback.assert_called_once_with('Password: ', fallback.assert_called_once_with('Password: ',
fdopen.return_value) textio.return_value)
def test_flushes_stream_after_input(self): def test_flushes_stream_after_input(self):
# issue 7208 # issue 7208
with mock.patch('os.open') as open, \ 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.tcgetattr'), \
mock.patch('termios.tcsetattr'): mock.patch('termios.tcsetattr'):
open.return_value = 3 open.return_value = 3

View File

@ -142,6 +142,10 @@ Core and Builtins
Library 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 - Issue #17198: Fix a NameError in the dbm module. Patch by Valentina
Mukhamedzhanova. Mukhamedzhanova.