Decorator based error handling for Python
Merry is installed with pip:
$ pip install merry
The purpose of merry is to help you move error handling code away from your application logic. Take, for example, this function, which embeds error handling code:
def write_to_file(filename, data):
try:
with open(filename, 'w') as f:
f.write(data)
except IOError:
print('Error: can't write to file')
except Exception as e:
print('Unexpected error: ' + str(e))
write_to_file('some_file', 'some_data')
Even with this simple example, you can see how the indentation forced by the try/except block makes the code much harder to read and visually follow.
Merry allows you to move exception handlers to external functions, so that they don't interfere with the application logic:
from merry import Merry
merry = Merry()
@merry._try
def write_to_file(filename, data):
with open(filename, 'w') as f:
f.write(data)
@merry._except(IOError)
def ioerror():
print('Error: can't write to file')
@merry._except(Exception)
def catch_all(e):
print('Unexpected error: ' + str(e)
write_to_file('some_file', 'some_data')
While in this example there are more lines of code after merry is used, the
key benefit is that the application logic, which is in the write_to_file
function, is now completely clean of try/except statements. The exception
handlers become auxiliary functions that can even be moved to a separate
module so that they stay completely out of the way.
The decorated exception handlers can optionally take one argument. If you include this argument, then merry sends the exception object.
@merry._except(Exception)
def catch_all(e):
print('Unexpected error: ' + str(e)
For cases that require a more complex try/except block, there are also
decorators available for else
and finally
:
@merry._else
def else_clause():
print('No exceptions where raised!')
@merry._finally
def finally_clause():
print('Clean up time!')
Returning values from functions protected with the try
decorator or the
corresponding except
, else
and finally
handlers follow certain rules
that try to implement a behavior similar to a Python try/except:
- The value returned by a function decorated with the
try
decorator is normally returned to the caller. - If there is an exception, then the value returned by the
except
handler that matches the exception is returned to the caller. - The
else
handler only runs if thetry
function does not raise an exception and returnsNone
- If the
try
function returnsNone
and there is anelse
handler, then its return value is given to the caller. - If there is a
finally
handler and it returns a value that is notNone
, then this value takes the place of any other returned value.
In many cases, exception handlers need to have access to application state to
do their work. When using merry, the merry.g
object can be used as storage
of application state that needs to be accessible to error handlers:
@merry._try
def app_logic():
db = open_database()
merry.g.database = db # save it in the error context just in case
# do database stuff here
@merry._except(Exception)
def catch_all():
db = getattr(merry.g, 'database', None)
if db is not None and is_database_open(db):
close_database(db)
print('Unexpected error, quitting')
sys.exit(1)
When working with debuggers, it is a good idea to let all exceptions reach the top of the stack, so that the debugger handles them. With merry, if you enable debug mode all exceptions bubble all the way up:
merry = Merry(debug=True)
But when working in debug mode, there might be certain exceptions that are expected to trigger and do not need to bubble up. For this reason, the debug mode can be overriden by individual error handlers:
@merry._except(IOError, debug=False)
def ioerror():
# this function will run even in debug mode
print('Error: can't write to file')
The reverse is also possible. If you are running with debug mode turned off,
but want to suspend an exception handler and have that exception bubble up,
just set debug=True
for that handler.
When an application is running in production mode, it is desired that all errors are suppressed and instead they are sent to a log. Merry creates a logger on which it writes all the exceptions it handles, include their backtraces. This logger is a standard instance of the Python standard library logging class.
The default logger instance is called 'merry'
, and can be referenced as
merry.logger
. If desired, merry can hook up to a logger object owned by the
application:
custom_logger = logging.getLogger('my_logger')
custom_logger.setLevel(logging.INFO)
merry = Merry(logger_name='my_logger')
By default, the logger created by merry does not have any handlers attached, so caught exceptions will not be logged anywhere. If you want exceptions to be written to the console, you can add a handler that writes to stderr:
merry = Merry()
merry.logger.addHandler(logging.StreamHandler(sys.stderr))
The log level and format can be adjusted as well. See the documentation on the logging module for more information on how to do this.