#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.)
diff --git a/Lib/getpass.py b/Lib/getpass.py
index 6ec6c788..53c38b8 100644
--- 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,53 +42,57 @@
 
     Always restores terminal settings before returning.
     """
-    fd = None
-    tty = None
     passwd = 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 OSError as e:
-        # If that fails, see if stdin can be controlled.
+    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:
-                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)
+                fd = sys.stdin.fileno()
+            except (AttributeError, ValueError):
+                fd = None
+                passwd = fallback_getpass(prompt, stream)
+            input = sys.stdin
+            if not stream:
+                stream = sys.stderr
 
-    stream.write('\n')
-    return passwd
+        if fd is not 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
+                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):