Skip to content

Reference

Crash Catcher

Error logger/crash reporter decorator

crash_catcher(filename, message, title, postamble=None, overwrite=True, **extra)

Catch crash (uncaught exception) and create a crash dump file on error named filename

Parameters:

Name Type Description Default
filename str | Path | PathLike

name of crash dump file to create

required
message str

message to print to stderr upon crash

required
title str

title to print to start of crash dump file

required
postamble str | None

optional message printed to stderr after crash dump file is created.

None
overwrite bool

if True, overwrite existing file, otherwise increment filename until a non-existent filename is found

True
extra Any

If kwargs provided, any additional arguments to the function will be printed to the crash file.

{}
This decorator should be applied to the main function of the program.

The message, title, and postamble arguments may include the template {filename} which will be replaced with the actual filename of the crash log. For example, if overwrite=False and the crash file crash_catcher.log already exists, the filename will be incremented to crash_catcher (1).log, crash_catcher (2).log, and so on until a non-existent filename is found. This filename will be used to render the {filename} template.

Source code in crash_catcher/crash_catcher.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def crash_catcher(
    filename: str | pathlib.Path | os.PathLike,
    message: str,
    title: str,
    postamble: str | None = None,
    overwrite: bool = True,
    **extra: Any,
):
    """Catch crash (uncaught exception) and create a crash dump file on error named filename

    Args:
        filename: name of crash dump file to create
        message: message to print to stderr upon crash
        title: title to print to start of crash dump file
        postamble: optional message printed to stderr after crash dump file is created.
        overwrite: if True, overwrite existing file, otherwise increment filename until a non-existent filename is found
        extra: If kwargs provided, any additional arguments to the function will be printed to the crash file.

    Note: This decorator should be applied to the main function of the program.
        The message, title, and postamble arguments may include the template {filename} which
        will be replaced with the actual filename of the crash log. For example, if overwrite=False
        and the crash file `crash_catcher.log` already exists, the filename will be incremented to
        `crash_catcher (1).log`, `crash_catcher (2).log`, and so on until a non-existent filename is found.
        This filename will be used to render the {filename} template.
    """

    filename = (
        pathlib.Path(filename) if not isinstance(filename, pathlib.Path) else filename
    )
    filename = filename.resolve()

    class Default(dict):
        def __missing__(self, key):
            return key

    def decorated(func):
        @functools.wraps(func)
        def wrapped(*args, **kwargs):
            nonlocal filename, message, title, postamble
            caller = sys._getframe().f_back.f_code.co_name
            name = func.__name__
            timestamp = datetime.datetime.now().isoformat()
            start_t = time.perf_counter()
            try:
                return func(*args, **kwargs)
            except Exception as e:
                stop_t = time.perf_counter()

                # if filename needs to be incremented, do so now
                # then update message, title, and postamble {filename} template
                if filename.exists() and not overwrite:
                    filename = _increment_filename(filename)
                message = message.format_map(Default(filename=filename))
                title = title.format_map(Default(filename=filename))
                postamble = (
                    postamble.format_map(Default(filename=filename))
                    if postamble
                    else ""
                )

                print(message, file=sys.stderr)
                print(f"{e}", file=sys.stderr)

                # handle any callbacks
                for f, msg in _global_callbacks.values():
                    if msg:
                        print(msg)
                    f()

                with open(filename, "w") as f:
                    f.write(f"{title}\n")
                    f.write(f"Created: {datetime.datetime.now()}\n")
                    f.write("\nSYSTEM INFO:\n")
                    f.write(f"Platform: {platform.platform()}\n")
                    f.write(f"Python: {pathlib.Path(sys.executable).resolve()}\n")
                    f.write(f"Python version: {sys.version}\n")
                    f.write(f"Python path: {sys.path}\n")
                    f.write(f"sys.argv: {sys.argv}\n")
                    f.write("\nCRASH DATA:\n")
                    f.write(
                        f"{name} called by {caller} at {timestamp} crashed after {stop_t-start_t} seconds\n"
                    )
                    f.write(f"{args=}\n")
                    f.write(f"{kwargs=}\n")
                    for k, v in _global_crash_data.items():
                        f.write(f"{k}: {v}\n")
                    for arg, value in extra.items():
                        f.write(f"{arg}: {value}\n")
                    f.write(f"Error: {e}\n")
                    traceback.print_exc(file=f)
                print(f"Crash log written to '{filename}'", file=sys.stderr)
                print(f"{postamble}", file=sys.stderr)
                sys.exit(1)

        return wrapped

    return decorated

register_crash_callback(func, message=None)

Register callback to be run if crash is caught.

Parameters:

Name Type Description Default
func Callable[[], None]

callable that will be called (with no args) by crash_catcher

required
message str | None

optional message

None

Returns:

Type Description
int

id for callback which may be used with unregister_crash_callback() to remove the callback.

Multiple callabacks may be registered by calling this function repeatedly.

Callbacks will be executed in order they are registered.

Source code in crash_catcher/crash_catcher.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def register_crash_callback(
    func: Callable[[], None], message: str | None = None
) -> int:
    """Register callback to be run if crash is caught.

    Args:
        func: callable that will be called (with no args) by crash_catcher
        message: optional message

    Returns:
        id for callback which may be used with unregister_crash_callback() to remove the callback.

    Note: Multiple callabacks may be registered by calling this function repeatedly.
        Callbacks will be executed in order they are registered.
    """

    callback_id = time.monotonic_ns()
    _global_callbacks[callback_id] = (func, message)
    return callback_id

set_crash_data(key_, data)

Set data to be printed in crash log

Source code in crash_catcher/crash_catcher.py
38
39
40
def set_crash_data(key_: Any, data: Any):
    """Set data to be printed in crash log"""
    _global_crash_data[key_] = data

unregister_crash_callback(callback_id)

Unregister a crash callback previously registered with register_crash_callback().

Parameters:

Name Type Description Default
callback_id int

the ID of the callback to unregister as returned by register_crash_callback()

required

Raises:

Type Description
ValueError

if the callback_id is not valid

Note: After a callback is unregisterd, it will not be called if a crash is caught.

Source code in crash_catcher/crash_catcher.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def unregister_crash_callback(callback_id: int):
    """Unregister a crash callback previously registered with register_crash_callback().

    Args:
        callback_id: the ID of the callback to unregister as returned by register_crash_callback()

    Raises:
        ValueError: if the callback_id is not valid

    Note: After a callback is unregisterd, it will not be called if a crash is caught.
    """
    try:
        del _global_callbacks[callback_id]
    except KeyError:
        raise ValueError(f"Invalid callback_id: {callback_id}")