Skip to content

implemented debug() editor function which lets you navigate in the stack frame #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion doc/source/changes/version_0_32.rst.inc
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
.. py:currentmodule:: larray_editor

.. _misc_editor:
New features
^^^^^^^^^^^^

* added :py:obj:`debug()` function which opens an editor window with an extra widget to navigate back in the call
stack (the chain of functions called to reach the current line of code).


Miscellaneous improvements
^^^^^^^^^^^^^^^^^^^^^^^^^^

* added keyword arguments ``rtol``, ``atol`` and ``nans_equal`` to the :py:obj:`compare()` function
(closes :editor_issue:`172`).

* :py:obj:`run_editor_on_exception()` now uses :py:obj:`debug()` so that one can inspect what the state was in all
functions traversed to reach the code which triggered the exception.


Fixes
^^^^^
Expand Down
89 changes: 66 additions & 23 deletions larray_editor/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import larray as la

from larray_editor.editor import REOPEN_LAST_FILE, MappingEditor, ArrayEditor
from larray_editor.traceback_tools import extract_stack, extract_tb, StackSummary

__all__ = ['view', 'edit', 'compare', 'REOPEN_LAST_FILE', 'run_editor_on_exception']
__all__ = ['view', 'edit', 'debug', 'compare', 'REOPEN_LAST_FILE', 'run_editor_on_exception']


def qapplication():
Expand Down Expand Up @@ -104,7 +105,10 @@ def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth
>>> # will open an editor for a1 only
>>> edit(a1) # doctest: +SKIP
"""
install_except_hook()
# we don't use install_except_hook/restore_except_hook so that we can restore the hook actually used when
# this function is called instead of the one which was used when the module was loaded.
orig_except_hook = sys.excepthook
sys.excepthook = _qt_except_hook

_app = QApplication.instance()
if _app is None:
Expand Down Expand Up @@ -147,7 +151,7 @@ def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth
dlg.show()
_app.exec_()

restore_except_hook()
sys.excepthook = orig_except_hook


def view(obj=None, title='', depth=0, display_caller_info=True):
Expand Down Expand Up @@ -182,6 +186,43 @@ def view(obj=None, title='', depth=0, display_caller_info=True):
edit(obj, title=title, readonly=True, depth=depth + 1, display_caller_info=display_caller_info)


def _debug(stack_summary, stack_pos=None):
# we don't use install_except_hook/restore_except_hook so that we can restore the hook actually used when
# this function is called instead of the one which was used when the module was loaded.
orig_except_hook = sys.excepthook
sys.excepthook = _qt_except_hook

_app = QApplication.instance()
if _app is None:
_app = qapplication()
_app.setOrganizationName("LArray")
_app.setApplicationName("Debugger")
parent = None
else:
parent = _app.activeWindow()

assert isinstance(stack_summary, StackSummary)
dlg = MappingEditor(parent)
setup_ok = dlg.setup_and_check(stack_summary, stack_pos=stack_pos)
if setup_ok:
dlg.show()
_app.exec_()

sys.excepthook = orig_except_hook


def debug(depth=0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add this function to the larray repository ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

"""
Opens a new debug window.

depth : int, optional
Stack depth where to look for variables. Defaults to 0 (where this function was called).
"""
caller_frame = sys._getframe(depth + 1)
stack_summary = extract_stack(caller_frame)
_debug(stack_summary)


def compare(*args, **kwargs):
"""
Opens a new comparator window, comparing arrays or sessions.
Expand Down Expand Up @@ -224,7 +265,10 @@ def compare(*args, **kwargs):
>>> compare(a1, a2, title='first comparison') # doctest: +SKIP
>>> compare(a1 + 1, a2, title='second comparison', names=['a1+1', 'a2']) # doctest: +SKIP
"""
install_except_hook()
# we don't use install_except_hook/restore_except_hook so that we can restore the hook actually used when
# this function is called instead of the one which was used when the module was loaded.
orig_except_hook = sys.excepthook
sys.excepthook = _qt_except_hook

title = kwargs.pop('title', '')
names = kwargs.pop('names', None)
Expand Down Expand Up @@ -267,7 +311,8 @@ def get_name(i, obj, depth=0):
dlg.show()
_app.exec_()

restore_except_hook()
sys.excepthook = orig_except_hook


_orig_except_hook = sys.excepthook

Expand Down Expand Up @@ -307,15 +352,7 @@ def _trace_code_file(tb):
return os.path.normpath(tb.tb_frame.f_code.co_filename)


def _get_vars_from_frame(frame):
frame_globals, frame_locals = frame.f_globals, frame.f_locals
d = collections.OrderedDict()
d.update([(k, frame_globals[k]) for k in sorted(frame_globals.keys())])
d.update([(k, frame_locals[k]) for k in sorted(frame_locals.keys())])
return d


def _get_debug_except_hook(root_path=None, usercode_traceback=True):
def _get_debug_except_hook(root_path=None, usercode_traceback=True, usercode_frame=True):
try:
main_file = os.path.abspath(sys.modules['__main__'].__file__)
except AttributeError:
Expand All @@ -333,29 +370,31 @@ def excepthook(type, value, tback):

main_tb = current_tb if _trace_code_file(current_tb) == main_file else tback

if usercode_traceback:
user_tb_length = None
if usercode_traceback or usercode_frame:
if main_tb != current_tb:
print("Warning: couldn't find frame corresponding to user code, showing the full traceback "
"and inspect last frame instead (which might be in library code)",
file=sys.stderr)
limit = None
else:
user_tb_length = 1
# continue as long as the next tb is still in the current project
while current_tb.tb_next and _trace_code_file(current_tb.tb_next).startswith(root_path):
current_tb = current_tb.tb_next
user_tb_length += 1
limit = user_tb_length
else:
limit = None
traceback.print_exception(type, value, main_tb, limit=limit)

tb_limit = user_tb_length if usercode_traceback else None
traceback.print_exception(type, value, main_tb, limit=tb_limit)

stack = extract_tb(main_tb, limit=tb_limit)
stack_pos = user_tb_length - 1 if user_tb_length is not None and usercode_frame else None
print("\nlaunching larray editor to debug...", file=sys.stderr)
edit(_get_vars_from_frame(current_tb.tb_frame))
_debug(stack, stack_pos=stack_pos)

return excepthook


def run_editor_on_exception(root_path=None, usercode_traceback=True):
def run_editor_on_exception(root_path=None, usercode_traceback=True, usercode_frame=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to repeat the new argument in the file viewer/__init__.pt in the larray repository

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

"""
Run the editor when an unhandled exception (a fatal error) happens.

Expand All @@ -366,9 +405,13 @@ def run_editor_on_exception(root_path=None, usercode_traceback=True):
usercode_traceback : bool, optional
Whether or not to show only the part of the traceback (error log) which corresponds to the user code.
Otherwise, it will show the complete traceback, including code inside libraries. Defaults to True.
usercode_frame : bool, optional
Whether or not to start the debug window in the frame corresponding to the user code.
This argument is ignored (it is always True) if usercode_traceback is True. Defaults to True.

Notes
-----
sets sys.excepthook
"""
sys.excepthook = _get_debug_except_hook(root_path=root_path, usercode_traceback=usercode_traceback)
sys.excepthook = _get_debug_except_hook(root_path=root_path, usercode_traceback=usercode_traceback,
usercode_frame=usercode_frame)
63 changes: 50 additions & 13 deletions larray_editor/editor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import os
import re

import matplotlib
import numpy as np
import collections

from larray import LArray, Session, empty
from larray_editor.traceback_tools import StackSummary
from larray_editor.utils import (PY2, PYQT5, _, create_action, show_figure, ima, commonpath, dependencies,
get_versions, get_documentation_url, urls, RecentlyUsedList)
from larray_editor.arraywidget import ArrayEditorWidget
Expand All @@ -13,7 +15,7 @@
from qtpy.QtCore import Qt, QUrl
from qtpy.QtGui import QDesktopServices, QKeySequence
from qtpy.QtWidgets import (QMainWindow, QWidget, QListWidget, QListWidgetItem, QSplitter, QFileDialog, QPushButton,
QDialogButtonBox, QShortcut, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QUndoStack,
QDialogButtonBox, QShortcut, QVBoxLayout, QGridLayout, QLineEdit, QUndoStack,
QCheckBox, QComboBox, QMessageBox, QDialog, QInputDialog, QLabel, QGroupBox, QRadioButton)

try:
Expand Down Expand Up @@ -42,9 +44,9 @@

REOPEN_LAST_FILE = object()

assignment_pattern = re.compile('[^\[\]]+[^=]=[^=].+')
setitem_pattern = re.compile('(.+)\[.+\][^=]=[^=].+')
history_vars_pattern = re.compile('_i?\d+')
assignment_pattern = re.compile(r'[^\[\]]+[^=]=[^=].+')
setitem_pattern = re.compile(r'(.+)\[.+\][^=]=[^=].+')
history_vars_pattern = re.compile(r'_i?\d+')
# XXX: add all scalars except strings (from numpy or plain Python)?
# (long) strings are not handled correctly so should NOT be in this list
# tuple, list
Expand Down Expand Up @@ -307,7 +309,7 @@ def __init__(self, parent=None):

self.setup_menu_bar()

def _setup_and_check(self, widget, data, title, readonly, **kwargs):
def _setup_and_check(self, widget, data, title, readonly, stack_pos=None):
"""Setup MappingEditor"""
layout = QVBoxLayout()
widget.setLayout(layout)
Expand All @@ -332,6 +334,7 @@ def _setup_and_check(self, widget, data, title, readonly, **kwargs):
kernel = kernel_manager.kernel

# TODO: use self._reset() instead
# FIXME: when using the editor as a debugger this is annoying
kernel.shell.run_cell('from larray import *')
text_formatter = kernel.shell.display_formatter.formatters['text/plain']

Expand Down Expand Up @@ -373,9 +376,34 @@ def void_formatter(array, *args, **kwargs):
right_panel_widget.setLayout(right_panel_layout)

main_splitter = QSplitter(Qt.Horizontal)
main_splitter.addWidget(self._listwidget)
debug = isinstance(data, StackSummary)
if debug:
self._stack_frame_widget = QListWidget(self)
stack_frame_widget = self._stack_frame_widget
stack_frame_widget.itemSelectionChanged.connect(self.on_stack_frame_changed)
stack_frame_widget.setMinimumWidth(60)

for frame_summary in data:
funcname = frame_summary.name
filename = os.path.basename(frame_summary.filename)
listitem = QListWidgetItem(stack_frame_widget)
listitem.setText("{}, {}:{}".format(funcname, filename, frame_summary.lineno))
# we store the frame locals in the user data of the list
listitem.setData(Qt.UserRole, frame_summary.locals)
listitem.setToolTip(frame_summary.line)
row = stack_pos if stack_pos is not None else len(data) - 1
stack_frame_widget.setCurrentRow(row)

left_panel_widget = QSplitter(Qt.Vertical)
left_panel_widget.addWidget(self._listwidget)
left_panel_widget.addWidget(stack_frame_widget)
left_panel_widget.setSizes([500, 200])
data = self.data
else:
left_panel_widget = self._listwidget
main_splitter.addWidget(left_panel_widget)
main_splitter.addWidget(right_panel_widget)
main_splitter.setSizes([10, 90])
main_splitter.setSizes([180, 620])
main_splitter.setCollapsible(1, False)

layout.addWidget(main_splitter)
Expand All @@ -394,8 +422,7 @@ def void_formatter(array, *args, **kwargs):
else:
QMessageBox.critical(self, "Error", "File {} could not be found".format(data))
self.new()
# convert input data to Session if not
else:
elif not debug:
self._push_data(data)

def _push_data(self, data):
Expand All @@ -406,6 +433,16 @@ def _push_data(self, data):
self.add_list_items(arrays)
self._listwidget.setCurrentRow(0)

def on_stack_frame_changed(self):
selected = self._stack_frame_widget.selectedItems()
if selected:
assert len(selected) == 1
selected_item = selected[0]
assert isinstance(selected_item, QListWidgetItem)
data = selected_item.data(Qt.UserRole)
self._reset()
self._push_data(data)

def _reset(self):
self.data = Session()
self._listwidget.clear()
Expand Down Expand Up @@ -488,9 +525,9 @@ def add_list_items(self, names):

def delete_list_item(self, to_delete):
deleted_items = self._listwidget.findItems(to_delete, Qt.MatchExactly)
assert len(deleted_items) == 1
deleted_item_idx = self._listwidget.row(deleted_items[0])
self._listwidget.takeItem(deleted_item_idx)
if len(deleted_items) == 1:
deleted_item_idx = self._listwidget.row(deleted_items[0])
self._listwidget.takeItem(deleted_item_idx)

def select_list_item(self, to_display):
changed_items = self._listwidget.findItems(to_display, Qt.MatchExactly)
Expand Down Expand Up @@ -614,7 +651,7 @@ def ipython_cell_executed(self):
# last command. Which means that if the last command did not produce any output, _ is not modified.
cur_output = user_ns['_oh'].get(cur_input_num)
if cur_output is not None:
if self._display_in_grid('_', cur_output):
if self._display_in_grid('<expr>', cur_output):
self.view_expr(cur_output)

if isinstance(cur_output, collections.Iterable):
Expand Down
9 changes: 9 additions & 0 deletions larray_editor/tests/test_api_larray.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from larray_editor.api import *
from larray_editor.utils import logger

run_editor_on_exception(usercode_traceback=False, usercode_frame=True)

logger.setLevel(logging.DEBUG)

Expand Down Expand Up @@ -127,6 +128,7 @@ def make_demo(width=20, ball_radius=5, path_radius=5, steps=30):
# import cProfile as profile
# profile.runctx('edit(Session(arr2=arr2))', vars(), {},
# 'c:\\tmp\\edit.profile')
debug()
edit()
# edit(ses)
# edit(file)
Expand Down Expand Up @@ -171,3 +173,10 @@ def make_demo(width=20, ball_radius=5, path_radius=5, steps=30):
arr2 = 2 * arr1
arr3 = where(arr1 % 2 == 0, arr1, -arr1)
compare(arr1, arr2, arr3, bg_gradient='blue-red')


def test_run_editor_on_exception():
return arr2['my_invalid_key']


test_run_editor_on_exception()
Loading