[error, ports] Make stack traces more directly reflect teh location of the issue

This commit is contained in:
Jan Petykiewicz 2025-10-30 01:14:37 -07:00
parent 240007eb7a
commit dadaf48d35
2 changed files with 59 additions and 7 deletions

View File

@ -1,3 +1,10 @@
import traceback
import pathlib
MASQUE_DIR = str(pathlib.Path(__file__).parent)
class MasqueError(Exception): class MasqueError(Exception):
""" """
Parent exception for all Masque-related Exceptions Parent exception for all Masque-related Exceptions
@ -39,3 +46,50 @@ class OneShotError(MasqueError):
""" """
def __init__(self, func_name: str) -> None: def __init__(self, func_name: str) -> None:
Exception.__init__(self, f'Function "{func_name}" with @oneshot was called more than once') Exception.__init__(self, f'Function "{func_name}" with @oneshot was called more than once')
def format_stacktrace(
stacklevel: int = 1,
*,
skip_file_prefixes: tuple[str, ...] = (MASQUE_DIR,),
low_file_prefixes: tuple[str, ...] = ('<runpy', '<string>'),
low_file_suffixes: tuple[str, ...] = ('IPython/utils/py3compat.py', ),
) -> str:
"""
Utility function for making nicer stack traces (e.g. excluding <frozen runpy> and similar)
Args:
stacklevel: Number of frames to remove from near this function (default is to
show caller but not ourselves). Similar to `warnings.warn` and `logging.warning`.
skip_file_prefixes: Indicates frames to ignore after counting stack levels; similar
to `warnings.warn` *TODO check if this is actually the same effect re:stacklevel*.
Forces stacklevel to max(2, stacklevel).
Default is to exclude anything within `masque`.
low_file_prefixes: Indicates frames to ignore on the other (entry-point) end of the stack,
based on prefixes on their filenames.
low_file_suffixes: Indicates frames to ignore on the other (entry-point) end of the stack,
based on suffixes on their filenames.
Returns:
Formatted trimmed stack trace
"""
if skip_file_prefixes:
stacklevel = max(2, stacklevel)
stack = traceback.extract_stack()
bad_inds = [ii + 1 for ii, frame in enumerate(stack)
if frame.filename.startswith(low_file_prefixes) or frame.filename.endswith(low_file_suffixes)]
first_ok = max([0] + bad_inds)
last_ok = -stacklevel - 1
while last_ok >= -len(stack) and stack[last_ok].filename.startswith(skip_file_prefixes):
last_ok -= 1
if selected := stack[first_ok:last_ok + 1]:
pass
elif selected := stack[:-stacklevel]:
pass
else:
selected = stack
return ''.join(traceback.format_list(selected))

View File

@ -1,7 +1,5 @@
from typing import overload, Self, NoReturn, Any from typing import overload, Self, NoReturn, Any
from collections.abc import Iterable, KeysView, ValuesView, Mapping from collections.abc import Iterable, KeysView, ValuesView, Mapping
import warnings
import traceback
import logging import logging
import functools import functools
from collections import Counter from collections import Counter
@ -14,7 +12,7 @@ from numpy.typing import ArrayLike, NDArray
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
from .utils import rotate_offsets_around from .utils import rotate_offsets_around
from .error import PortError from .error import PortError, format_stacktrace
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -307,9 +305,9 @@ class PortList(metaclass=ABCMeta):
msg = 'Ports have conflicting types:\n' msg = 'Ports have conflicting types:\n'
for nn, (kk, vv) in enumerate(connections.items()): for nn, (kk, vv) in enumerate(connections.items()):
if type_conflicts[nn]: if type_conflicts[nn]:
msg = ''.join(traceback.format_stack()) + '\n' + msg
warnings.warn(msg, stacklevel=2)
msg += f'{kk} | {a_types[nn]}:{b_types[nn]} | {vv}\n' msg += f'{kk} | {a_types[nn]}:{b_types[nn]} | {vv}\n'
msg += '\nStack trace:\n' + format_stacktrace()
logger.warning(msg)
a_offsets = numpy.array([pp.offset for pp in a_ports]) a_offsets = numpy.array([pp.offset for pp in a_ports])
b_offsets = numpy.array([pp.offset for pp in b_ports]) b_offsets = numpy.array([pp.offset for pp in b_ports])
@ -522,9 +520,9 @@ class PortList(metaclass=ABCMeta):
msg = 'Ports have conflicting types:\n' msg = 'Ports have conflicting types:\n'
for nn, (kk, vv) in enumerate(map_in.items()): for nn, (kk, vv) in enumerate(map_in.items()):
if type_conflicts[nn]: if type_conflicts[nn]:
msg = ''.join(traceback.format_stack()) + '\n' + msg
warnings.warn(msg, stacklevel=2)
msg += f'{kk} | {s_types[nn]}:{o_types[nn]} | {vv}\n' msg += f'{kk} | {s_types[nn]}:{o_types[nn]} | {vv}\n'
msg += '\nStack trace:\n' + format_stacktrace()
logger.warning(msg)
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi) rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
if not has_rot.any(): if not has_rot.any():