Read Programming Python Online

Authors: Mark Lutz

Tags: #COMPUTERS / Programming Languages / Python

Programming Python (86 page)

BOOK: Programming Python
12.74Mb size Format: txt, pdf, ePub
ads
Adding a GUI As a Separate Program: Command Pipes

The net effect of the two
programs of the preceding section is similar to a GUI
program reading the output of a shell command over a pipe file with
os.popen
(or the
subprocess
.
Popen
interface upon which it is based).
As we’ll see later, though, sockets also support independent servers,
and can link programs running on remote machines across a network—a much
larger idea we’ll be exploring in
Chapter 12
.

Perhaps subtler and more significant for our GUI exploration here
is the fact that without an
after
timer loop and nonblocking input sources of the sort used in the prior
section, the GUI may become stuck and unresponsive while waiting for
data from the non-GUI program and may not be able to handle more than
one data stream.

For instance, consider the
guiStreams
call we wrote in
Example 10-12
to redirect the
output of a shell command spawned with
os.popen
to a GUI window. We could use this
with simplistic code like that in
Example 10-26
to capture the
output of a spawned Python program and display it in a separately
running GUI program’s window. This is as concise as it is because it
relies on the read/write loop and
GuiOutput
class in
Example 10-12
to both manage the
GUI and read the pipe; it’s essentially the same as one of the options
in that example’s self-test code, but we read the printed output of a
Python program here.

Example 10-26. PP4E\Gui\Tools\pipe-gui1.py

# GUI reader side: route spawned program standard output to a GUI window
from PP4E.Gui.Tools.guiStreams import redirectedGuiShellCmd # uses GuiOutput
redirectedGuiShellCmd('python -u pipe-nongui.py') # -u: unbuffered

Notice the
-u
Python
command-line flag used here: it forces the spawned program’s standard
streams to be
unbuffered
, so we get printed text
immediately as it is produced, instead of waiting for the spawned
program to completely finish.

We talked about this option in
Chapter 5
, when discussing deadlocks and pipes.
Recall that
print
writes to
sys.stdout
, which is normally buffered when
connected to a pipe this way. If we don’t use the
-u
flag here and the spawned program doesn’t
manually call
sys.stdout.flush
, we
won’t see any output in the GUI until the spawned program exits or until
its buffers fill up. If the spawned program is a perpetual loop that
does not exit, we may be waiting a long time for output to appear on the
pipe, and hence, in the GUI.

This approach makes the non-GUI code in
Example 10-27
much simpler: it
just writes to standard output as usual, and it need not be concerned
with creating a socket interface. Compare this with its socket-based
equivalent in
Example 10-24
—the loop is the
same, but we don’t need to connect to sockets first (the spawning parent
reads the normal output stream), and don’t need to manually flush output
as it’s produced (the
-u
flag in the spawning parent prevents
buffering).

Example 10-27. PP4E\Gui\Tools\pipe-nongui.py

# non-GUI side: proceed normally, no need for special code
import time
while True: # non-GUI code
print(time.asctime()) # sends to GUI process
time.sleep(2.0) # no need to flush here

Start the GUI script in
Example 10-26
: it launches the
non-GUI program automatically, reads its output as it is created, and
produces the window in
Figure 10-15
—it’s similar to the
socket-based example’s result in
Figure 10-14
, but displays the
str
text strings we get from reading
pipes, not the byte strings of sockets.

Figure 10-15. Messages printed to a GUI from a non-GUI program (command
pipe)

This works, but the GUI is odd—we never call
mainloop
ourselves, and we get a default empty
top-level window. In fact, it apparently works at all only because the
tkinter
update
call issued within the
redirect function enters the Tk event loop momentarily to process
pending events. To do better,
Example 10-28
creates an enclosing
GUI and kicks off an event loop manually by the time the shell command
is spawned; when run, it produces the same output window (
Figure 10-15
).

Example 10-28. PP4E\Gui\Tools\pipe-gui2.py

# GUI reader side: like pipes-gui1, but make root window and mainloop explicit
from tkinter import *
from PP4E.Gui.Tools.guiStreams import redirectedGuiShellCmd
def launch():
redirectedGuiShellCmd('python -u pipe-nongui.py')
window = Tk()
Button(window, text='GO!', command=launch).pack()
window.mainloop()

The
-u
unbuffered flag is
crucial here again—without it, you won’t see the text output window. The
GUI will be blocked in the initial pipe input call indefinitely because
the spawned program’s standard output will be queued up in an in-memory
buffer.

On the other hand, this
-u
unbuffered flag doesn’t prevent blocking in the prior section’s
socket
scheme, because that example resets streams
to other objects after the spawned program starts; more on this in
Chapter 12
. Also remember that the buffering
argument in
os.popen
(and
subprocess.Popen
) controls buffering in the
caller
, not in the spawned program;
-u
pertains to the latter.

The specter of blocking input calls

Either way we code them, however, when the GUIs of
Example 10-26
and
Example 10-28
are run they
become unresponsive for two seconds at a time while they read data
from the
os.popen
pipe. In fact,
they are just plain sluggish—window moves, resizes, redraws, raises,
and so on, are delayed for up to two seconds, until the non-GUI
program sends data to the GUI to make the pipe read call return.
Perhaps worse, if you press the “GO!” button twice in the second
version of the GUI, only one window updates itself every two seconds,
because the GUI is stuck in the second button press callback—it never
exits the loop that reads from the pipe until the spawned non-GUI
program exits. Exits are not necessarily graceful either (you get
multiple error messages in the terminal window).

Because of such constraints, to avoid blocked states, a
separately running GUI cannot generally read data directly if its
appearance may be delayed. For instance, in the socket-based scripts
of the prior section (
Example 10-25
), the
after
timer loop allows the GUI to
poll
for data instead of
waiting
, and display it as it arrives. Because it
doesn’t wait for the data to show up, its GUI remains active in
between outputs.

Of course, the real issue here is that the read/write loop in
the
guiStreams
utility function
used is too simplistic; issuing a read call within a GUI is generally
prone to blocking. There are a variety of ways we might try to avoid
this.

Updating GUIs within threads…and other nonsolutions

One candidate fix is to
try to run the redirection loop call in a thread—for
example, by changing the
launch
function in
Example 10-28
as follows (this is from file
pipe-gui2-thread.py
on the examples
distribution):

def launch():
import _thread
_thread.start_new_thread(redirectedGuiShellCmd, ('python -u pipe-nongui.py',))

But then we would be updating the GUI from a spawned thread,
which, as we’ve learned, is a generally bad idea. Parallel updates can
wreak havoc in GUIs.

If fact, with this change the GUI fails
spectacularly
—it hangs immediately on the first
“GO!” button press on my Windows 7 laptop, becomes unresponsive, and
must be forcibly closed. This happens before (or perhaps during) the
creation of the new pop-up scrolled-text window. When this example was
run on Windows XP for the prior edition of this book, it also hung on
the first “GO!” press occasionally and always hung eventually if you
pressed the button enough times; the process had to be forcibly
killed. Direct GUI updates in threads are not a viable
solution.

Alternatively, we could try to use the Python
select.select
call (described in
Chapter 12
) to implement polling for data on the
input pipe; unfortunately,
select
works only on sockets in Windows today (it also works on pipes and
other file descriptors in Unix).

In other contexts, a separately spawned GUI might also use
signals to inform the non-GUI program when points of interaction
arise, and vice versa (the Python
signal
module and
os.kill
call were introduced in
Chapter 5
). The downside with this approach
is that it still requires changes to the non-GUI program to handle the
signals.

Named pipes (the fifo files introduced in
Chapter 5
) are sometimes an alternative to
the socket calls of the original Examples
10-23
through
10-25
, but sockets work on standard
Windows Python, and fifos do not (
os.mkfifo
is not available in Windows in
Python 3.1, though it is in Cygwin Python). Even where they do work,
we would still need an
after
timer
loop in the GUI to avoid blocking.

We might also use tkinter’s
createfilehandler
to register a callback to
be run when input shows up on the input pipe:

def callback(file, mask):
...read from file here...
import _tkinter, tkinter
_tkinter.createfilehandler(file, tkinter.READABLE, callback)

The file handler creation call is also available within
tkinter
and as a method of a
Tk
instance object. Unfortunately again, as
noted near the end of
Chapter 9
,
this call is not available on Windows and is a Unix-only
alternative.

Avoiding blocking input calls with non-GUI threads

As a far more general solution to the blocking input delays of
the prior section, the GUI process might instead spawn a thread that
reads the socket or pipe and places the data on a queue. In fact, the
thread techniques we met earlier in this chapter could be used
directly in such a role. This way, the GUI is not blocked while the
thread waits for data to show up, and the thread does not attempt to
update the GUI itself. Moreover, more than one data stream or
long-running activity can overlap in time.

Example 10-29
shows
how. The main trick this script employs is to split up the input and
output parts of the original
redirectedGuiShellCmd
of the
guiStreams
module we met earlier in
Example 10-12
. By so doing, the
input portion can be spawned off in a parallel thread and not block
the GUI. The main GUI thread uses an
after
timer loop as usual, to watch for data
to be added by the reader thread to a shared queue. Because the main
thread doesn’t read program output itself, it does not get stuck in
wait states.

Example 10-29. PP4E\Gui\Tools\pipe_gui3.py

"""
read command pipe in a thread and place output on a queue checked in timer loop;
allows script to display program's output without being blocked between its outputs;
spawned programs need not connect or flush, but this approaches complexity of sockets
"""
import _thread as thread, queue, os
from tkinter import Tk
from PP4E.Gui.Tools.guiStreams import GuiOutput
stdoutQueue = queue.Queue() # infinite size
def producer(input):
while True:
line = input.readline() # OK to block: child thread
stdoutQueue.put(line) # empty at end-of-file
if not line: break
def consumer(output, root, term=''):
try:
line = stdoutQueue.get(block=False) # main thread: check queue
except queue.Empty: # 4 times/sec, OK if empty
pass
else:
if not line: # stop loop at end-of-file
output.write(term) # else display next line
return
output.write(line)
root.after(250, lambda: consumer(output, root, term))
def redirectedGuiShellCmd(command, root):
input = os.popen(command, 'r') # start non-GUI program
output = GuiOutput(root)
thread.start_new_thread(producer, (input,)) # start reader thread
consumer(output, root)
if __name__ == '__main__':
win = Tk()
redirectedGuiShellCmd('python -u pipe-nongui.py', win)
win.mainloop()

As usual, we use a queue here to avoid updating the GUI except
in the main thread. Note that we didn’t need a thread or queue in the
prior section’s socket example, just because we’re able to poll a
socket to see whether it has data without blocking; an
after
timer loop was enough. For a
shell-command pipe, though, a thread is an easy way to avoid
blocking.

When run, this program’s self-test code creates a
ScrolledText
window that displays the
current date and time sent from the
pipes-nongui.py
script in
Example 10-27
. In fact, its
window is identical to that of the prior versions (see
Figure 10-15
). The window is
updated with a new line every two seconds because that’s how often the
spawned
pipes-nongui
script prints
a message to
stdout
.

Note how the producer thread calls
readline()
to load just one line at a time.
We can’t use input calls that consume the entire stream all at once
(e.g.,
read()
,
readlines()
), because such calls would not
return until the program exits and sends end-of-file. The
read(N)
call would work to grab one piece of
the output as well, but we assume that the output stream is text here.
Also notice that the
-u
unbuffered
stream flag is used here again, to get output as it is produced;
without it, output won’t show up in the GUI at all because it is
buffered in the spawned program (try it yourself).

BOOK: Programming Python
12.74Mb size Format: txt, pdf, ePub
ads

Other books

La jauría by Émile Zola
Defy by Raine Thomas
Celestial Beauty by Angela Castle
Commencement by Sullivan, J. Courtney
Hit and The Marksman by Brian Garfield
Edisto - Padgett Powell by Padgett Powell