├── .gitignore ├── MANIFEST.in ├── setup.py ├── LICENSE.txt ├── withrestart ├── tests │ ├── overhead.py │ └── __init__.py ├── callstack.py └── __init__.py ├── ChangeLog.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | *.o 3 | *.pyc 4 | *~ 5 | bbfreeze/*.exe 6 | bbfreeze.egg-info 7 | MANIFEST 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include README.txt 3 | include LICENSE.txt 4 | include ChangeLog.txt 5 | 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is the dexml setuptools script. 3 | # Originally developed by Ryan Kelly, 2009. 4 | # 5 | # This script is placed in the public domain. 6 | # 7 | 8 | from distutils.core import setup 9 | 10 | import withrestart 11 | VERSION = withrestart.__version__ 12 | 13 | NAME = "withrestart" 14 | DESCRIPTION = "a Pythonisation of the restart-based condition system from Common Lisp" 15 | LONG_DESC = withrestart.__doc__ 16 | AUTHOR = "Ryan Kelly" 17 | AUTHOR_EMAIL = "ryan@rfk.id.au" 18 | URL = "http://github.com/rfk/withrestart" 19 | LICENSE = "MIT" 20 | KEYWORDS = "condition restart error exception" 21 | 22 | setup(name=NAME, 23 | version=VERSION, 24 | author=AUTHOR, 25 | author_email=AUTHOR_EMAIL, 26 | url=URL, 27 | description=DESCRIPTION, 28 | long_description=LONG_DESC, 29 | license=LICENSE, 30 | keywords=KEYWORDS, 31 | packages=["withrestart","withrestart.tests"], 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Ryan Kelly 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /withrestart/tests/overhead.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | withrestart.tests.overhead: functions for testing withrestart overhead 4 | 5 | 6 | This module provides two simple functions "test_tryexcept" and "test_restart" 7 | that are used to compare the overhead of a restart-based approach to a bare 8 | try-except clause. 9 | """ 10 | 11 | from withrestart import * 12 | 13 | def test_tryexcept(input,output): 14 | def endpoint(v): 15 | if v == 7: 16 | raise ValueError 17 | return v 18 | def callee(v): 19 | return endpoint(v) 20 | def caller(v): 21 | try: 22 | return callee(v) 23 | except ValueError: 24 | return 0 25 | assert caller(input) == output 26 | 27 | 28 | def test_restart(input,output): 29 | def endpoint(v): 30 | if v == 7: 31 | raise ValueError 32 | return v 33 | def callee(v): 34 | with restarts(use_value) as invoke: 35 | return invoke(endpoint,v) 36 | def caller(v): 37 | with Handler(ValueError,"use_value",0): 38 | return callee(v) 39 | assert caller(input) == output 40 | 41 | 42 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | 2 | v0.2.7: 3 | 4 | * correctly report traceback info when re-raising an exception. 5 | 6 | v0.2.6: 7 | 8 | * make find_restart() search by function object as well as by name. 9 | * make add_handler() automatically determine exc_type for functions 10 | named "handle_" as well as those named "". 11 | 12 | v0.2.5: 13 | 14 | * make raise_error() inject the new error back into the error-handling 15 | machinery; to get the old behaviour, just raise the error directly. 16 | 17 | v0.2.4: 18 | 19 | * speed up invocation of functions within a restart context, by replacing 20 | the _cur_calls CallStack with a custom control-flow exception. 21 | * compatability with psyco's simulated stack frames; just make sure you 22 | import psyco first or call withrestart.callstack.enable_psyco_support(). 23 | 24 | v0.2.3: 25 | 26 | * speed up establishment of handlers and restarts, by using _getframe(n) 27 | instead of explicitly walking up the call stack. 28 | * invoke default handlers only if no other handler has been established. 29 | Previously they were invoked if no other handler invoked a restart. 30 | This required some significant changes to how handlers are invoked, 31 | but none of that should leak into the public API. 32 | 33 | v0.2.2: 34 | 35 | * implement logic for the "skip" restart by raising an ExitRestart 36 | exception rather than by returning NoValue; this makes the non-local 37 | control flow more explicit. 38 | * add a "default_handlers" attribute to RestartSuite. Setting this to a 39 | Handler or HandlerSuite instance will cause those handlers to be invoked 40 | if control passes cleanly through all other handlers. 41 | 42 | v0.2.1: 43 | 44 | * make CallStack drop references to stack frames once they have exited, 45 | to avoid creating lots of uncollectable garbage. 46 | 47 | v0.2.0: 48 | 49 | * cleanly support yielding from within a restart context; previously 50 | this would leave the generator's retarts active. 51 | * require handlers to explicitly raise InvokeRestart(); this makes the 52 | non-local control flow more explicit. 53 | * add a proper RestartSuite class for grouping several restarts into 54 | a single context: 55 | * the RestartSuite is returned when entering its context, and 56 | can be used to invoke functions within the scope of only those 57 | restarts that it contains. 58 | * the decorator syntax for adding restarts is now a normal method 59 | on the RestartSuite object. 60 | * add a proper HandlerSuite class for grouping several handlers into 61 | a single context: 62 | * the decorator syntax for adding handlers is now a normal method 63 | on the HandlerSuite object. 64 | 65 | -------------------------------------------------------------------------------- /withrestart/callstack.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | withrestart.callstack: class to manage per-call-stack context. 4 | 5 | This module provides the CallStack class, which provides a simple stack-like 6 | interface for managing additional context during the flow of execution. 7 | Think of it like a thread-local stack with some extra smarts to account for 8 | suspended generators etc. 9 | 10 | To work correctly while mixing CallStack operations with generators, this 11 | module requires a working implementation of sys._getframe(). 12 | 13 | """ 14 | 15 | import sys 16 | 17 | try: 18 | from sys import _getframe 19 | _getframe() 20 | except Exception: 21 | try: 22 | import threading 23 | class _DummyFrame: 24 | f_back = None 25 | def __init__(self): 26 | self.thread = threading.currentThread() 27 | def __hash__(self): 28 | return hash(self.thread) 29 | def __eq__(self,other): 30 | return self.thread == other.thread 31 | def _getframe(n=0): 32 | return _DummyFrame() 33 | except Exception: 34 | class _DummyFrame: 35 | f_back = None 36 | def _getframe(n=0): 37 | return _DummyFrame 38 | 39 | 40 | def enable_psyco_support(): 41 | """Enable support for psyco's simulated frame objects. 42 | 43 | This function patches psyco's simulated frame objects to be usable 44 | as dictionary keys, and switches internal use of _getframe() to use 45 | the version provided by psyco. 46 | """ 47 | global _getframe 48 | import psyco.support 49 | psyco.support.PythonFrame.__eq__ = lambda s,o: s._frame == o._frame 50 | psyco.support.PythonFrame.__hash__ = lambda self: hash(self._frame) 51 | psyco.support.PsycoFrame.__eq__ = lambda s,o: s._tag[2] == o._tag[2] 52 | psyco.support.PsycoFrame.__hash__ = lambda self: hash(self._tag[2]) 53 | _getframe = psyco.support._getframe 54 | 55 | 56 | if "psyco" in sys.modules: 57 | enable_psyco_support() 58 | 59 | 60 | class CallStack(object): 61 | """Class managing per-call-stack context information. 62 | 63 | Instances of this class can be used to manage a stack of addionnal 64 | information alongside the current execution stack. They have the 65 | following methods: 66 | 67 | * push(item): add an item to the stack for the current exec frame 68 | * pop(item): pop an item from the stack for the current exec frame 69 | * items(): get iterator over stack of items for the current frame 70 | 71 | """ 72 | 73 | def __init__(self): 74 | self._frame_stacks = {} 75 | 76 | def __len__(self): 77 | return len(self._frame_stacks) 78 | 79 | def clear(self): 80 | self._frame_stacks.clear() 81 | 82 | def push(self,item,offset=0): 83 | """Push the given item onto the stack for current execution frame. 84 | 85 | If 'offset' is given, it is the number of execution frames to skip 86 | backwards before adding the item. 87 | """ 88 | # We add one to the offset to account for this function call. 89 | frame = _getframe(offset+1) 90 | try: 91 | frame_stack = self._frame_stacks[frame] 92 | except KeyError: 93 | self._frame_stacks[frame] = frame_stack = [] 94 | frame_stack.append(item) 95 | 96 | def pop(self): 97 | """Pop the top item from the stack for the current execution frame.""" 98 | frame = _getframe(1) 99 | frame_stack = None 100 | while frame_stack is None: 101 | try: 102 | frame_stack = self._frame_stacks[frame] 103 | except KeyError: 104 | frame = frame.f_back 105 | if frame is None: 106 | raise IndexError("stack is empty") 107 | frame_stack.pop() 108 | if not frame_stack: 109 | del self._frame_stacks[frame] 110 | 111 | def items(self): 112 | """Iterator over stack of items for current execution frame.""" 113 | frame = _getframe(1) 114 | while frame is not None: 115 | try: 116 | frame_stack = self._frame_stacks[frame] 117 | except KeyError: 118 | pass 119 | else: 120 | for item in reversed(frame_stack): 121 | yield item 122 | frame = frame.f_back 123 | 124 | 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Status: Unmaintained 3 | ==================== 4 | 5 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 6 | 7 | I am [no longer actively maintaining this project](https://rfk.id.au/blog/entry/archiving-open-source-projects/). 8 | 9 | 10 | withrestart: structured error recovery using named restart functions 11 | ===================================================================== 12 | 13 | This is a Pythonisation (Lispers might rightly say "bastardisation") of the 14 | restart-based condition system of Common Lisp. It's designed to make error 15 | recovery simpler and easier by removing the assumption that unhandled errors 16 | must be fatal. 17 | 18 | A "restart" represents a named strategy for resuming execution of a function 19 | after the occurrence of an error. At any point during its execution a 20 | function can push a Restart object onto its call stack. If an exception 21 | occurs within the scope of that Restart, code higher-up in the call chain can 22 | invoke it to recover from the error and let the function continue execution. 23 | By providing several restarts, functions can offer several different strategies 24 | for recovering from errors. 25 | 26 | A "handler" represents a higher-level strategy for dealing with the occurrence 27 | of an error. It is conceptually similar to an "except" clause, in that one 28 | establishes a suite of Handler objects to be invoked if an error occurs during 29 | the execution of some code. There is, however, a crucial difference: handlers 30 | are executed without unwinding the call stack. They thus have the opportunity 31 | to take corrective action and then resume execution of whatever function 32 | raised the error. 33 | 34 | For example, consider a function that reads the contents of all files from a 35 | directory into a dict in memory:: 36 | 37 | def readall(dirname): 38 | data = {} 39 | for filename in os.listdir(dirname): 40 | filepath = os.path.join(dirname,filename) 41 | data[filename] = open(filepath).read() 42 | return data 43 | 44 | If one of the files goes missing after the call to os.listdir() then the 45 | subsequent open() will raise an IOError. While we could catch and handle the 46 | error inside this function, what would be the appropriate action? Should 47 | files that go missing be silently ignored? Should they be re-created with 48 | some default contents? Should a special sentinel value be placed in the 49 | data dictionary? What value? The readall() function does not have enough 50 | information to decide on an appropriate recovery strategy. 51 | 52 | Instead, readall() can provide the *infrastructure* for doing error recovery 53 | and leave the final decision up to the calling code. The following definition 54 | uses three pre-defined restarts to let the calling code (a) skip the missing 55 | file completely, (2) retry the call to open() after taking some corrective 56 | action, or (3) use some other value in place of the missing file:: 57 | 58 | def readall(dirname): 59 | data = {} 60 | for filename in os.listdir(dirname): 61 | filepath = os.path.join(dirname,filename) 62 | with restarts(skip,retry,use_value) as invoke: 63 | data[filename] = invoke(open,filepath).read() 64 | return data 65 | 66 | Of note here is the use of the "with" statement to establish a new context 67 | in the scope of restarts, and use of the "invoke" wrapper when calling a 68 | function that might fail. The latter allows restarts to inject an alternate 69 | return value for the failed function. 70 | 71 | Here's how the calling code would look if it wanted to silently skip the 72 | missing file:: 73 | 74 | def concatenate(dirname): 75 | with Handler(IOError,"skip"): 76 | data = readall(dirname) 77 | return "".join(data.itervalues()) 78 | 79 | This pushes a Handler instance into the execution context, which will detect 80 | IOError instances and respond by invoking the "skip" restart point. If this 81 | handler is invoked in response to an IOError, execution of the readall() 82 | function will continue immediately following the "with restarts(...)" block. 83 | 84 | Note that there is no way to achieve this skip-and-continue behaviour using an 85 | ordinary try-except block; by the time the IOError has propagated up to the 86 | concatenate() function for processing, all context from the execution of 87 | readall() will have been unwound and cannot be resumed. 88 | 89 | Calling code that wanted to re-create the missing file would simply push a 90 | different error handler:: 91 | 92 | def concatenate(dirname): 93 | def handle_IOError(e): 94 | open(e.filename,"w").write("MISSING") 95 | raise InvokeRestart("retry") 96 | with Handler(IOError,handle_IOError): 97 | data = readall(dirname) 98 | return "".join(data.itervalues()) 99 | 100 | By raising InvokeRestart, this handler transfers control back to the restart 101 | that was established by the readall() function. This particular restart 102 | will re-execute the failing function call and let readall() continue with its 103 | operation. 104 | 105 | Calling code that wanted to use a special sentinel value would use a handler 106 | to pass the required value to the "use_value" restart:: 107 | 108 | def concatenate(dirname): 109 | class MissingFile: 110 | def read(): 111 | return "MISSING" 112 | def handle_IOError(e): 113 | raise InvokeRestart("use_value",MissingFile()) 114 | with Handler(IOError,handle_IOError): 115 | data = readall(dirname) 116 | return "".join(data.itervalues()) 117 | 118 | 119 | By separating the low-level details of recovering from an error from the 120 | high-level strategy of what action to take, it's possible to create quite 121 | powerful recovery mechanisms. 122 | 123 | While this module provides a handful of pre-built restarts, functions will 124 | usually want to create their own. This can be done by passing a callback 125 | into the Restart object constructor:: 126 | 127 | def readall(dirname): 128 | data = {} 129 | for filename in os.listdir(dirname): 130 | filepath = os.path.join(dirname,filename) 131 | def log_error(): 132 | print "an error occurred" 133 | with Restart(log_error): 134 | data[filename] = open(filepath).read() 135 | return data 136 | 137 | 138 | Or by using a decorator to define restarts inline:: 139 | 140 | def readall(dirname): 141 | data = {} 142 | for filename in os.listdir(dirname): 143 | filepath = os.path.join(dirname,filename) 144 | with restarts() as invoke: 145 | @invoke.add_restart 146 | def log_error(): 147 | print "an error occurred" 148 | data[filename] = open(filepath).read() 149 | return data 150 | 151 | Handlers can also be defined inline using a similar syntax:: 152 | 153 | def concatenate(dirname): 154 | with handlers() as h: 155 | @h.add_handler 156 | def IOError(e): 157 | open(e.filename,"w").write("MISSING") 158 | raise InvokeRestart("retry") 159 | data = readall(dirname) 160 | return "".join(data.itervalues()) 161 | 162 | 163 | Now finally, a disclaimer. I've never written any Common Lisp. I've only read 164 | about the Common Lisp condition system and how awesome it is. I'm sure there 165 | are many things that it can do that this module simply cannot. For example: 166 | 167 | * Since this is built on top of a standard exception-throwing system, the 168 | handlers can only be executed after the stack has been unwound to the 169 | most recent restart context; in Common Lisp they're executed without 170 | unwinding the stack at all. 171 | * Since this is built on top of a standard exception-throwing system, it's 172 | probably too heavyweight to use for generic condition signalling system. 173 | 174 | Nevertheless, there's no shame in pinching a good idea when you see one... 175 | 176 | -------------------------------------------------------------------------------- /withrestart/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import with_statement 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import threading 8 | import timeit 9 | 10 | import withrestart 11 | from withrestart import * 12 | 13 | #import psyco 14 | #withrestart.callstack.enable_psyco_support() 15 | #psyco.full() 16 | 17 | 18 | def div(a,b): 19 | return a/b 20 | 21 | class TestRestarts(unittest.TestCase): 22 | """Testcases for the "withrestart" module.""" 23 | 24 | def tearDown(self): 25 | # Check that no stray frames exist in various CallStacks 26 | try: 27 | self.assertEquals(len(withrestart._cur_restarts),0) 28 | self.assertEquals(len(withrestart._cur_handlers),0) 29 | finally: 30 | withrestart._cur_restarts.clear() 31 | withrestart._cur_handlers.clear() 32 | 33 | def test_basic(self): 34 | def handle_TypeError(e): 35 | raise InvokeRestart("use_value",7) 36 | with Handler(TypeError,handle_TypeError): 37 | with Restart(use_value): 38 | self.assertEquals(div(6,3),2) 39 | self.assertEquals(invoke(div,6,3),2) 40 | self.assertEquals(invoke(div,6,"2"),7) 41 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 42 | 43 | def test_multiple(self): 44 | def handle_TE(e): 45 | raise InvokeRestart("use_value",7) 46 | def handle_ZDE(e): 47 | raise InvokeRestart("raise_error",RuntimeError) 48 | with handlers((TypeError,handle_TE),(ZeroDivisionError,handle_ZDE)): 49 | with restarts(use_value,raise_error) as invoke: 50 | self.assertEquals(div(6,3),2) 51 | self.assertEquals(invoke(div,6,3),2) 52 | self.assertEquals(invoke(div,6,"2"),7) 53 | self.assertRaises(RuntimeError,invoke,div,6,0) 54 | 55 | def test_nested(self): 56 | with Handler(TypeError,"use_value",7): 57 | with restarts(use_value) as invoke: 58 | self.assertEquals(div(6,3),2) 59 | self.assertEquals(invoke(div,6,3),2) 60 | self.assertEquals(invoke(div,6,"2"),7) 61 | with Handler(TypeError,"use_value",9): 62 | self.assertEquals(invoke(div,6,"2"),9) 63 | self.assertEquals(invoke(div,6,"2"),7) 64 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 65 | with handlers((ZeroDivisionError,"raise_error",RuntimeError)): 66 | self.assertRaises(MissingRestartError,invoke,div,6,0) 67 | with restarts(raise_error,invoke) as invoke: 68 | self.assertEquals(div(6,3),2) 69 | self.assertEquals(invoke(div,6,3),2) 70 | self.assertEquals(invoke(div,6,"2"),7) 71 | self.assertRaises(RuntimeError,invoke,div,6,0) 72 | self.assertRaises(MissingRestartError,invoke,div,6,0) 73 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 74 | self.assertEquals(invoke(div,6,"2"),7) 75 | 76 | 77 | def test_default_handlers(self): 78 | with restarts(use_value) as invoke: 79 | self.assertEquals(div(6,3),2) 80 | self.assertEquals(invoke(div,6,3),2) 81 | self.assertRaises(TypeError,invoke,div,6,"2") 82 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 83 | invoke.default_handlers = Handler(TypeError,"use_value",7) 84 | self.assertEquals(invoke(div,6,"2"),7) 85 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 86 | with Handler(TypeError,"use_value",9): 87 | self.assertEquals(invoke(div,6,"2"),9) 88 | self.assertEquals(invoke(div,6,"2"),7) 89 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 90 | with handlers((ZeroDivisionError,"raise_error",RuntimeError)): 91 | self.assertRaises(MissingRestartError,invoke,div,6,0) 92 | with restarts(raise_error,invoke) as invoke: 93 | self.assertEquals(div(6,3),2) 94 | self.assertEquals(invoke(div,6,3),2) 95 | self.assertEquals(invoke(div,6,"2"),7) 96 | self.assertRaises(RuntimeError,invoke,div,6,0) 97 | self.assertRaises(MissingRestartError,invoke,div,6,0) 98 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 99 | self.assertEquals(invoke(div,6,"2"),7) 100 | 101 | 102 | def test_skip(self): 103 | def calculate(i): 104 | if i == 7: 105 | raise ValueError("7 is not allowed") 106 | return i 107 | def aggregate(items): 108 | total = 0 109 | for i in items: 110 | with restarts(skip,use_value) as invoke: 111 | total += invoke(calculate,i) 112 | return total 113 | self.assertEquals(aggregate(range(6)),sum(range(6))) 114 | self.assertRaises(ValueError,aggregate,range(8)) 115 | with Handler(ValueError,"skip"): 116 | self.assertEquals(aggregate(range(8)),sum(range(8)) - 7) 117 | with Handler(ValueError,"use_value",9): 118 | self.assertEquals(aggregate(range(8)),sum(range(8)) - 7 + 9) 119 | 120 | 121 | def test_raise_error(self): 122 | with Handler(TypeError,"raise_error",ValueError): 123 | with restarts(use_value,raise_error) as invoke: 124 | self.assertEquals(invoke(div,6,3),2) 125 | self.assertRaises(ValueError,invoke,div,6,"2") 126 | with Handler(ValueError,"raise_error",RuntimeError): 127 | with restarts(use_value,raise_error) as invoke: 128 | self.assertEquals(invoke(div,6,3),2) 129 | self.assertRaises(RuntimeError,invoke,div,6,"2") 130 | with Handler(ValueError,"use_value",None): 131 | with restarts(use_value,raise_error) as invoke: 132 | self.assertEquals(invoke(div,6,3),2) 133 | self.assertEquals(invoke(div,6,"2"),None) 134 | 135 | 136 | def test_threading(self): 137 | def calc(a,b): 138 | with restarts(use_value) as invoke: 139 | return invoke(div,a,b) 140 | evt1 = threading.Event() 141 | evt2 = threading.Event() 142 | evt3 = threading.Event() 143 | errors = [] 144 | def thread1(): 145 | try: 146 | self.assertRaises(TypeError,calc,6,"2") 147 | with Handler(TypeError,"use_value",4): 148 | self.assertEquals(calc(6,"2"),4) 149 | evt1.set() 150 | evt2.wait() 151 | self.assertEquals(calc(6,"2"),4) 152 | evt3.set() 153 | self.assertRaises(TypeError,calc,6,"2") 154 | except Exception, e: 155 | evt1.set() 156 | evt3.set() 157 | errors.append(e) 158 | def thread2(): 159 | try: 160 | self.assertRaises(TypeError,calc,6,"2") 161 | evt1.wait() 162 | self.assertRaises(TypeError,calc,6,"2") 163 | def calc2(a,b): 164 | with Restart(raise_error) as invoke: 165 | return invoke(calc,a,b) 166 | self.assertRaises(TypeError,calc2,6,"2") 167 | with Handler(TypeError,"raise_error",ValueError): 168 | self.assertRaises(ValueError,calc2,6,"2") 169 | evt2.set() 170 | evt3.wait() 171 | self.assertRaises(ValueError,calc2,6,"2") 172 | self.assertRaises(TypeError,calc2,6,"2") 173 | self.assertRaises(TypeError,calc,6,"2") 174 | except Exception: 175 | evt2.set() 176 | errors.append(sys.exc_info()) 177 | t1 = threading.Thread(target=thread1) 178 | t2 = threading.Thread(target=thread2) 179 | t1.start() 180 | t2.start() 181 | t1.join() 182 | t2.join() 183 | for e in errors: 184 | raise e[0], e[1], e[2] 185 | 186 | 187 | def test_inline_definitions(self): 188 | with handlers() as h: 189 | @h.add_handler 190 | def TypeError(e): 191 | raise InvokeRestart("my_use_value",7) 192 | with restarts() as invoke: 193 | @invoke.add_restart 194 | def my_use_value(v): 195 | return v 196 | self.assertEquals(div(6,3),2) 197 | self.assertEquals(invoke(div,6,3),2) 198 | self.assertEquals(invoke(div,6,"2"),7) 199 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 200 | @invoke.add_restart(name="my_raise_error") 201 | def my_raise_error_restart(e): 202 | raise e 203 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 204 | @h.add_handler(exc_type=ZeroDivisionError) 205 | def handle_ZDE(e): 206 | raise InvokeRestart("my_raise_error",RuntimeError) 207 | self.assertRaises(RuntimeError,invoke,div,6,0) 208 | invoke.del_restart("my_raise_error") 209 | self.assertRaises(MissingRestartError,invoke,div,6,0) 210 | h.del_handler(handle_ZDE) 211 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 212 | @invoke.add_restart(name="my_raise_error") 213 | def my_raise_error_restart(e): 214 | raise e 215 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 216 | @h.add_handler 217 | def handle_ZeroDivisionError(e): 218 | raise InvokeRestart(my_raise_error_restart,RuntimeError) 219 | self.assertRaises(RuntimeError,invoke,div,6,0) 220 | invoke.del_restart(my_raise_error_restart) 221 | self.assertRaises(MissingRestartError,invoke,div,6,0) 222 | h.del_handler(handle_ZeroDivisionError) 223 | self.assertRaises(ZeroDivisionError,invoke,div,6,0) 224 | 225 | 226 | def test_retry(self): 227 | call_count = {} 228 | def callit(v): 229 | if v not in call_count: 230 | call_count[v] = v 231 | else: 232 | call_count[v] -= 1 233 | if call_count[v] > 0: 234 | raise ValueError("call me again") 235 | return v 236 | self.assertRaises(ValueError,callit,2) 237 | self.assertRaises(ValueError,callit,2) 238 | self.assertEquals(callit(2),2) 239 | errors = [] 240 | with handlers() as h: 241 | @h.add_handler(exc_type=ValueError) 242 | def OnValueError(e): 243 | errors.append(e) 244 | raise InvokeRestart("retry") 245 | with restarts(retry) as invoke: 246 | self.assertEquals(invoke(callit,3),3) 247 | self.assertEquals(len(errors),3) 248 | 249 | 250 | def test_generators(self): 251 | def if_not_seven(i): 252 | if i == 7: 253 | raise ValueError("can't use 7") 254 | return i 255 | def check_items(items): 256 | for i in items: 257 | with restarts(skip,use_value) as invoke: 258 | yield invoke(if_not_seven,i) 259 | self.assertEquals(sum(check_items(range(6))),sum(range(6))) 260 | self.assertRaises(ValueError,sum,check_items(range(8))) 261 | with Handler(ValueError,"skip"): 262 | self.assertEquals(sum(check_items(range(8))),sum(range(8))-7) 263 | with Handler(ValueError,"use_value",2): 264 | self.assertEquals(sum(check_items(range(8))),sum(range(8))-7+2) 265 | # Make sure that the restarts inside the suspended generator 266 | # are not visible outside it. 267 | g = check_items(range(8)) 268 | try: 269 | self.assertEquals(g.next(),0) 270 | self.assertEquals(find_restart("skip"),None) 271 | finally: 272 | g.close() 273 | 274 | 275 | def test_overhead(self): 276 | """Test overhead in comparison to a standard try-except block. 277 | 278 | For compatability with the "timeit" module, the "test_tryexcept" 279 | and "test_restart" functions used by this test are importable from 280 | the separate sub-module "withrestart.tests.overhead". 281 | """ 282 | # If psyco is in use, we need to muck around getting simulated 283 | # frame objects. All performance bets are off. 284 | if "psyco" in sys.modules: 285 | return 286 | def dotimeit(name,args): 287 | testcode = "%s(%s)" % (name,args,) 288 | setupcode = "from withrestart.tests.overhead import %s" % (name,) 289 | t = timeit.Timer(testcode,setupcode) 290 | return min(t.repeat(number=10000)) 291 | def assertOverheadLessThan(scale,args): 292 | t1 = dotimeit("test_tryexcept",args) 293 | t2 = dotimeit("test_restart",args) 294 | print "%.4f / %.4f == %.4f" % (t2,t1,t2/t1) 295 | self.assertTrue(t1*scale > t2) 296 | # Restarts not used 297 | assertOverheadLessThan(20,"4,4") 298 | # Restarts not used 299 | assertOverheadLessThan(20,"100,100") 300 | # Restarts used to return default value 301 | assertOverheadLessThan(27,"7,0") 302 | 303 | def test_callstack(self): 304 | stack = CallStack() 305 | stack.push("hello") 306 | assert list(stack.items()) == ["hello"] 307 | def testsingle(): 308 | stack.push("world") 309 | stack.push("how") 310 | assert list(stack.items()) == ["how","world","hello"] 311 | stack.pop() 312 | assert list(stack.items()) == ["world","hello"] 313 | testsingle() 314 | assert list(stack.items()) == ["hello"] 315 | def testnested(): 316 | stack.push("world") 317 | stack.push("how") 318 | assert list(stack.items()) == ["how","world","hello"] 319 | def subtest(): 320 | stack.push("are") 321 | stack.push("you") 322 | assert list(stack.items())==["you","are","how","world","hello"] 323 | subtest() 324 | assert list(stack.items()) == ["how","world","hello"] 325 | stack.pop() 326 | assert list(stack.items()) == ["world","hello"] 327 | testnested() 328 | assert list(stack.items()) == ["hello"] 329 | def testgenerator(): 330 | stack.push("world") 331 | stack.push("how") 332 | assert list(stack.items()) == ["how","world","hello"] 333 | def gen(): 334 | stack.push("are",1) 335 | stack.push("you") 336 | assert list(stack.items())==["you","are","how","world","hello"] 337 | yield None 338 | assert list(stack.items())==["you","are","how","world","hello"] 339 | stack.pop() 340 | yield None 341 | assert list(stack.items())==["are","how","world","hello"] 342 | g = gen() 343 | assert list(stack.items()) == ["how","world","hello"] 344 | g.next() 345 | assert list(stack.items()) == ["are","how","world","hello"] 346 | g.next() 347 | assert list(stack.items()) == ["are","how","world","hello"] 348 | try: 349 | g.next() 350 | except StopIteration: 351 | pass 352 | stack.pop() 353 | assert list(stack.items()) == ["how","world","hello"] 354 | testgenerator() 355 | assert list(stack.items()) == ["hello"] 356 | 357 | 358 | 359 | def test_README(self): 360 | """Ensure that the README is in sync with the docstring. 361 | 362 | This test should always pass; if the README is out of sync it just 363 | updates it with the contents of withrestart.__doc__. 364 | """ 365 | dirname = os.path.dirname 366 | readme = os.path.join(dirname(dirname(dirname(__file__))),"README.txt") 367 | if not os.path.isfile(readme): 368 | f = open(readme,"wb") 369 | f.write(withrestart.__doc__) 370 | f.close() 371 | else: 372 | f = open(readme,"rb") 373 | if f.read() != withrestart.__doc__: 374 | f.close() 375 | f = open(readme,"wb") 376 | f.write(withrestart.__doc__) 377 | f.close() 378 | 379 | -------------------------------------------------------------------------------- /withrestart/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | withrestart: structured error recovery using named restart functions 4 | 5 | This is a Pythonisation (Lispers might rightly say "bastardisation") of the 6 | restart-based condition system of Common Lisp. It's designed to make error 7 | recovery simpler and easier by removing the assumption that unhandled errors 8 | must be fatal. 9 | 10 | A "restart" represents a named strategy for resuming execution of a function 11 | after the occurrence of an error. At any point during its execution a 12 | function can push a Restart object onto its call stack. If an exception 13 | occurs within the scope of that Restart, code higher-up in the call chain can 14 | invoke it to recover from the error and let the function continue execution. 15 | By providing several restarts, functions can offer several different strategies 16 | for recovering from errors. 17 | 18 | A "handler" represents a higher-level strategy for dealing with the occurrence 19 | of an error. It is conceptually similar to an "except" clause, in that one 20 | establishes a suite of Handler objects to be invoked if an error occurs during 21 | the execution of some code. There is, however, a crucial difference: handlers 22 | are executed without unwinding the call stack. They thus have the opportunity 23 | to take corrective action and then resume execution of whatever function 24 | raised the error. 25 | 26 | For example, consider a function that reads the contents of all files from a 27 | directory into a dict in memory:: 28 | 29 | def readall(dirname): 30 | data = {} 31 | for filename in os.listdir(dirname): 32 | filepath = os.path.join(dirname,filename) 33 | data[filename] = open(filepath).read() 34 | return data 35 | 36 | If one of the files goes missing after the call to os.listdir() then the 37 | subsequent open() will raise an IOError. While we could catch and handle the 38 | error inside this function, what would be the appropriate action? Should 39 | files that go missing be silently ignored? Should they be re-created with 40 | some default contents? Should a special sentinel value be placed in the 41 | data dictionary? What value? The readall() function does not have enough 42 | information to decide on an appropriate recovery strategy. 43 | 44 | Instead, readall() can provide the *infrastructure* for doing error recovery 45 | and leave the final decision up to the calling code. The following definition 46 | uses three pre-defined restarts to let the calling code (a) skip the missing 47 | file completely, (2) retry the call to open() after taking some corrective 48 | action, or (3) use some other value in place of the missing file:: 49 | 50 | def readall(dirname): 51 | data = {} 52 | for filename in os.listdir(dirname): 53 | filepath = os.path.join(dirname,filename) 54 | with restarts(skip,retry,use_value) as invoke: 55 | data[filename] = invoke(open,filepath).read() 56 | return data 57 | 58 | Of note here is the use of the "with" statement to establish a new context 59 | in the scope of restarts, and use of the "invoke" wrapper when calling a 60 | function that might fail. The latter allows restarts to inject an alternate 61 | return value for the failed function. 62 | 63 | Here's how the calling code would look if it wanted to silently skip the 64 | missing file:: 65 | 66 | def concatenate(dirname): 67 | with Handler(IOError,"skip"): 68 | data = readall(dirname) 69 | return "".join(data.itervalues()) 70 | 71 | This pushes a Handler instance into the execution context, which will detect 72 | IOError instances and respond by invoking the "skip" restart point. If this 73 | handler is invoked in response to an IOError, execution of the readall() 74 | function will continue immediately following the "with restarts(...)" block. 75 | 76 | Note that there is no way to achieve this skip-and-continue behaviour using an 77 | ordinary try-except block; by the time the IOError has propagated up to the 78 | concatenate() function for processing, all context from the execution of 79 | readall() will have been unwound and cannot be resumed. 80 | 81 | Calling code that wanted to re-create the missing file would simply push a 82 | different error handler:: 83 | 84 | def concatenate(dirname): 85 | def handle_IOError(e): 86 | open(e.filename,"w").write("MISSING") 87 | raise InvokeRestart("retry") 88 | with Handler(IOError,handle_IOError): 89 | data = readall(dirname) 90 | return "".join(data.itervalues()) 91 | 92 | By raising InvokeRestart, this handler transfers control back to the restart 93 | that was established by the readall() function. This particular restart 94 | will re-execute the failing function call and let readall() continue with its 95 | operation. 96 | 97 | Calling code that wanted to use a special sentinel value would use a handler 98 | to pass the required value to the "use_value" restart:: 99 | 100 | def concatenate(dirname): 101 | class MissingFile: 102 | def read(): 103 | return "MISSING" 104 | def handle_IOError(e): 105 | raise InvokeRestart("use_value",MissingFile()) 106 | with Handler(IOError,handle_IOError): 107 | data = readall(dirname) 108 | return "".join(data.itervalues()) 109 | 110 | 111 | By separating the low-level details of recovering from an error from the 112 | high-level strategy of what action to take, it's possible to create quite 113 | powerful recovery mechanisms. 114 | 115 | While this module provides a handful of pre-built restarts, functions will 116 | usually want to create their own. This can be done by passing a callback 117 | into the Restart object constructor:: 118 | 119 | def readall(dirname): 120 | data = {} 121 | for filename in os.listdir(dirname): 122 | filepath = os.path.join(dirname,filename) 123 | def log_error(): 124 | print "an error occurred" 125 | with Restart(log_error): 126 | data[filename] = open(filepath).read() 127 | return data 128 | 129 | 130 | Or by using a decorator to define restarts inline:: 131 | 132 | def readall(dirname): 133 | data = {} 134 | for filename in os.listdir(dirname): 135 | filepath = os.path.join(dirname,filename) 136 | with restarts() as invoke: 137 | @invoke.add_restart 138 | def log_error(): 139 | print "an error occurred" 140 | data[filename] = open(filepath).read() 141 | return data 142 | 143 | Handlers can also be defined inline using a similar syntax:: 144 | 145 | def concatenate(dirname): 146 | with handlers() as h: 147 | @h.add_handler 148 | def IOError(e): 149 | open(e.filename,"w").write("MISSING") 150 | raise InvokeRestart("retry") 151 | data = readall(dirname) 152 | return "".join(data.itervalues()) 153 | 154 | 155 | Now finally, a disclaimer. I've never written any Common Lisp. I've only read 156 | about the Common Lisp condition system and how awesome it is. I'm sure there 157 | are many things that it can do that this module simply cannot. For example: 158 | 159 | * Since this is built on top of a standard exception-throwing system, the 160 | handlers can only be executed after the stack has been unwound to the 161 | most recent restart context; in Common Lisp they're executed without 162 | unwinding the stack at all. 163 | * Since this is built on top of a standard exception-throwing system, it's 164 | probably too heavyweight to use for generic condition signalling system. 165 | 166 | Nevertheless, there's no shame in pinching a good idea when you see one... 167 | 168 | """ 169 | 170 | __ver_major__ = 0 171 | __ver_minor__ = 2 172 | __ver_patch__ = 7 173 | __ver_sub__ = "" 174 | __version__ = "%d.%d.%d%s" % (__ver_major__,__ver_minor__, 175 | __ver_patch__,__ver_sub__) 176 | 177 | 178 | import sys 179 | 180 | from withrestart.callstack import CallStack 181 | _cur_restarts = CallStack() # per-frame active restarts 182 | _cur_handlers = CallStack() # per-frame active handlers 183 | 184 | 185 | class RestartError(Exception): 186 | """Base class for all user-visible exceptions raised by this module.""" 187 | pass 188 | 189 | 190 | class ControlFlowException(Exception): 191 | """Base class for all control-flow exceptions used by this module.""" 192 | pass 193 | 194 | 195 | class MissingRestartError(RestartError): 196 | """Exception raised when invoking a non-existent restart.""" 197 | def __init__(self,name): 198 | self.name = name 199 | def __str__(self): 200 | return "No restart named '%s' has been defined" % (self.name,) 201 | 202 | 203 | class InvokeRestart(ControlFlowException): 204 | """Exception raised by handlers to invoke a selected restart. 205 | 206 | This is used as a flow-control mechanism and should never be seen by 207 | code outside this module. It's purposely not a sublcass of RestartError; 208 | you really shouldn't be catching it except under special circumstances. 209 | """ 210 | def __init__(self,restart,*args,**kwds): 211 | if not isinstance(restart,Restart): 212 | name = restart; restart = find_restart(name) 213 | if restart is None: 214 | raise MissingRestartError(name) 215 | self.restart = restart 216 | self.args = args 217 | self.kwds = kwds 218 | 219 | def invoke(self): 220 | """Convenience methods for invoking the selected restart.""" 221 | return self.restart.invoke(*self.args,**self.kwds) 222 | 223 | def __str__(self): 224 | return "" % (self.restart.name,self.args,self.kwds) 225 | 226 | 227 | class ExitRestart(ControlFlowException): 228 | """Exception raised by restarts to immediately exit their context. 229 | 230 | Restarts can raise ExitRestart to immediately transfer control to the 231 | end of their execution context. It's primarily used by the pre-defined 232 | "skip" restart but others are welcome to use it for similar purposes. 233 | 234 | This is used as a flow-control mechanism and should never be seen by 235 | code outside this module. It's purposely not a sublcass of RestartError; 236 | you really shouldn't be catching it except under special circumstances. 237 | """ 238 | def __init__(self,restart=None): 239 | self.restart = restart 240 | 241 | 242 | class RetryLastCall(ControlFlowException): 243 | """Exception raised by restarts to re-execute the last function call. 244 | 245 | Restarts can raise RetryLastCall to immediately re-execute the last 246 | function invoked within a restart context. It's primarily used by 247 | the pre-defined "retry" restart but others are welcome to use it for 248 | similar purposes. 249 | 250 | This is used as a flow-control mechanism and should never be seen by 251 | code outside this module. It's purposely not a sublcass of RestartError; 252 | you really shouldn't be catching it except under special circumstances. 253 | """ 254 | pass 255 | 256 | 257 | class RaiseNewError(ControlFlowException): 258 | """Exception raised by restarts to re-raise a different error. 259 | 260 | Restarts can raise RaiseNewErorr to re-start the error handling machinery 261 | using a new exception. This is different to simply raising an exception 262 | inside the restart function - RaiseNewError causes the new exception to 263 | be pushed back through the error-handling machinery and potentially handled 264 | by the same restart context. 265 | 266 | This is used as a flow-control mechanism and should never be seen by 267 | code outside this module. It's purposely not a sublcass of RestartError; 268 | you really shouldn't be catching it except under special circumstances. 269 | """ 270 | 271 | def __init__(self,error): 272 | self.error = error 273 | 274 | 275 | 276 | class Restart(object): 277 | """Restart marker object. 278 | 279 | Instances of Restart represent named strategies for resuming execution 280 | after the occurrence of an error. Collections of Restart objects are 281 | pushed onto the execution context where code can cleanly restart after 282 | the occurrence of an error, but requires information from outside the 283 | function in order to do so. 284 | 285 | When an individual Restat object is used as a context manager, it will 286 | automatically wrap itself in a RestartSuite object. 287 | """ 288 | 289 | def __init__(self,func,name=None): 290 | """Restart object initializer. 291 | 292 | A Restart must be initialized with a callback function to execute 293 | when the restart is invoked. If the optional argument 'name' is 294 | given this becomes the name of the Restart; otherwise its name is 295 | taken from the callback function. 296 | """ 297 | self.func = func 298 | if name is None: 299 | self.name = func.func_name 300 | else: 301 | self.name = name 302 | 303 | def invoke(self,*args,**kwds): 304 | """Invoke this restart with the given arguments. 305 | 306 | This wrapper method also maintains some internal state for use by 307 | the restart-handling machinery. 308 | """ 309 | try: 310 | return self.func(*args,**kwds) 311 | except ExitRestart, e: 312 | e.restart = self 313 | raise 314 | 315 | def __enter__(self): 316 | suite = RestartSuite(self) 317 | _cur_restarts.push(suite,1) 318 | return suite 319 | 320 | def __exit__(self,exc_type,exc_value,traceback): 321 | _cur_restarts.items().next().__exit__(exc_type,exc_value,traceback) 322 | 323 | 324 | class RestartSuite(object): 325 | """Class holding a suite of restarts belonging to a common context. 326 | 327 | The RestartSuite class is used to bundle individual Restart objects 328 | into a set that is pushed/popped together. It's also possible to 329 | add and remove individual restarts from a suite dynamically, allowing 330 | them to be defined inline using decorator syntax. 331 | 332 | If the attribute "default_handlers" is set to a Handler or HandlerSuite 333 | instance, that instance will be invoked if no other handler has been 334 | established for the current exception type. 335 | """ 336 | 337 | def __init__(self,*restarts): 338 | self.restarts = [] 339 | self.default_handlers = None 340 | for r in restarts: 341 | if isinstance(r,RestartSuite): 342 | for r2 in r.restarts: 343 | self.restarts.append(r2) 344 | elif isinstance(r,Restart): 345 | self.restarts.append(r) 346 | else: 347 | self.restarts.append(Restart(r)) 348 | 349 | def add_restart(self,func=None,name=None): 350 | """Add the given function as a restart to this suite. 351 | 352 | If the 'name' keyword argument is given, that will be used instead 353 | of the name of the function. The following are all equivalent: 354 | 355 | def my_restart(): 356 | pass 357 | r.add_restart(Restart(my_restart,"skipit")) 358 | 359 | @r.add_restart(name="skipit") 360 | def my_restart(): 361 | pass 362 | 363 | @r.add_restart 364 | def skipit(): 365 | pass 366 | 367 | """ 368 | def do_add_restart(func): 369 | if isinstance(func,Restart): 370 | r = func 371 | else: 372 | r = Restart(func,name) 373 | self.restarts.append(r) 374 | return func 375 | if func is None: 376 | return do_add_restart 377 | else: 378 | return do_add_restart(func) 379 | 380 | def del_restart(self,restart): 381 | """Remove the given restart from this suite. 382 | 383 | The restart can be specified as a Restart instance, function or name. 384 | """ 385 | to_del = [] 386 | for r in self.restarts: 387 | if r is restart or r.func is restart or r.name == restart: 388 | to_del.append(r) 389 | for r in to_del: 390 | self.restarts.remove(r) 391 | 392 | def __call__(self,func,*args,**kwds): 393 | """Invoke the given function in the context of this restart suite. 394 | 395 | If a restart is invoked in response to an error, its return value 396 | is used in place of the function call. 397 | """ 398 | exc_type, exc_value, traceback = None, None, None 399 | try: 400 | return func(*args,**kwds) 401 | except InvokeRestart, e: 402 | if e.restart in self.restarts: 403 | try: 404 | return e.invoke() 405 | except RetryLastCall: 406 | return self(func,*args,**kwds) 407 | except RaiseNewError, newerr: 408 | exc_info = self._normalise_error(newerr.error) 409 | exc_type, exc_value = exc_info[:2] 410 | if exc_info[2] is not None: 411 | traceback = exc_info[2] 412 | else: 413 | raise 414 | except Exception: 415 | exc_type, exc_value, traceback = sys.exc_info() 416 | while exc_value is not None: 417 | try: 418 | self._invoke_handlers(exc_value) 419 | except InvokeRestart, e: 420 | if e.restart in self.restarts: 421 | try: 422 | return e.invoke() 423 | except RetryLastCall: 424 | return self(func,*args,**kwds) 425 | except RaiseNewError, newerr: 426 | exc_info = self._normalise_error(newerr.error) 427 | exc_type, exc_value = exc_info[:2] 428 | if exc_info[2] is not None: 429 | traceback = exc_info[2] 430 | else: 431 | raise 432 | else: 433 | raise exc_type, exc_value, traceback 434 | 435 | def _normalise_error(self,error): 436 | exc_type, exc_value, traceback = None, None, None 437 | if isinstance(error,BaseException): 438 | exc_type = type(error) 439 | exc_value = error 440 | elif isinstance(error,type): 441 | exc_type = error 442 | exc_value = error() 443 | else: 444 | values = tuple(error) 445 | if len(values) == 1: 446 | exc_type = values[0] 447 | exc_info = exc_type() 448 | elif len(values) == 2: 449 | exc_type, exc_info = values 450 | elif len(values) == 3: 451 | exc_type, exc_value, traceback = values 452 | else: 453 | raise ValueError("too many items in exception tuple") 454 | return exc_type, exc_value, traceback 455 | 456 | def __enter__(self): 457 | _cur_restarts.push(self,1) 458 | return self 459 | 460 | def __exit__(self,exc_type,exc_value,traceback,internal=False): 461 | try: 462 | if exc_type is not None: 463 | if exc_type is InvokeRestart: 464 | for r in self.restarts: 465 | if exc_value.restart is r: 466 | return self._invoke_restart(exc_value) 467 | else: 468 | return False 469 | elif exc_type is ExitRestart: 470 | for r in self.restarts: 471 | if exc_value.restart is r: 472 | return True 473 | else: 474 | return False 475 | else: 476 | try: 477 | self._invoke_handlers(exc_value) 478 | except InvokeRestart, e: 479 | for r in self.restarts: 480 | if e.restart is r: 481 | return self._invoke_restart(e) 482 | else: 483 | raise 484 | else: 485 | return False 486 | finally: 487 | if not internal: 488 | _cur_restarts.pop() 489 | 490 | def _invoke_restart(self,r): 491 | try: 492 | r.invoke() 493 | except ExitRestart, e: 494 | if e.restart not in self.restarts: 495 | raise 496 | except RetryLastCall: 497 | return False 498 | except RaiseNewError, e: 499 | exc_type, exc_value, traceback = self._normalise_error(e.error) 500 | return self.__exit__(exc_type,exc_value,traceback,internal=True) 501 | return True 502 | 503 | def _invoke_handlers(self,e): 504 | handlers = find_handlers(e) 505 | if handlers: 506 | for handler in handlers: 507 | handler.handle_error(e) 508 | else: 509 | if self.default_handlers is not None: 510 | if isinstance(e,self.default_handlers.exc_type): 511 | self.default_handlers.handle_error(e) 512 | 513 | # Convenience name for accessing RestartSuite class. 514 | restarts = RestartSuite 515 | 516 | 517 | def find_restart(name): 518 | """Find a defined restart with the given name. 519 | 520 | If no such restart is found then None is returned. 521 | """ 522 | for suite in _cur_restarts.items(): 523 | for restart in suite.restarts: 524 | if restart.name == name or restart.func == name: 525 | return restart 526 | return None 527 | 528 | 529 | 530 | def invoke(func,*args,**kwds): 531 | """Invoke the given function, or return a value from a restart. 532 | 533 | This function can be used to invoke a function or callable object within 534 | the current restart context. If the function runs to completion its 535 | result is returned. If an error occurrs, the handlers are executed and 536 | the result from any invoked restart becomes the return value of the 537 | function call. 538 | """ 539 | try: 540 | return func(*args,**kwds) 541 | except Exception, err: 542 | try: 543 | for handler in find_handlers(err): 544 | handler.handle_error(err) 545 | except InvokeRestart, e: 546 | try: 547 | return e.invoke() 548 | except RetryLastCall: 549 | return invoke(func,*args,**kwds) 550 | else: 551 | raise 552 | 553 | 554 | class Handler(object): 555 | """Restart handler object. 556 | 557 | Instances of Handler represent high-level control strategies for dealing 558 | with errors that have occurred. They can be thought of as an "except" 559 | clause the executes at the site of the error instead of unwinding the 560 | stack. Handlers push themselves onto the execution context when entered 561 | and pop themselves when exited. They will not swallow errors, but 562 | can cause errors to be swallowed at a lower level of the callstack by 563 | explicitly invoking a restart. 564 | """ 565 | 566 | def __init__(self,exc_type,func,*args,**kwds): 567 | """Handler object initializer. 568 | 569 | Handlers must be initialized with an exception type (or tuple of 570 | types) and a function to be executed when such errors occur. If 571 | the given function is a string, it names a restart that will be 572 | invoked immediately on error. 573 | 574 | Any additional args or kwargs will be passed into the handler 575 | function when it is executed. 576 | """ 577 | self.exc_type = exc_type 578 | self.func = func 579 | self.args = args 580 | self.kwds = kwds 581 | 582 | def handle_error(self,e): 583 | """Invoke this handler on the given error. 584 | 585 | This is a simple wrapper method to implement the shortcut syntax of 586 | passing the name of a restart directly into the handler. 587 | """ 588 | if isinstance(self.func,basestring): 589 | raise InvokeRestart(self.func,*self.args,**self.kwds) 590 | else: 591 | self.func(e,*self.args,**self.kwds) 592 | 593 | def __enter__(self): 594 | _cur_handlers.push(self,1) 595 | return self 596 | 597 | def __exit__(self,exc_type,exc_value,traceback): 598 | _cur_handlers.pop() 599 | 600 | 601 | class HandlerSuite(object): 602 | """Class to easily combine multiple handlers into a single context. 603 | 604 | HandleSuite objects represent a set of Handlers that are pushed/popped 605 | as a group. The suite can also have handlers dynamically added or removed, 606 | allowing then to be defined in-line using decorator syntax. 607 | """ 608 | 609 | def __init__(self,*handlers): 610 | self.handlers = [] 611 | self.exc_type = () 612 | for h in handlers: 613 | if isinstance(h,(Handler,HandlerSuite,)): 614 | self._add_handler(h) 615 | else: 616 | self._add_handler(Handler(*h)) 617 | 618 | def handle_error(self,e): 619 | for handler in self.handlers: 620 | if isinstance(e,handler.exc_type): 621 | handler.handle_error(e) 622 | 623 | def __enter__(self): 624 | _cur_handlers.push(self,1) 625 | return self 626 | 627 | def __exit__(self,exc_type,exc_info,traceback): 628 | _cur_handlers.pop() 629 | 630 | def add_handler(self,func=None,exc_type=None): 631 | """Add the given function as a handler to this suite. 632 | 633 | If the given function is already a Handler or HandlerSuite object, 634 | it is used directory. Otherwise, if the exc_type keyword argument 635 | is given, a Handler is created for that exception type. Finally, 636 | if exc_type if not specified then is is looked up using the name of 637 | the given function. Thus the following are all equivalent: 638 | 639 | def handle_IOError(e): 640 | pass 641 | h.add_handler(Handler(IOError,handle_IOError)) 642 | 643 | @h.add_handler(exc_type=IOError): 644 | def handle_IOError(e): 645 | pass 646 | 647 | @h.add_handler 648 | def IOError(e): 649 | pass 650 | 651 | """ 652 | def do_add_handler(func): 653 | if isinstance(func,(Handler,HandlerSuite,)): 654 | h = func 655 | else: 656 | if exc_type is not None: 657 | h = Handler(exc_type,func) 658 | else: 659 | exc_name = func.func_name 660 | try: 661 | exc = _load_name_in_scope(func,exc_name) 662 | except NameError: 663 | if exc_name.startswith("handle_"): 664 | exc_name = exc_name[7:] 665 | exc = _load_name_in_scope(func,exc_name) 666 | else: 667 | raise 668 | h = Handler(exc,func) 669 | self._add_handler(h) 670 | return func 671 | if func is None: 672 | return do_add_handler 673 | else: 674 | return do_add_handler(func) 675 | 676 | def _add_handler(self,handler): 677 | """Internal logic for adding a handler to the suite. 678 | 679 | This appends the handler to self.handlers, and adjusts self.exc_type 680 | to reflect the newly-handled exception types. 681 | """ 682 | self.handlers.append(handler) 683 | if isinstance(handler.exc_type,tuple): 684 | self.exc_type = self.exc_type + handler.exc_type 685 | else: 686 | self.exc_type = self.exc_type + (handler.exc_type,) 687 | 688 | def del_handler(self,handler): 689 | """Remove any handlers matching the given value from the suite. 690 | 691 | The 'handler' argument can be a Handler instance, function or 692 | exception type. 693 | """ 694 | to_del = [] 695 | for h in self.handlers: 696 | if h is handler or h.func is handler or h.exc_type is handler: 697 | to_del.append(h) 698 | for h in to_del: 699 | self.handlers.remove(h) 700 | 701 | # Convenience name for accessing HandlerSuite class. 702 | handlers = HandlerSuite 703 | 704 | 705 | def find_handlers(err): 706 | """Find the currently-established handlers for the given error. 707 | 708 | This function returns a list of all handlers currently established for 709 | the given error, in the order in which they should be invoked. 710 | """ 711 | handlers = [] 712 | for handler in _cur_handlers.items(): 713 | if isinstance(err,handler.exc_type): 714 | handlers.append(handler) 715 | return handlers 716 | 717 | 718 | def skip(): 719 | """Pre-defined restart that skips to the end of the restart context.""" 720 | raise ExitRestart 721 | 722 | def retry(): 723 | """Pre-defined restart that retries the most-recently-invoked function.""" 724 | raise RetryLastCall 725 | 726 | def raise_error(error): 727 | """Pre-defined restart that raises the given error.""" 728 | raise RaiseNewError(error) 729 | 730 | def use_value(value): 731 | """Pre-defined restart that returns the given value.""" 732 | return value 733 | 734 | 735 | def _load_name_in_scope(func,name): 736 | """Get the value of variable 'name' as seen in scope of given function. 737 | 738 | If no such variable is found in the function's scope, NameError is raised. 739 | """ 740 | try: 741 | try: 742 | idx = func.func_code.co_cellvars.index(name) 743 | except ValueError: 744 | try: 745 | idx = func.func_code.co_freevars.index(name) 746 | idx -= len(func.func_code.co_cellvars) 747 | except ValueError: 748 | raise NameError(name) 749 | return func.func_closure[idx].cell_contents 750 | except NameError: 751 | try: 752 | try: 753 | return func.func_globals[name] 754 | except KeyError: 755 | return __builtins__[name] 756 | except KeyError: 757 | raise NameError(name) 758 | 759 | --------------------------------------------------------------------------------