Error Handling

The code in this notebook helps with handling errors. Normally, an error in notebook code causes the execution of the code to stop; while an infinite loop in notebook code causes the notebook to run without end. This notebook provides two classes to help address these concerns.

Prerequisites

  • This notebook needs some understanding on advanced concepts in Python, notably
    • classes
    • the Python with statement
    • tracing
    • measuring time
    • exceptions

Synopsis

To use the code provided in this chapter, write

>>> from fuzzingbook.ExpectError import <identifier>

and then make use of the following features.

The ExpectError class allows you to catch and report exceptions, yet resume execution. This is useful in notebooks, as they would normally interrupt execution as soon as an exception is raised. Its typical usage is in conjunction with a with clause:

>>> with ExpectError():
>>>     x = 1 / 0
Traceback (most recent call last):
  File "<ipython-input-1-264328004f25>", line 2, in <module>
    x = 1 / 0
ZeroDivisionError: division by zero (expected)

The ExpectTimeout class allows you to interrupt execution after the specified time. This is useful for interrupting code that might otherwise run forever.

>>> with ExpectTimeout(5):
>>>     long_running_test()
Start
0 seconds have passed
1 seconds have passed
2 seconds have passed
3 seconds have passed

Traceback (most recent call last):
  File "<ipython-input-1-7e5136e65261>", line 2, in <module>
    long_running_test()
  File "<ipython-input-1-c23727bcee7d>", line 5, in long_running_test
    print(i, "seconds have passed")
  File "<ipython-input-1-c23727bcee7d>", line 5, in long_running_test
    print(i, "seconds have passed")
  File "<ipython-input-1-a8ce35c8777b>", line 25, in check_time
    raise TimeoutError
TimeoutError (expected)

The exception and the associated traceback are printed as error messages. If you do not want that, use these keyword options:

  • print_traceback (default True) can be set to False to avoid the traceback being printed
  • mute (default False) can be set to True to completely avoid any output.

Catching Errors

The class ExpectError allows to express that some code produces an exception. A typical usage looks as follows:

from ExpectError import ExpectError

with ExpectError():
    function_that_is_supposed_to_fail()

If an exception occurs, it is printed on standard error; yet, execution continues.

import bookutils
import traceback
import sys
from types import FrameType, TracebackType
class ExpectError:
    """Execute a code block expecting (and catching) an error."""

    def __init__(self, exc_type: Optional[type] = None, 
                 print_traceback: bool = True, mute: bool = False):
        """
        Constructor. Expect an exception of type `exc_type` (`None`: any exception).
        If `print_traceback` is set (default), print a traceback to stderr.
        If `mute` is set (default: False), do not print anything.
        """
        self.print_traceback = print_traceback
        self.mute = mute
        self.expected_exc_type = exc_type

    def __enter__(self) -> Any:
        """Begin of `with` block"""
        return self

    def __exit__(self, exc_type: type, 
                 exc_value: BaseException, tb: TracebackType) -> Optional[bool]:
        """End of `with` block"""
        if exc_type is None:
            # No exception
            return

        if (self.expected_exc_type is not None
            and exc_type != self.expected_exc_type):
            raise  # Unexpected exception

        # An exception occurred
        if self.print_traceback:
            lines = ''.join(
                traceback.format_exception(
                    exc_type,
                    exc_value,
                    tb)).strip()
        else:
            lines = traceback.format_exception_only(
                exc_type, exc_value)[-1].strip()

        if not self.mute:
            print(lines, "(expected)", file=sys.stderr)
        return True  # Ignore it

Here's an example:

def fail_test() -> None:
    # Trigger an exception
    x = 1 / 0
with ExpectError():
    fail_test()
Traceback (most recent call last):
  File "<ipython-input-1-67c629a2a842>", line 2, in <module>
    fail_test()
  File "<ipython-input-1-3f58bc38b36b>", line 3, in fail_test
    x = 1 / 0
ZeroDivisionError: division by zero (expected)
with ExpectError(print_traceback=False):
    fail_test()
ZeroDivisionError: division by zero (expected)

We can specify the type of the expected exception. This way, if something else happens, we will get notified.

with ExpectError(ZeroDivisionError):
    fail_test()
Traceback (most recent call last):
  File "<ipython-input-1-a7a7ccccd794>", line 2, in <module>
    fail_test()
  File "<ipython-input-1-3f58bc38b36b>", line 3, in fail_test
    x = 1 / 0
ZeroDivisionError: division by zero (expected)
with ExpectError():
    with ExpectError(ZeroDivisionError):
        some_nonexisting_function()
Traceback (most recent call last):
  File "<ipython-input-1-e6c7dad1986d>", line 3, in <module>
    some_nonexisting_function()  # type: ignore
  File "<ipython-input-1-e6c7dad1986d>", line 3, in <module>
    some_nonexisting_function()  # type: ignore
NameError: name 'some_nonexisting_function' is not defined (expected)

Catching Timeouts

The class ExpectTimeout(seconds) allows to express that some code may run for a long or infinite time; execution is thus interrupted after seconds seconds. A typical usage looks as follows:

from ExpectError import ExpectTimeout

with ExpectTimeout(2) as t:
    function_that_is_supposed_to_hang()

If an exception occurs, it is printed on standard error (as with ExpectError); yet, execution continues.

Should there be a need to cancel the timeout within the with block, t.cancel() will do the trick.

The implementation uses sys.settrace(), as this seems to be the most portable way to implement timeouts. It is not very efficient, though. Also, it only works on individual lines of Python code and will not interrupt a long-running system function.

import sys
import time
class ExpectTimeout:
    """Execute a code block expecting (and catching) a timeout."""

    def __init__(self, seconds: Union[int, float], 
                 print_traceback: bool = True, mute: bool = False):
        """
        Constructor. Interrupe execution after `seconds` seconds.
        If `print_traceback` is set (default), print a traceback to stderr.
        If `mute` is set (default: False), do not print anything.
        """

        self.seconds_before_timeout = seconds
        self.original_trace_function: Optional[Callable] = None
        self.end_time: Optional[float] = None
        self.print_traceback = print_traceback
        self.mute = mute

    def check_time(self, frame: FrameType, event: str, arg: Any) -> Callable:
        """Tracing function"""
        if self.original_trace_function is not None:
            self.original_trace_function(frame, event, arg)

        current_time = time.time()
        if self.end_time and current_time >= self.end_time:
            raise TimeoutError

        return self.check_time

    def __enter__(self) -> Any:
        """Begin of `with` block"""

        start_time = time.time()
        self.end_time = start_time + self.seconds_before_timeout

        self.original_trace_function = sys.gettrace()
        sys.settrace(self.check_time)
        return self

    def __exit__(self, exc_type: type, 
                 exc_value: BaseException, tb: TracebackType) -> Optional[bool]:
        """End of `with` block"""

        self.cancel()

        if exc_type is None:
            return

        # An exception occurred
        if self.print_traceback:
            lines = ''.join(
                traceback.format_exception(
                    exc_type,
                    exc_value,
                    tb)).strip()
        else:
            lines = traceback.format_exception_only(
                exc_type, exc_value)[-1].strip()

        if not self.mute:
            print(lines, "(expected)", file=sys.stderr)
        return True  # Ignore it

    def cancel(self) -> None:
        sys.settrace(self.original_trace_function)

Here's an example:

def long_running_test() -> None:
    print("Start")
    for i in range(10):
        time.sleep(1)
        print(i, "seconds have passed")
    print("End")
with ExpectTimeout(5, print_traceback=False):
    long_running_test()
Start
0 seconds have passed
1 seconds have passed
2 seconds have passed
3 seconds have passed
TimeoutError (expected)

Note that it is possible to nest multiple timeouts.

with ExpectTimeout(5):
    with ExpectTimeout(3):
        long_running_test()
    long_running_test()
Start
0 seconds have passed
1 seconds have passed
Traceback (most recent call last):
  File "<ipython-input-1-d87d5d50b60e>", line 3, in <module>
    long_running_test()
  File "<ipython-input-1-c23727bcee7d>", line 5, in long_running_test
    print(i, "seconds have passed")
  File "<ipython-input-1-c23727bcee7d>", line 5, in long_running_test
    print(i, "seconds have passed")
  File "<ipython-input-1-a8ce35c8777b>", line 25, in check_time
    raise TimeoutError
TimeoutError (expected)
Start
0 seconds have passed
Traceback (most recent call last):
  File "<ipython-input-1-d87d5d50b60e>", line 4, in <module>
    long_running_test()
  File "<ipython-input-1-c23727bcee7d>", line 5, in long_running_test
    print(i, "seconds have passed")
  File "<ipython-input-1-c23727bcee7d>", line 5, in long_running_test
    print(i, "seconds have passed")
  File "<ipython-input-1-a8ce35c8777b>", line 25, in check_time
    raise TimeoutError
TimeoutError (expected)

That's it, folks – enjoy!

Lessons Learned

  • With the ExpectError class, it is very easy to handle errors without interrupting notebook execution.

Creative Commons License The content of this project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. The source code that is part of the content, as well as the source code used to format and display that content is licensed under the MIT License. Last change: 2021-04-06 12:47:19+02:00CiteImprint