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
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)
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"])
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)
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>