I’ve been doing some nasty database work with Python and MySQL. Specifically, I’ve got long programs that run eight to ten hours, and sometimes I need to kill them and restart them later.
So… I wanted a way to trap CTRL+C and clean up. That is, I wanted to have transactions without actually having to use transactions. I’m funny that way. (In other news, I’m probably moving to PostgreSQL at some point.)
Anyway, here’s what I came up with. You might find it useful, too.
#!/usr/bin/python -i ''' Trap keyboard interrupts. No rights reserved; use at your own risk. @author: Stacy Prowell (http://stacyprowell.com) ''' import signal class BreakHandler: ''' Trap CTRL-C, set a flag, and keep going. This is very useful for gracefully exiting database loops while simulating transactions. To use this, make an instance and then enable it. You can check whether a break was trapped using the trapped property. # Create and enable a break handler. ih = BreakHandler() ih.enable() for x in big_set: complex_operation_1() complex_operation_2() complex_operation_3() # Check whether there was a break. if ih.trapped: # Stop the loop. break ih.disable() # Back to usual operation... ''' def __init__(self, emphatic=9): ''' Create a new break handler. @param emphatic: This is the number of times that the user must press break to *disable* the handler. If you press break this number of times, the handler is automagically disabled, and one more break will trigger an old style keyboard interrupt. The default is nine. This is a Good Idea, since if you happen to lose your connection to the handler you can *still* disable it. ''' self._count = 0 self._enabled = False self._emphatic = emphatic self._oldhandler = None return def _reset(self): ''' Reset the trapped status and count. You should not need to use this directly; instead you can disable the handler and then re-enable it. This is better, in case someone presses CTRL-C during this operation. ''' self._count = 0 return def enable(self): ''' Enable trapping of the break. This action also resets the handler count and trapped properties. ''' if not self._enabled: self._reset() self._enabled = True self._oldhandler = signal.signal(signal.SIGINT, self) return def disable(self): ''' Disable trapping the break. You can check whether a break was trapped using the count and trapped properties. ''' if self._enabled: self._enabled = False signal.signal(signal.SIGINT, self._oldhandler) self._oldhandler = None return def __call__(self, signame, sf): ''' An break just occurred. Save information about it and keep going. ''' self._count += 1 # If we've exceeded the "emphatic" count disable this handler. if self._count >= self._emphatic: self.disable() return def __del__(self): ''' Python is reclaiming this object, so make sure we are disabled. ''' self.disable() return @property def count(self): ''' The number of breaks trapped. ''' return self._count @property def trapped(self): ''' Whether a break was trapped. ''' return self._count > 0
To use this, make an instance and then enable the instance. If you repeatedly hit CTRL+C you will eventually disable it (see emphatic in the __init__).
# Trap break. bh = BreakHandler() bh.enable() for item in big_set: if bh.trapped: print 'Stopping at user request (keyboard interrupt)...' break do_thing_1() do_thing_2() do_thing_3() do_cleanup() bh.disable()
If you press CTRL+C, it is detected at the start of the loop, and break is called. Then do_cleanup() runs, and the break handler is disabled, returning things to normal. The idea is that do_thing_1(), do_thing_2(), and do_thing_3() all run to completion, even when the user presses CTRL+C, and do_cleanup() always runs when the loop is exited.
I consider this public domain; if you like it, use it. If you don’t like it, don’t use it. If you have suggestions, please leave me a comment.