Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 16dbbae

Browse files
committed
#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.)
1 parent acb362e commit 16dbbae

3 files changed

Lines changed: 70 additions & 52 deletions

File tree

Lib/getpass.py

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
# Guido van Rossum (Windows support and cleanup)
1616
# Gregory P. Smith (tty support & GetPassWarning)
1717

18-
import os, sys, warnings
18+
import contextlib
19+
import io
20+
import os
21+
import sys
22+
import warnings
1923

2024
__all__ = ["getpass","getuser","GetPassWarning"]
2125

@@ -38,53 +42,57 @@ def unix_getpass(prompt='Password: ', stream=None):
3842
3943
Always restores terminal settings before returning.
4044
"""
41-
fd = None
42-
tty = None
4345
passwd = None
44-
try:
45-
# Always try reading and writing directly on the tty first.
46-
fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
47-
tty = os.fdopen(fd, 'w+', 1)
48-
input = tty
49-
if not stream:
50-
stream = tty
51-
except OSError as e:
52-
# If that fails, see if stdin can be controlled.
46+
with contextlib.ExitStack() as stack:
5347
try:
54-
fd = sys.stdin.fileno()
55-
except (AttributeError, ValueError):
56-
passwd = fallback_getpass(prompt, stream)
57-
input = sys.stdin
58-
if not stream:
59-
stream = sys.stderr
60-
61-
if fd is not None:
62-
passwd = None
63-
try:
64-
old = termios.tcgetattr(fd) # a copy to save
65-
new = old[:]
66-
new[3] &= ~termios.ECHO # 3 == 'lflags'
67-
tcsetattr_flags = termios.TCSAFLUSH
68-
if hasattr(termios, 'TCSASOFT'):
69-
tcsetattr_flags |= termios.TCSASOFT
48+
# Always try reading and writing directly on the tty first.
49+
fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
50+
tty = io.FileIO(fd, 'w+')
51+
stack.enter_context(tty)
52+
input = io.TextIOWrapper(tty)
53+
stack.enter_context(input)
54+
if not stream:
55+
stream = input
56+
except OSError as e:
57+
# If that fails, see if stdin can be controlled.
58+
stack.close()
59+
try:
60+
fd = sys.stdin.fileno()
61+
except (AttributeError, ValueError):
62+
fd = None
63+
passwd = fallback_getpass(prompt, stream)
64+
input = sys.stdin
65+
if not stream:
66+
stream = sys.stderr
67+
68+
if fd is not None:
7069
try:
71-
termios.tcsetattr(fd, tcsetattr_flags, new)
72-
passwd = _raw_input(prompt, stream, input=input)
73-
finally:
74-
termios.tcsetattr(fd, tcsetattr_flags, old)
75-
stream.flush() # issue7208
76-
except termios.error:
77-
if passwd is not None:
78-
# _raw_input succeeded. The final tcsetattr failed. Reraise
79-
# instead of leaving the terminal in an unknown state.
80-
raise
81-
# We can't control the tty or stdin. Give up and use normal IO.
82-
# fallback_getpass() raises an appropriate warning.
83-
del input, tty # clean up unused file objects before blocking
84-
passwd = fallback_getpass(prompt, stream)
85-
86-
stream.write('\n')
87-
return passwd
70+
old = termios.tcgetattr(fd) # a copy to save
71+
new = old[:]
72+
new[3] &= ~termios.ECHO # 3 == 'lflags'
73+
tcsetattr_flags = termios.TCSAFLUSH
74+
if hasattr(termios, 'TCSASOFT'):
75+
tcsetattr_flags |= termios.TCSASOFT
76+
try:
77+
termios.tcsetattr(fd, tcsetattr_flags, new)
78+
passwd = _raw_input(prompt, stream, input=input)
79+
finally:
80+
termios.tcsetattr(fd, tcsetattr_flags, old)
81+
stream.flush() # issue7208
82+
except termios.error:
83+
if passwd is not None:
84+
# _raw_input succeeded. The final tcsetattr failed. Reraise
85+
# instead of leaving the terminal in an unknown state.
86+
raise
87+
# We can't control the tty or stdin. Give up and use normal IO.
88+
# fallback_getpass() raises an appropriate warning.
89+
if stream is not input:
90+
# clean up unused file objects before blocking
91+
stack.close()
92+
passwd = fallback_getpass(prompt, stream)
93+
94+
stream.write('\n')
95+
return passwd
8896

8997

9098
def win_getpass(prompt='Password: ', stream=None):

Lib/test/test_getpass.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import getpass
22
import os
33
import unittest
4-
from io import StringIO
4+
from io import BytesIO, StringIO
55
from unittest import mock
66
from test import support
77

@@ -88,18 +88,22 @@ class UnixGetpassTest(unittest.TestCase):
8888

8989
def test_uses_tty_directly(self):
9090
with mock.patch('os.open') as open, \
91-
mock.patch('os.fdopen'):
91+
mock.patch('io.FileIO') as fileio, \
92+
mock.patch('io.TextIOWrapper') as textio:
9293
# By setting open's return value to None the implementation will
9394
# skip code we don't care about in this test. We can mock this out
9495
# fully if an alternate implementation works differently.
9596
open.return_value = None
9697
getpass.unix_getpass()
9798
open.assert_called_once_with('/dev/tty',
9899
os.O_RDWR | os.O_NOCTTY)
100+
fileio.assert_called_once_with(open.return_value, 'w+')
101+
textio.assert_called_once_with(fileio.return_value)
99102

100103
def test_resets_termios(self):
101104
with mock.patch('os.open') as open, \
102-
mock.patch('os.fdopen'), \
105+
mock.patch('io.FileIO'), \
106+
mock.patch('io.TextIOWrapper'), \
103107
mock.patch('termios.tcgetattr') as tcgetattr, \
104108
mock.patch('termios.tcsetattr') as tcsetattr:
105109
open.return_value = 3
@@ -110,21 +114,23 @@ def test_resets_termios(self):
110114

111115
def test_falls_back_to_fallback_if_termios_raises(self):
112116
with mock.patch('os.open') as open, \
113-
mock.patch('os.fdopen') as fdopen, \
117+
mock.patch('io.FileIO') as fileio, \
118+
mock.patch('io.TextIOWrapper') as textio, \
114119
mock.patch('termios.tcgetattr'), \
115120
mock.patch('termios.tcsetattr') as tcsetattr, \
116121
mock.patch('getpass.fallback_getpass') as fallback:
117122
open.return_value = 3
118-
fdopen.return_value = StringIO()
123+
fileio.return_value = BytesIO()
119124
tcsetattr.side_effect = termios.error
120125
getpass.unix_getpass()
121126
fallback.assert_called_once_with('Password: ',
122-
fdopen.return_value)
127+
textio.return_value)
123128

124129
def test_flushes_stream_after_input(self):
125130
# issue 7208
126131
with mock.patch('os.open') as open, \
127-
mock.patch('os.fdopen'), \
132+
mock.patch('io.FileIO'), \
133+
mock.patch('io.TextIOWrapper'), \
128134
mock.patch('termios.tcgetattr'), \
129135
mock.patch('termios.tcsetattr'):
130136
open.return_value = 3

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ Core and Builtins
142142
Library
143143
-------
144144

145+
- Issue #18116: getpass was always getting an error when testing /dev/tty,
146+
and thus was always falling back to stdin. It also leaked an open file
147+
when it did so. Both of these issues are now fixed.
148+
145149
- Issue #17198: Fix a NameError in the dbm module. Patch by Valentina
146150
Mukhamedzhanova.
147151

0 commit comments

Comments
 (0)