Redirecting the stdout on a gtk.TextView
Suppose you want to spawn a system process in python and you want to intercept the stdout of the process and redirecting it on a gtk.TextView.
For example in this tutorial you will see how to redirect a generic command ( in the example there is a typical blocking process).
The most correct solution of the problem is not so immediate, if you search around you will get some answers related to threads and so on… If you study threads is a good thing but we can do it without knowing threads. Expecially because threads in python can be buggy in some particular cases ( like in pygtk itself).
Spawning a process
To spawn a process the best solution ever is to use the subprocess module. The syntax is simple:
- popen_object = subprocess.Popen(command, shell = True, stdout = subprocess.PIPE)
- We tell to subprocess to spawn a process with the command “command”
- The option shell = True tell subprocess that the command is a shell command
- We also want to redirect the stdout to a pipe -> that is the popen_object.
import subprocess proc = subprocess.Popen("ls ../", shell = True,stdout=subrocess.PIPE) # ls ../ is an example
The proc object is a Popen object, it has some interesting methods and attributes, in particular we can access
the stdout of the process, (like) it’s a file.
Interesting methods and attributes:
- proc.communicate() will wait until the process finish and give us the stdout
- proc.kill() will kill the process
- proc.stdout is a file descriptor (an opened file) that we can use
There are a lot of other features, if you are interested (maybe you are) look at the documentation.
Redirecting the stdout
To read and redirect the stdout of the process to the TextView widget we will use the glib library that’s shipped with pygtk. This library has a function glib.io_add_watch that is pretty smart:
def glib.io_add_watch(fd, condition, callback, ...)
fd : a Python file object or an integer file descriptor ID condition : a condition mask callback : a function to call ... : additional arguments to pass to callback Returns : an integer ID of the event source
The glib.io_add_watch() function arranges for the file (specified by fd) to be monitored by the main loop for the specified condition. fd may be a Python file object or an integer file descriptor. The value of condition is a combination of:
glib.IO_IN There is data to read. glib.IO_OUT Data can be written (without blocking). glib.IO_PRI There is urgent data to read. glib.IO_ERR Error condition. glib.IO_HUP Hung up (the connection has been broken, usually for pipes and sockets).
Additional arguments to pass to callback can be specified after callback. The idle priority may be specified as a keyword-value pair with the keyword “priority”. The signature of the callback function is:
def callback(source, cb_condition, ...)
where source is fd, the file descriptor; cb_condition is the condition that triggered the signal; and, ... are the zero or more arguments that were passed to the glib.io_add_watch() function.
If the callback function returns FALSE it will be automatically removed from the list of event sources and will not be called again. If it returns TRUE it will be called again when the condition is matched.
- The fd is an opened file like our proc.stdout!!
- the condition is something like “there’s something to read”: in computer language glib.IO_IN
- the callback is a normal function with this prototype: callback(fd, condition_triggered), the condition triggered tell us why the callback was called (there’s something to read, someone has interrupted the process and so on…)
- NOTE: this particular callback has to return True or False, from the reference:
“If the callback function returns
FALSEit will be automatically removed from the list of event sources and will not be called again. If it returns
TRUEit will be called again when the condition is matched.”
Creating you smart TextView
the plan is to create a special textview, we pass a command, and this Textview presents itself giving us the command stdout. Well we know that her constructor has one argument:
#Test driven developement, write the test: def test(): # This command will download one project of mine deheh ctv = CommandTextView("svn co https://fitta.svn.sourceforge.net/svnroot/fitta fitta") win=gtk.Window() win.connect("delete-event", lambda wid,event: gtk.main_quit()) # Defining callbacks with lambdas win.set_size_request(200,300) win.add(ctv) win.show_all() ctv.run() gtk.main() if __name__=='__main__' : test()
We need also a method run that starts all the things, now let’s write the class, in the constructor simply
we will save the command as a variable and we’ll do the initialization stuff, in the run we will use subprocess to spawn the process and we will use glib.io_add_watch to fetch the stdout.
import gtk,glib import subprocess class CommandTextView(gtk.TextView): ''' Nice TextView that reads the output of a command syncronously ''' def __init__(self, command): '''command : the shell command to spawn''' super(CommandTextView, self).__init__() self.command = command def run(self): ''' Runs the process ''' proc = subprocess.Popen(self.command, stdout = subprocess.PIPE) # Spawning glib.io_add_watch(proc.stdout, # file descriptor glib.IO_IN, # condition self.write_to_buffer ) # callback def write_to_buffer(self, fd, condition): if condition == glib.IO_IN: #if there's something interesting to read char = fd.read(1) # we read one byte per time, to avoid blocking buf = self.get_buffer() buf.insert_at_cursor(char) # When running don't touch the TextView!! return True # FUNDAMENTAL, otherwise the callback isn't recalled else: return False # Raised an error: exit and I don't want to see you anymore
Every time there is something to read the glib.io_add_watch recall the callback,
and in the callback we write to the textview, nice and simple.
For the complete example, you can download at this link: