Skip to content

using tables to structure tracebacksยค

tracebacks are critical to improving code, but they must something that can be explored. this is not the case for AT the moment. exceptions are represented as long strings with no structure.

this approach uses to tables to structure tracebacks with improve the quality of experience for screen readers.

we still need to surface the code hints provided by the new traceback module works

https://github.com/python/cpython/blob/main/Lib/traceback.py#L544

    import sysconfig, operator, pathlib
    import traceback, contextlib, pandas, nbconvert_a11y
    %reload_ext nbconvert_a11y.outputs
    %reload_ext nbconvert_a11y.tables
    from nbconvert_a11y import tables
/tmp/ipykernel_53456/3040624387.py:2: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466

  import traceback, contextlib, pandas, nbconvert_a11y
    def multiply_wrong(x):
        "a function that wil raise an error"
        x[:-1].T@x

create an exception

    try:
        pandas.Series(range(10)).to_frame().pipe(multiply_wrong)
    except ValueError as e:
        exception = e
    traceback.print_exception(exception)
Traceback (most recent call last):
  File "/tmp/ipykernel_53456/1648588067.py", line 2, in <module>
    pandas.Series(range(10)).to_frame().pipe(multiply_wrong)
  File "/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/generic.py", line 6225, in pipe
    return common.pipe(self, func, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/common.py", line 500, in pipe
    return func(obj, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/ipykernel_53456/3574275760.py", line 3, in multiply_wrong
    x[:-1].T@x
    ~~~~~~~~^~
  File "/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/frame.py", line 1775, in __matmul__
    return self.dot(other)
           ^^^^^^^^^^^^^^^
  File "/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/frame.py", line 1725, in dot
    raise ValueError("matrices are not aligned")
ValueError: matrices are not aligned

turn the exceptions traceback into an array of arrays

    def get_exception_array(exception):
        return [
            list(frame) + [frame.colno, frame.end_colno][:0] for frame in traceback.extract_tb(exception.__traceback__)
        ]

format the exception as a dataframe

    df = pandas.DataFrame(
        get_exception_array(exception),
        columns=["file", "line", "method", "source"]
    )[["line", "method", "source", "file"]]

the filename makes it really hard to reason with what is being reported. it would be good to extract module name instead even though it is not part of the current repr

the path locations that we can use to shorten the file representation

    def get_paths():
        import ipykernel
        sys = sysconfig.get_paths()
        sys.update(__main__=ipykernel.compiler.get_tmp_directory())
        return sys
    def shorten_path(paths):
        sys = get_paths()
        visited = set()
        relative = [None]*len(paths)
        for lib in ("__main__", "purelib", "platlib", "stdlib", "platstdlib"):
            value = pathlib.Path(sys[lib])
            if value not in visited:
                for i, path in enumerate(paths):
                    if relative[i] is not None:
                        continue
                    try:
                        relative[i] = F"{lib}:{pathlib.Path(path).relative_to(value)}"
                    except ValueError:
                        pass

                visited.add(value)
        return relative

the dataframe indexes by shorted filenames making it easier to listen to

the traceback table is nested inside a figcaption that captures externals messages contained on the exception.

    pandas.DataFrame(get_exception_array(exception), columns=["file", "line", "method", "source"])
rows
6
columns
4
indexes
rows
1
columns
1
indexfilelinemethodsource
0/tmp/ipykernel_53456/1648588067.py2<module>pandas.Series(range(10)).to_frame().pipe(multiply_wrong)
1/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/generic.py6225pipereturn common.pipe(self, func, *args, **kwargs)
2/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/common.py500pipereturn func(obj, *args, **kwargs)
3/tmp/ipykernel_53456/3574275760.py3multiply_wrongx[:-1].T@x
4/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/frame.py1775__matmul__return self.dot(other)
5/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/frame.py1725dotraise ValueError("matrices are not aligned")
    def repr_traceback_frame(exception):
        df = pandas.DataFrame(get_exception_array(exception), columns=["file", "line", "method", "source"])
        df = df.set_index([pandas.Index(
            df.file.pipe(shorten_path),
            name="module"
        )])
        return df

    def repr_traceback(exception):
        figure = nbconvert_a11y.outputs.new(
            "figure", 
            tables.get_table(repr_traceback_frame(exception), type_=type(exception), COL_INDEX=tables.SHOW_INDEX.nonvisual, ROW_INDEX=tables.SHOW_INDEX.nonvisual, SEMANTIC=False),
            nbconvert_a11y.outputs.new("figcaption", F"{type(exception).__name__}: {exception}")
        )
        return str(figure)

an accessible figure/table for the traceback

    display({"text/html": repr_traceback(exception)}, raw=True)
rows
6
columns
4
indexes
rows
1
columns
1
modulefilelinemethodsource
__main__:1648588067.py/tmp/ipykernel_53456/1648588067.py2<module>pandas.Series(range(10)).to_frame().pipe(multiply_wrong)
purelib:pandas/core/generic.py/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/generic.py6225pipereturn common.pipe(self, func, *args, **kwargs)
purelib:pandas/core/common.py/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/common.py500pipereturn func(obj, *args, **kwargs)
__main__:3574275760.py/tmp/ipykernel_53456/3574275760.py3multiply_wrongx[:-1].T@x
purelib:pandas/core/frame.py/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/frame.py1775__matmul__return self.dot(other)
purelib:pandas/core/frame.py/home/tbone/mambaforge/envs/p311/lib/python3.11/site-packages/pandas/core/frame.py1725dotraise ValueError("matrices are not aligned")
ValueError: matrices are not aligned

custom css to make the table appear similar to native tracebacks

%%html
<style>
table[itemscope]::before {
    content: "Traceback (most recent call last):";
}
table[itemscope] {
    td data {color: unset;}
    tr:first-child, tr > th:first-child {
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        height: 1px;
        overflow: hidden;
        position: absolute;
        white-space: nowrap;
        width: 1px;
    }
    tr {
        display: block;
        position: relative;

        td {
            display: inline;
        }
        td:nth-of-type(1)::before {content: 'File "';}
        td:nth-of-type(1)::after {content: '", ';}
        td:nth-of-type(2)::before {content: 'line ';}
        td:nth-of-type(2)::after {content: ', ';}
        td:nth-of-type(3)::before {content: 'in ';}
        td {
            left: 2em;
            position: relative;
        }
        td:nth-of-type(4) {
            display: block;
            left: 4em;
        }
    }

}
figure {
    figcaption {
        font-family: monospace;
    }
}
</style>