Skip to main content

Labeling failures for humans

Project description

Failures

Labeling failures for humans


Tests CodeCov PyPI - Python Version Read the Docs License GitHub issues PyPI - Downloads

What is failures

Failures is a python3 package that provides utilities to explicitly label nested failures (errors) with simple syntax, and gives better flexibility on what errors to handle and how to handle them.

Installation

failures is available at PyPI, and requires python 3.8 or higher, install it by executing the pip command:

pip install failures

Note This package comes with a default handler that prints failures to the standard output, if you want them to be highlighted with colors, you can install pip install "failures[colors]"

Usage

Failures scope (context)

This example will showcase the use of failure.scope to create a labeled failures scope and handle them in the outermost Scope layer.

Let's create a file example.py

import math
import failures


def data_num_inverse_sqrt(data: dict) -> float:
    with failures.scope("number", show_failure):
        with failures.scope("extract"):
            num = data["num"]  # fails if 'num' not in data or data is not a dict (1)
        with failures.scope("convert"):
            num = int(num)  # fails if num is not a sequence of digits (2)
        with failures.scope("evaluate"):
            return inverse_sqrt(num)


def inverse_sqrt(num: int) -> float:
    # independent (decoupled) function
    with failures.scope("inverse"):
        num = 1 / num  # fails if num == 0 (3)
    with failures.scope("square_root"):
        return round(math.sqrt(num), 2)  # fails if num < 0 (4)


def show_failure(label: str, error: Exception) -> None:
    # This function will serve as a failure handler
    # and will simply print the source and the error
    print(f"[{label}] {error!r}")

The code is self-explanatory, data_num_inverse_sqrt function is the outer layer function that expects exactly a dict (or a map-like object) that has a non-null positive number exactly with the key 'num', this function also uses an independent function inverse_sqrt that might also fail, and finally we implement our own simple function for handling failures show_failure.

>>> from example import data_num_inverse_sqrt
>>> data_num_inverse_sqrt(5)  # (1)
[number.extract] TypeError("'int' object is not subscriptable")

>>> data_num_inverse_sqrt({'my_num': 5})  # (1)
[number.extract] KeyError('num')

>>> data_num_inverse_sqrt({"num": "two"})  # (2)
[number.convert] ValueError("invalid literal for int() with base 10: 'two'")

>>> data_num_inverse_sqrt({"num": "0"})  # (3)
[number.evaluate.inverse] ZeroDivisionError('division by zero')

>>> data_num_inverse_sqrt({"num": "-5"})  # (4)
[number.evaluate.square_root] ValueError('math domain error')

>>> data_num_inverse_sqrt({"num": "4"})  # No errors
0.5

>>> data_num_inverse_sqrt({"num": 4}) # same, no errors
0.5

Each scoped context captures any raised exception withing its scope and tag it with its label, then either handles it if a handler function was passed to it or re-raise it to the outer layer scope, each failures' scope captures a tagged and prepends its label until reaching the top level one.

Failures scoped (decorator)

The failures.scoped decorator is used to wrap a function within a labeled scope to reduce indentation and redundancy.

Sot this function

def number_processing(number):
    with failures.scope("number_processing"):   # label same as function name
        with failures.scope("step1"):
            ...
        with failures.scope("step2"):
            ...

can be replaced by

@failures.scoped
def number_processing(number):
    with failures.scope("step1"):
        ...
    with failures.scope("step2"):
        ...

The decorator can also take optional parameters to customize the Scope object wrapping the function

def number_processing(number):
    with failures.scope("processing", show_failure):
        with failures.scope("step1"):
            ...
        with failures.scope("step2"):
            ...

can be replaced by

@failures.scoped(name="processing", handler=show_failure)
def number_processing(number):
    with failures.scope("step1"):
        ...
    with failures.scope("step2"):
        ...

Failures handler

The failures.handler utility creates a custom failure handler with additional filters, this function takes one required positional-only argument for the actual failure handling function, and two optional keyword-only argument, ignore to mark failures that should be ignored (suppressed), and propagate to mark failures that shouldn't be captured (unhandled).

ignore and propagate can take an exception type (like ValueError), or a tuple of exception types.

>>> from failures import scope, handler
>>> from example import show_failure
>>> def my_function(error: Exception):
...     with scope('test_handler', handler(show_failure, ignore=KeyError, propagate=TypeError)):
...         raise error
...

>>> my_function(ValueError("value error"))  # Handled
[test_handler] ValueError('value error')
>>> my_function(TypeError("value error"))  # Unhandled
Traceback (most recent call last):
    ...
failures.core.Failure: ('test_handler', TypeError('value error'))
>>> my_function(TypeError("type error"))  # Ignored
>>>

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

failures-0.2.0.tar.gz (17.1 kB view hashes)

Uploaded Source

Built Distribution

failures-0.2.0-py3-none-any.whl (10.5 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page