Auto-reloading Python Applications
I wrote a really handy little script for a project I’m working on involving a Sugar Activity that automatically reloads the code whenever a file is changed. It borrows heavily from the Django project’s manage.py runserver command, and takes the form of an activity launcher script. You could use this for auto-reloading of any single-threaded Python application, not just GUI applications.
Although the code monitoring part is basically the same as the Django server’s, I added an feature that’s always seemed missing to me - When an exception is thrown starting up the program, it prints out a stack trace, waits 10 seconds, and then resumes. This is really handy if you leave it running, and are in the habit of compusively saving every 15 seconds, like me. Sometimes when you make a syntax error in a core part of the application, the server crashes and you have to restart it, which seems a bit silly. I also had to re-work it a bit to play along with the GUI framework (PyGTK).
I started off with the autoreload module used in
Django,
which has a handy python_reloader wrapper function. If the application is
single threaded and doesn’t use an event loop, you could probably just use this.
It works by starting a child process, which then starts two threads - one for
your application’s entry point, and the other that monitors the code for changes
by inspecting sys.modules every second. If a change is detected, the monitor
thread shuts down the process. Meanwhile, the first process has been waiting for
this, and starts a brand new child process. This is all done through one script,
by cleverly setting an environment variable RUN_MAIN for the child process
which makes it start the two threads instead of its own child.
Unfortunately, I needed this to work inside of the GTK event loop, and not in
two different threads. There was an easy workaround though - the
gtk.timeout_add and gobject.timeout_add functions allow callbacks to be
scheduled at a predefined interval. I wrote a handler for calling the
code_changed function, and one for quitting the GTK main loop. RESTART_CODE
is a return code that’s used to indicate the application ended because of a code
change, instead of just crashing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Now I needed the main part of the activity runner, which sets up the options and
environment variables. I used optparse to add two command-line options, a
watch flag (which does what you’d expect), and a timeout flag. The timeout flag
exits the application after a given number of seconds, which is sometimes handy
for testing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
This next part is trickier. The first block should only execute in the child
process, but comes first so that it can disable starting a child process of its
own. Similar to python_reloader and friends, the next block turns on the
environment variable flag and runs itself as a child process. When the child
exits, the return code is used to decide what to do next. A zero means that the
event loop was exited normally, probably through the application GUI, so the
main loop ends when this happens. I changed the script here to allow for
errors, by adding a brief delay when a non-zero, non-restart code is returned,
instead of just quitting (the original behaviour)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Since the master process is stuck in that loop until the application exits, all that’s left now is to start the app! I’m leaving out some Sugar-specific environment variable stuff, but this is where you’d call your application entry point.
1 2 | |
This might seem like a lot of effort just for an auto-reloading script, but in my case it had a more important use. I’m now able to develop on my Mac while sharing the code folders through NFS with a virtual machine I use to run the application. The application just stays running on the VM all the time, and whenever I save a code file, it restarts automatically. I set up the VM once, display it on another monitor, and can leave it alone after that!