Skip to content

pomponchik/escaping

Repository files navigation

logo

Downloads Downloads codecov Lines of code Hits-of-Code Test-Package Python versions PyPI version Checked with mypy Ruff

If you've just confessed and you can't wait to sin again, try this package. It will help you hide your mistakes and make your life more carefree :) Seriously, the library allows you to solve the problem of exception handling in a more adult way by providing:

  • 🛡️ A universal interface for the decorator and context manager.
  • 🛡️ Built-in logging.
  • 🛡️ Calling callbacks.

Table of contents

Quick start

Install it:

pip install escaping

And use:

import escape

@escape
def function():
    raise ValueError

function()  # The exception is suppressed.

Read about other library features below.

About

This project is dedicated to the most important problem in programming - how do we need to handle errors? Here are some answers to this question that it gives:

  • This should be done in a standardized way. You can decide for yourself how errors will be handled, but with this project, any method you choose can easily become the standard.

  • Mistakes should not be hidden. Even if the exception is suppressed, you should be aware of it.

An interesting solution that is proposed here is that you are provided with a single interface for error suppression, which can be used as a context manager for any block of code, as well as as a decorator for ordinary, coroutine and generator functions. Wherever you need to suppress an error, you do it the same way, according to the same rules:

import escape

@escape
def function():
    ...

@escape
async def function():
    ...

@escape
def function():
    yield something
    ...

with escape:
    ...

The rules by which you want to suppress errors can be "baked" into a special object so that you don't duplicate it in different parts of the code later. This means that you can come up with error suppression rules once, and then use them everywhere, without duplicating code, which is assumed when using ordinary try-except blocks.

Decorator mode

The @escape decorator suppresses exceptions in a wrapped function (including generator and coroutine ones), which are passed in parentheses. In this way, you can pass any number of exceptions, for example:

import asyncio
import escape

@escape(ValueError, ZeroDivisionError)
def function():
    raise ValueError('oh!')

@escape(ValueError, ZeroDivisionError)
async def async_function():
    raise ZeroDivisionError('oh!')

function()  # Silence.
asyncio.run(async_function())  # Silence.

If you use @escape with parentheses but do not pass any exception types, no exceptions will be suppressed:

@escape()
def function():
    raise ValueError('oh!')

function()
# > ValueError: oh!

If an exception occurred inside the function wrapped by the decorator, it will return the default value - None. You can specify your own default value:

@escape(ValueError, default='some value')
def function():
    raise ValueError

assert function() == 'some value'  # It's going to work.

Finally, you can use @escape as a decorator without parentheses.

@escape
def function():
    raise ValueError

function()  # Silence still.

In this mode, not all exceptions from the hierarchy are suppressed, but only those that can be expected in the user code. Exception and all its descendants are suppressed, as well as, starting with Python 3.11, groups of exceptions. However, exceptions GeneratorExit, KeyboardInterrupt and SystemExit are not escaped in this mode. This is due to the fact that in most programs none of them is part of the semantics of the program, but is used exclusively for system needs. For example, if KeyboardInterrupt was blocked, you would not be able to stop your program using the Control-C keyboard shortcut.

You can also use the same set of exceptions in parenthesis mode as without parentheses. To do this, use the Ellipsis (three dots):

@escape(...)
def function_1():
    raise ValueError

@escape
def function_2():
    raise ValueError

function_1()  # These two functions are completely equivalent.
function_2()  # These two functions are completely equivalent.

Ellipsis can also be used in enumeration, along with other exceptions:

@escape(GeneratorExit, ...)

Context manager mode

You can use escape as a context manager, which escapes exceptions in the code block wrapped by it. You can call it according to the same rules as the decorator - pass exceptions or ellipsis there. It also works almost the same way as contextlib.suppress from the standard library, but with a bit more opportunities. Some examples:

with escape(ValueError):
    raise ValueError

with escape:
    raise ValueError

with escape(...):
    raise ValueError

However, as you should understand, the default value cannot be specified in this case. If you try to specify a default value for the context manager, get ready to face an exception:

with escape(default='some value'):
    ...

# > escape.errors.SetDefaultReturnValueForContextManagerError: You cannot set a default value for the context manager. This is only possible for the decorator.

Logging

You can pass a logger object to the escape. In such case, if an exception is raised inside the context or the function wrapped by the decorator, it will be logged:

import logging
import escape

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.StreamHandler(),
    ]
)

logger = logging.getLogger('logger_name')

with escape(..., logger=logger):
    1/0

# You will see a description of the error in the console.

It works in any mode: both in the case of the context manager and the decorator.

By default only exceptions are logged. If the code block or function was executed without errors, the log will not be recorded. Also the log is recorded regardless of whether the exception was suppressed or not. However, depending on this, you will see different log messages to distinguish one situation from another.

But! You can change the standard logging behavior.

If you want the log to be recorded for any outcome, including the one where no errors occurred, specify the success_logging=True flag (messages will be recorded with the info level):

with escape(success_logging=True, logger=logger):
    pass
    # > The code block was executed successfully.

In addition, you can change the standard messages that you see in the logs. Keep in mind that this feature narrows down the variety of standard messages, which differ depending on where the error occurred (in a regular function, in a generator or asynchronous function, or perhaps in a block of code wrapped by a context manager), or whether the error was intercepted. You can define your own messages for only two types of situations: when the code was executed without exceptions, and when with an exception.

Pass your message as error_log_message if you want to see it when an error occurred inside the code:

with escape(..., error_log_message='Oh my God!', logger=logger):
    raise ValueError
    # > Oh my God!

By analogy, pass success_log_message as a message if there are no errors in the code block (but don't forget to set success_logging=True!):

with escape(success_log_message='Good news, everyone!', success_logging=True, logger=logger):
    pass
    # > Good news, everyone!

In addition, if the exception was suppressed inside the escape, the log will be recorded using the exception method - this means that the trace will be saved. Otherwise, the error method will be used - without saving the traceback, because otherwise, if you catch this exception somewhere else and pledge the traceback, there will be several duplicate tracebacks in your log file.

Callbacks

You can pass callback functions to escape, which will be automatically called when the wrapped code block or function has completed.

A callback passed as success_callback will be called when the code is executed without errors:

with escape(success_callback=lambda: print('The code block ended without errors.')):
    ...

By analogy, if you pass error_callback, this function will be called when an exception is raised inside:

with escape(error_callback=lambda: print('Attention!')):
    ...

If you pass a callback as a before parameter, it'll be called before the code block anyway:

with escape(before=lambda: print('Something is going to happen now...')):
    ...

Notice, if an error occurs in this callback that will not be suppressed, the main code will not be executed - an exception will be raised before it starts executing.

If an error occurs in one of the callbacks, the exception will be suppressed if it would have been suppressed if it had happened in a wrapped code block or function. You can see the corresponding log entry about this if you pass the logger object for registration. If the error inside the callback has been suppressed, it will not affect the logic that was wrapped by escape in any way.

Baking rules

You can set up an error escaping policy once and then reuse it in different situations. To do this, get a special object through the bake method:

escaper = escape.bake(ValueError)

Creating this object, you can pass all the same arguments as when using escape directly as a decorator or a context manager: exceptions, callbacks, or a logger. The object "remembers" these arguments until the moment you decide to use it:

with escaper:
    raise ValueError  # It will be suppressed.
@escaper
def function():
    raise ValueError  # It will be suppressed too.

function()

If necessary, you can combine "baked" arguments and arguments that are passed on demand (executing the sample code requires pre-installation of the emptylog library):

import escape
from emptylog import PrintingLogger

escaper = escape.bake(logger=PrintingLogger())

@escaper(ValueError)
def function():
    raise ValueError  # It will be suppressed too.

function()
#> 2024-09-06 14:45:19.606267 | EXCEPTION | When executing function "function", the exception "ValueError" was suppressed.

In this way, you can add additional exceptions that need to be suppressed - they will be added to the general list of suppressed ones. In addition, you can override some of the named arguments that are "baked" into the object on demand - in this case, the argument that was passed later will be used.

Arguments baking is an extremely powerful tool, useful for large programs. It allows you to get rid of multiple duplications of code that are often encountered during error handling. In addition, with its help, you can describe the error handling policy centrally, in one place for the entire program, which makes maintaining or changing the program a much easier task.