Search code examples
python-3.xmacostkinterinteractive

Why doesn't Tkinter window close on `destroy` in interactive session (IDLE / REPL) in macOS?


I have a function to display a GUI window where the user can type the password. I use IDLE shell a lot during development, so using getpass is not an option.

This function is part of a utility script that has other functions I want to be able to use (after submitting the password using this function).

import tkinter


def password_from_user():
    password = None
    
    def get_password(*__):
        nonlocal password
        password = entry.get()
        window.destroy()
    
    window = tkinter.Tk()
    entry = tkinter.Entry(window, width=40, show='*')
    entry.bind('<Return>', get_password)
    entry.pack()
    entry.focus_set()
    
    window.mainloop()
    
    return password

The password gets stored and returned from the function. However, the Tkinter window stays open. I can press +Tab to switch back to IDLE shell, but it's slightly inconvenient. I'd like this window to be completely closed.

If I run this as a script, of course, everything gets closed at the end:

if __name__ == '__main__':
    print(password_from_user())

But, I need this function only for IDLE session. If I were only using command line, I would use getpass which would be more than sufficient.

Edit 1: I have tried using both destroy and quit. Neither of them works for me.

Edit 2: Just tested on a Windows machine with Python 3.8.5. It works. So, I'm pretty sure it's something to do with macOS.


Solution

  • Figured out a solution that satisfies me.

    Save the Tkinter code in a separate module (say, _password_box_ui.py):

    import tkinter
    
    password = None
    
    def get_password(*__):
        global password
        password = entry.get()
        window.quit()
    
    window = tkinter.Tk()
    window.title('Password')
    
    entry = tkinter.Entry(window, width=40, show='*')
    entry.bind('<Return>', get_password)
    entry.pack()
    entry.focus_set()
    
    window.mainloop()
    
    print(password)
    

    Then, in another module (which I'll import in the IDLE shell), use a function like this:

    import pathlib
    import subprocess
    import sys
    
    
    def password_from_user():
        process = subprocess.run(
            [
                f'{sys.base_exec_prefix}/bin/python',
                pathlib.Path(__file__).parent / '_password_box_ui.py',
            ],
            capture_output=True,
        )
        return process.stdout.rstrip(b'\n').decode()
    

    May not be very elegant, but serves my purpose, which is to use IDLE to get password from the user using a GUI (so that, ultimately, no password needs to be stored in plaintext anywhere).