What magic prevents Tkinter programs from blocking in interactive shell? - python

Note: This is somewhat a follow-up on the question: Tkinter - when do I need to call mainloop?
Usually when using Tkinter, you call Tk.mainloop to run the event loop and ensure that events are properly processed and windows remain interactive without blocking.
When using Tkinter from within an interactive shell, running the main loop does not seem necessary. Take this example:
>>> import tkinter
>>> t = tkinter.Tk()
A window will appear, and it will not block: You can interact with it, drag it around, and close it.
So, something in the interactive shell does seem to recognize that a window was created and runs the event loop in the background.
Now for the interesting thing. Take the example from above again, but then in the next prompt (without closing the window), enter anything—without actually executing it (i.e. don’t press enter). For example:
>>> t = tkinter.Tk()
>>> print('Not pressing enter now.') # not executing this
If you now try to interact with the Tk window, you will see that it completely blocks. So the event loop which we thought would be running in the background stopped while we were entering a command to the interactive shell. If we send the entered command, you will see that the event loop continues and whatever we did during the blocking will continue to process.
So the big question is: What is this magic that happens in the interactive shell? What runs the main loop when we are not doing it explicitly? And why does it need to halt when we enter commands (instead of halting when we execute them)?
Note: The above works like this in the command line interpreter, not IDLE. As for IDLE, I assume that the GUI won’t actually tell the underlying interpreter that something has been entered but just keep the input locally around until it’s being executed.

It's actually not being an interactive interpreter that matters here, but waiting for input on a TTY. You can get the same behavior from a script like this:
import tkinter
t = tkinter.Tk()
input()
(On Windows, you may have to run the script in pythonw.exe instead of python.exe, but otherwise, you don't have to do anything special.)
So, how does it work? Ultimately, the trick comes down to PyOS_InputHook—the same way the readline module works.
If stdin is a TTY, then, each time it tries to fetch a line with input(), various bits of the code module, the built-in REPL, etc., Python calls any installed PyOS_InputHook instead of just reading from stdin.
It's probably easier to understand what readline does: it tries to select on stdin or similar, looping for each new character of input, or every 0.1 seconds, or every signal.
What Tkinter does is similar. It's more complicated because it has to deal with Windows, but on *nix it's doing something pretty similar to readline. Except that it's calling Tcl_DoOneEvent each time through the loop.
And that's the key. Calling Tcl_DoOneEvent repeatedly is exactly the same thing that mainloop does.
(Threads make everything more complicated, of course, but let's assume you haven't created any background threads. In your real code, if you want to create background threads, you'll just have a thread for all the Tkinter stuff that blocks on mainloop anyway, right?)
So, as long as your Python code is spending most of its time blocked on TTY input (as the interactive interpreter usually is), the Tcl interpreter is chugging along and your GUI is responding. If you make the Python interpreter block on something other than TTY input, the Tcl interpreter is not running and the your GUI is not responding.
What if you wanted to do the same thing manually in pure Python code? You'd of need to do that if you want to, e.g., integrate a Tkinter GUI and a select-based network client into a single-threaded app, right?
That's easy: Drive one loop from the other.
You can select with a timeout of 0.02s (the same timeout the default input hook uses), and call t.dooneevent(Tkinter.DONT_WAIT) each time through the loop.
Or, alternatively, you can let Tk drive by calling mainloop, but use after and friends to make sure you call select often enough.

Related

Python file closes after program execution finishes when using os.startfile()

I have a program that produces a csv file and right at the end I am using os.startfile(fileName) but then due to the program finishing execution the opening file just closes also, same happens if I add a sleep after also, file loads up then once the sleep ends it closes again?
Any help would be appreciated.
From the documentation for os.startfile:
startfile() returns as soon as the associated application is launched. There is no option to wait for the application to close, and no way to retrieve the application’s exit status.
When using this function, there is no way to make your script wait for the program to complete because you have no way of knowing when it is complete. Because the program is being launched as a subprocess of your python script, the program will exit when the python script exits.
Since you don't say in your question exactly what the desired behavior is, I'm going to guess that you want the python script to block until the program finishes execution (as opposed to detaching the subprocess). There are multiple ways to do this.
Use the subprocess module
The subprocess module allows you to make a subprocess call that will not return until the subprocess completes. The exact call you make to launch the subprocess depends heavily on your specific situation, but this is a starting point:
subprocess.Popen(['start', fileName], shell=True)
Use input to allow user to close script
You can have your script block until the user tells the python script that the external program has closed. This probably requires the least modification to your code, but I don't think it's a good solution, as it depends on user input.
os.startfile(fileName)
input('Press enter when external program has completed...')

Python GUI using os.system to run python script cause main GUI "Not Responding"

StackOverflow community.
I am writing a python GUI to monitor another program's data in OSX environment and at one point I decide to click one button to open another python script that I wrote. It does work but it also causes a lag problem of the main GUI program as soon as I click the button. For the lag problem I mean the GUI window is "Not Responding" and I need to force quit. The method I use to run the new script is,
def create_html():
os.system('python realtime.py')
My program doesn't have the multiple class structure, just the simple canvas, and framework. I wonder if this is also the problem to cause my program running slow.
The problem is you are using os.system, which is a blocking call. It will not return control to your main code until python realtime.py is done executing and returned.
You need to use a call that will not block the rest of your program, such as subprocess.Popen.
You can also see this QA for further information

Simplest way to have Python output, from a compiled package?

Prior info: I'm on a Mac.
Q: How can I get terminal-like text output from the program execution, if I compile it with py2app for redistribution?
My case is a program that copies a lot of big files and takes a while to process so I would like to at least have an output notification everytime each file is copied.
This is easy if I run it on the command line, I can just print a new line.
But when I make a self-sufficient package, it simply opens on the bottom dock, with no window, and closes upon completion.
A simple text window would be fine.
Thanks in advance.
If you want to create a simple text window, you need to pick a GUI framework to do that with. For something this simple, there's no reason not to use Tkinter (which comes with any Python) or PyObjC (which is pre-installed with Apple's Python 2.7), unless you happen to be more familiar with wx, gobject, Qt, etc.
At any rate, however you do it, you'll need to write a function that takes a message and appends it to the text window (maybe creating it lazily, if necessary), and call that function wherever you would normally print. You may also want to write and install a logging handler that does the same thing, so you can just log.info stuff. (You could instead create a file-like object that does this and redirect stdout and/or stderr, but unless you have no control over the printing code, that's going to be a lot more work.)
The only real problem here is that a GUI needs an event loop, and you probably just wrote your code as a sequential script.
One way around that is to turn your whole current script into a background thread. If you're using a GUI library that allows you to access the widgets from background threads, everything is easy; your printfunc just does textwidget.append(msg). If not, it may at least have a call_on_main_thread type function, so your printfunc does call_on_main_thread(textwidget.append, msg). If worst comes to worst (and I believe with Tkinter, it does), you have to create an explicit queue to push messages through, and write a queue handler in the event loop. This recipe should give you an idea. Replace the body of workerThread with your code, and end it with self.endApplication(). (There are probably better examples out there; this was just what I found first in a quick search.)
The other way around that is to have your code cooperatively operate with the event loop. Some libraries, like wx, have functions like SafeYield that make things work if you just call it after every chunk of processing. Others don't have that, but have a way to explicitly drive the event loop from your code. Others have neither—but every event loop framework has to have a way to schedule new events, so you can break your code up into a sequence of functions that each finish quickly and then do something like root.after_idle(nextfunc).
However… are you sure you need to do this?
First, any app, including one created by py2app, will send its stdout to the terminal if you run it with Foo.app/Contents/MacOS/Foo. And you can even set things up so that open Foo.app works that way, if you want. Obviously this doesn't help for people who just double-click the app in Finder (because then there is no terminal), but sometimes it's sufficient to just have to output available when people need it and know how to follow instructions.
And you can take this farther: Create a Foo.command file that just does something like $(dirname $0)/Foo.app/Contents/MacOS/Foo, and when you double-click that file, it launches Terminal.app and runs your script.
Or you can get even simpler: Just use logging to syslog the output, and if you want to see when each file is done, just watch the log messages go by in Console.app.
Finally, do you even need py2app in the first place? If you don't have any external dependencies, just rename you script to Foo.command, and double-clicking it will run it in Terminal.app. If you do have external dependencies, you might still be able to get away with bundling it all together as a folder with a .command in it instead of as a .app.
Obviously none of these ideas are exactly a professional or newbie-friendly way to build an interface, so if that matters, you will have to create a GUI.

GUI program with toggable console?

I've seen some apps allow you to show/hide the console when you need to read log messages. For example Blender3D allows that (blender.org).
I was wondering if this can be done in Python and how.
My main window is a Panda3D (panda3d.org) window.
I've read somewhere that one option is to hide the "real" console (pythonw) and create another console and just redirect everything from the "real" one to it, every time you want to "show" the "real" console. No idea how this can be done.
Or at least a way to choose whether to start the program with the console or without it by reading a configuration file or something.
I'm assuming you are talking about Windows because this console toggling in blender is Windows exclusive. I'm guessing Blender uses GetConsoleWindow and ShowWindow on Windows.
This is how you could do it in python with pywin32:
import win32gui, win32console, win32api, win32con
import time
console_window = win32console.GetConsoleWindow()
time.sleep(1)
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
time.sleep(1)
win32gui.ShowWindow(console_window, win32con.SW_SHOW)
time.sleep(1)
If you run this program with python and not pythonw it will show the console, sleep for a second, hide the console, sleep for another second and then hide it again.
Mind that this code only works on Windows. On other platforms silly stuff like this is not necessary because if you want a program to show a console then you run it from the console.

Ctrl-C doesn't work with PyQt [duplicate]

This question already has answers here:
What is the correct way to make my PyQt application quit when killed from the console (Ctrl-C)?
(9 answers)
Closed 9 years ago.
Why doesn't Ctrl+C work to break a Python program that uses PyQt? I want to debug it and get a stack trace and for some reason, this is harder to do than with C++!
CTRL+C causes a signal to be sent to
the process. Python catches the
signal, and sets a global variable,
something like CTRL_C_PRESSED = True.
Then, whenever the Python interpreter
gets to execute a new opcode, it sees
the variable set and raises a
KeybordInterrupt.
This means that CTRL+C works only if
the Python interpreter is spinning. If
the interpreter is executing an
extension module written in C that
executes a long-running operation,
CTRL+C won't interrupt it, unless it
explicitly "cooperates" with Python.
Eg: time.sleep() is theoretically a
blocking operation, but the
implementation of that function
"cooperates" with the Python
interpreter to make CTRL+C work.
This is all by design: CTRL+C is meant
to do a "clean abort"; this is why it
gets turned into an exception by
Python (so that the cleanups are
executed during stack unwind), and its
support by extension modules is sort
of "opt-in". If you want to totally
abort the process, without giving it a
chance to cleanup, you can use CTRL+.
When Python calls QApplication::exec()
(the C++ function), Qt doesn't know
how to "cooperate" with Python for
CTRL+C, and this is why it does not
work. I don't think there's a good way
to "make it work"; you may want to see
if you can handle it through a global
event filter.
— Giovanni Bajo
Adding this to the main program solved the problem.
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
I'm not sure what this has to do with the explanation.
I agree with Neil G, and would add this:
If you do not call QApplication.exec_() to start the event loop, and instead execute your program in an interactive python shell (using python -i), then pyqt will automatically process events whenever the interactive prompt is waiting, and Ctrl-C should again behave as expected. This is because the Qt event loop will be sharing time with the python interpreter, rather than running exclusively, allowing the interpreter a chance to catch those interrupts.

Categories