mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Template debugger: Add breakpoints for python templates. On a breakpoint open a dialog displaying variables in the current frame.
Also indicate when the debugger is waiting for the 'Go' button. This fixes problems when people don't notice they have checked the Enable breakpoints box.
This commit is contained in:
parent
77dc74137f
commit
6374517dd4
@ -7,6 +7,7 @@ __license__ = 'GPL v3'
|
|||||||
|
|
||||||
import json, os, traceback, re
|
import json, os, traceback, re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import sys
|
||||||
|
|
||||||
from qt.core import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, QFont,
|
from qt.core import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, QFont,
|
||||||
QApplication, QTextCharFormat, QColor, QCursor,
|
QApplication, QTextCharFormat, QColor, QCursor,
|
||||||
@ -389,6 +390,18 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
|||||||
self.builtins = (builtin_functions if builtin_functions else
|
self.builtins = (builtin_functions if builtin_functions else
|
||||||
formatter_functions().get_builtins_and_aliases())
|
formatter_functions().get_builtins_and_aliases())
|
||||||
|
|
||||||
|
# Set up the breakpoint bar
|
||||||
|
s = gprefs.get('template_editor_break_on_print', False)
|
||||||
|
self.go_button.setEnabled(s)
|
||||||
|
self.remove_all_button.setEnabled(s)
|
||||||
|
self.set_all_button.setEnabled(s)
|
||||||
|
self.toggle_button.setEnabled(s)
|
||||||
|
self.breakpoint_line_box.setEnabled(s)
|
||||||
|
self.breakpoint_line_box_label.setEnabled(s)
|
||||||
|
self.break_box.setChecked(s)
|
||||||
|
self.break_box.stateChanged.connect(self.break_box_changed)
|
||||||
|
self.go_button.clicked.connect(self.go_button_pressed)
|
||||||
|
|
||||||
# Set up the display table
|
# Set up the display table
|
||||||
self.table_column_widths = None
|
self.table_column_widths = None
|
||||||
try:
|
try:
|
||||||
@ -451,16 +464,6 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
|||||||
'<a href="{}">{}</a>'.format(
|
'<a href="{}">{}</a>'.format(
|
||||||
localize_user_manual_link('https://manual.calibre-ebook.com/generated/en/template_ref.html'), tt))
|
localize_user_manual_link('https://manual.calibre-ebook.com/generated/en/template_ref.html'), tt))
|
||||||
|
|
||||||
s = gprefs.get('template_editor_break_on_print', False)
|
|
||||||
self.go_button.setEnabled(s)
|
|
||||||
self.remove_all_button.setEnabled(s)
|
|
||||||
self.set_all_button.setEnabled(s)
|
|
||||||
self.toggle_button.setEnabled(s)
|
|
||||||
self.breakpoint_line_box.setEnabled(s)
|
|
||||||
self.breakpoint_line_box_label.setEnabled(s)
|
|
||||||
self.break_box.setChecked(s)
|
|
||||||
self.break_box.stateChanged.connect(self.break_box_changed)
|
|
||||||
self.go_button.clicked.connect(self.go_button_pressed)
|
|
||||||
self.textbox.setFocus()
|
self.textbox.setFocus()
|
||||||
self.set_up_font_boxes()
|
self.set_up_font_boxes()
|
||||||
self.toggle_button.clicked.connect(self.toggle_button_pressed)
|
self.toggle_button.clicked.connect(self.toggle_button_pressed)
|
||||||
@ -546,11 +549,19 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
|||||||
for r in range(0, len(mi)):
|
for r in range(0, len(mi)):
|
||||||
w = QLineEdit(tv)
|
w = QLineEdit(tv)
|
||||||
w.setReadOnly(True)
|
w.setReadOnly(True)
|
||||||
|
w.setText(mi[r].title)
|
||||||
tv.setCellWidget(r, 0, w)
|
tv.setCellWidget(r, 0, w)
|
||||||
w = QLineEdit(tv)
|
w = QLineEdit(tv)
|
||||||
w.setReadOnly(True)
|
w.setReadOnly(True)
|
||||||
tv.setCellWidget(r, 1, w)
|
tv.setCellWidget(r, 1, w)
|
||||||
self.display_values('')
|
self.set_waiting_message()
|
||||||
|
|
||||||
|
def set_waiting_message(self):
|
||||||
|
if self.break_box.isChecked():
|
||||||
|
for i in range(len(self.mi)):
|
||||||
|
self.template_value.cellWidget(i, 1).setText('')
|
||||||
|
self.template_value.cellWidget(0, 1).setText(
|
||||||
|
_("*** Breakpoints are enabled. Waiting for the 'Go' button to be pressed"))
|
||||||
|
|
||||||
def show_context_menu(self, point):
|
def show_context_menu(self, point):
|
||||||
m = self.textbox.createStandardContextMenu()
|
m = self.textbox.createStandardContextMenu()
|
||||||
@ -678,6 +689,8 @@ def evaluate(book, context):
|
|||||||
self.breakpoint_line_box_label.setEnabled(new_state != 0)
|
self.breakpoint_line_box_label.setEnabled(new_state != 0)
|
||||||
if new_state == 0:
|
if new_state == 0:
|
||||||
self.display_values(str(self.textbox.toPlainText()))
|
self.display_values(str(self.textbox.toPlainText()))
|
||||||
|
else:
|
||||||
|
self.set_waiting_message()
|
||||||
|
|
||||||
def go_button_pressed(self):
|
def go_button_pressed(self):
|
||||||
self.display_values(str(self.textbox.toPlainText()))
|
self.display_values(str(self.textbox.toPlainText()))
|
||||||
@ -760,14 +773,17 @@ def evaluate(book, context):
|
|||||||
c = app.clipboard()
|
c = app.clipboard()
|
||||||
c.setText(str(self.icon_files.currentText()))
|
c.setText(str(self.icon_files.currentText()))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_python(self):
|
||||||
|
return self.textbox.toPlainText().startswith('python:')
|
||||||
|
|
||||||
def textbox_changed(self):
|
def textbox_changed(self):
|
||||||
cur_text = str(self.textbox.toPlainText())
|
cur_text = str(self.textbox.toPlainText())
|
||||||
if cur_text.startswith('python:'):
|
if self.is_python:
|
||||||
if self.highlighting_gpm is True:
|
if self.highlighting_gpm is True:
|
||||||
self.highlighter.initialize_rules(self.builtins, True)
|
self.highlighter.initialize_rules(self.builtins, True)
|
||||||
self.highlighting_gpm = False
|
self.highlighting_gpm = False
|
||||||
self.break_box.setChecked(False)
|
self.break_box.setEnabled(True)
|
||||||
self.break_box.setEnabled(False)
|
|
||||||
elif not self.highlighting_gpm:
|
elif not self.highlighting_gpm:
|
||||||
self.highlighter.initialize_rules(self.builtins, False)
|
self.highlighter.initialize_rules(self.builtins, False)
|
||||||
self.highlighting_gpm = True
|
self.highlighting_gpm = True
|
||||||
@ -776,8 +792,33 @@ def evaluate(book, context):
|
|||||||
self.last_text = cur_text
|
self.last_text = cur_text
|
||||||
self.highlighter.regenerate_paren_positions()
|
self.highlighter.regenerate_paren_positions()
|
||||||
self.text_cursor_changed()
|
self.text_cursor_changed()
|
||||||
if self.break_box.checkState() == Qt.CheckState.Unchecked:
|
if not self.break_box.isChecked():
|
||||||
self.display_values(cur_text)
|
self.display_values(cur_text)
|
||||||
|
else:
|
||||||
|
self.set_waiting_message()
|
||||||
|
|
||||||
|
def trace_lines(self, frame, event, arg):
|
||||||
|
if event != 'line':
|
||||||
|
return
|
||||||
|
# Only respond to events in the "string" which is the template
|
||||||
|
if frame.f_code.co_filename != '<string>':
|
||||||
|
return
|
||||||
|
# Check that there is a breakpoint at the line
|
||||||
|
if frame.f_lineno not in self.textbox.clicked_line_numbers:
|
||||||
|
return
|
||||||
|
l = self.template_value.selectionModel().selectedRows()
|
||||||
|
mi_to_use = self.mi[0 if len(l) == 0 else l[0].row()]
|
||||||
|
self.break_reporter_dialog = PythonBreakReporter(self, mi_to_use, frame)
|
||||||
|
if not self.break_reporter_dialog.exec():
|
||||||
|
raise StopException()
|
||||||
|
|
||||||
|
def trace_calls(self, frame, event, arg):
|
||||||
|
if event != 'call':
|
||||||
|
return
|
||||||
|
# If this is the "string" file (the template), return the trace_lines function
|
||||||
|
if frame.f_code.co_filename == '<string>':
|
||||||
|
return self.trace_lines
|
||||||
|
return None
|
||||||
|
|
||||||
def display_values(self, txt):
|
def display_values(self, txt):
|
||||||
tv = self.template_value
|
tv = self.template_value
|
||||||
@ -787,14 +828,21 @@ def evaluate(book, context):
|
|||||||
w = tv.cellWidget(r, 0)
|
w = tv.cellWidget(r, 0)
|
||||||
w.setText(mi.title)
|
w.setText(mi.title)
|
||||||
w.setCursorPosition(0)
|
w.setCursorPosition(0)
|
||||||
v = SafeFormat().safe_format(txt, mi, _('EXCEPTION:'),
|
if self.break_box.isChecked() and r == break_on_mi and self.is_python:
|
||||||
mi, global_vars=self.global_vars,
|
sys.settrace(self.trace_calls)
|
||||||
template_functions=self.all_functions,
|
else:
|
||||||
break_reporter=self.break_reporter if r == break_on_mi else None,
|
sys.settrace(None)
|
||||||
python_context_object=self.python_context_object)
|
try:
|
||||||
w = tv.cellWidget(r, 1)
|
v = SafeFormat().safe_format(txt, mi, _('EXCEPTION:'),
|
||||||
w.setText(v.translate(translate_table))
|
mi, global_vars=self.global_vars,
|
||||||
w.setCursorPosition(0)
|
template_functions=self.all_functions,
|
||||||
|
break_reporter=self.break_reporter if r == break_on_mi else None,
|
||||||
|
python_context_object=self.python_context_object)
|
||||||
|
w = tv.cellWidget(r, 1)
|
||||||
|
w.setText(v.translate(translate_table))
|
||||||
|
w.setCursorPosition(0)
|
||||||
|
finally:
|
||||||
|
sys.settrace(None)
|
||||||
|
|
||||||
def text_cursor_changed(self):
|
def text_cursor_changed(self):
|
||||||
cursor = self.textbox.textCursor()
|
cursor = self.textbox.textCursor()
|
||||||
@ -896,20 +944,21 @@ class BreakReporterItem(QTableWidgetItem):
|
|||||||
|
|
||||||
def __init__(self, txt):
|
def __init__(self, txt):
|
||||||
super().__init__(txt.translate(translate_table) if txt else txt)
|
super().__init__(txt.translate(translate_table) if txt else txt)
|
||||||
self.setFlags(self.flags() & ~(Qt.ItemFlag.ItemIsEditable|Qt.ItemFlag.ItemIsSelectable))
|
self.setFlags(self.flags() & ~(Qt.ItemFlag.ItemIsEditable))
|
||||||
|
|
||||||
|
|
||||||
class BreakReporter(QDialog):
|
class BreakReporterBase(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent, mi, op_label, op_value, locals_, line_number):
|
def setup_ui(self, mi, line_number, locals_, leading_rows):
|
||||||
super().__init__(parent)
|
|
||||||
self.mi = mi
|
self.mi = mi
|
||||||
|
self.leading_rows = leading_rows
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
l = QVBoxLayout(self)
|
l = QVBoxLayout(self)
|
||||||
t = self.table = QTableWidget(self)
|
t = self.table = QTableWidget(self)
|
||||||
|
t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
t.setColumnCount(2)
|
t.setColumnCount(2)
|
||||||
t.setHorizontalHeaderLabels((_('Name'), _('Value')))
|
t.setHorizontalHeaderLabels((_('Name'), _('Value')))
|
||||||
t.setRowCount(2)
|
t.setRowCount(leading_rows)
|
||||||
l.addWidget(t)
|
l.addWidget(t)
|
||||||
|
|
||||||
self.table_column_widths = None
|
self.table_column_widths = None
|
||||||
@ -935,35 +984,34 @@ class BreakReporter(QDialog):
|
|||||||
bb.accepted.connect(self.accept)
|
bb.accepted.connect(self.accept)
|
||||||
bb.rejected.connect(self.reject)
|
bb.rejected.connect(self.reject)
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
|
|
||||||
self.setWindowTitle(_('Break: line {0}, book {1}').format(line_number, self.mi.title))
|
self.setWindowTitle(_('Break: line {0}, book {1}').format(line_number, self.mi.title))
|
||||||
|
|
||||||
local_names = sorted(locals_.keys())
|
|
||||||
rows = len(local_names)
|
|
||||||
self.table.setRowCount(rows+2)
|
|
||||||
self.table.setItem(0, 0, BreakReporterItem(op_label))
|
|
||||||
self.table.item(0,0).setToolTip(_('The name of the template language operation'))
|
|
||||||
self.table.setItem(0, 1, BreakReporterItem(op_value))
|
|
||||||
|
|
||||||
self.mi_combo = QComboBox()
|
self.mi_combo = QComboBox()
|
||||||
t.setCellWidget(1, 0, self.mi_combo)
|
t.setCellWidget(leading_rows-1, 0, self.mi_combo)
|
||||||
self.mi_combo.addItems(self.get_field_keys())
|
self.mi_combo.addItems(self.get_field_keys())
|
||||||
self.mi_combo.setToolTip('Choose a book metadata field to display')
|
self.mi_combo.setToolTip('Choose a book metadata field to display')
|
||||||
self.mi_combo.setCurrentIndex(-1)
|
|
||||||
self.mi_combo.currentTextChanged.connect(self.get_field_value)
|
self.mi_combo.currentTextChanged.connect(self.get_field_value)
|
||||||
for i,k in enumerate(local_names):
|
self.mi_combo.setCurrentIndex(self.mi_combo.findText('title'))
|
||||||
itm = BreakReporterItem(k)
|
|
||||||
itm.setToolTip(_('A variable in the template'))
|
|
||||||
self.table.setItem(i+2, 0, itm)
|
|
||||||
itm = BreakReporterItem(locals_[k])
|
|
||||||
itm.setToolTip(_('The value of the variable'))
|
|
||||||
self.table.setItem(i+2, 1, itm)
|
|
||||||
|
|
||||||
self.restore_geometry(gprefs, 'template_editor_break_geometry')
|
self.restore_geometry(gprefs, 'template_editor_break_geometry')
|
||||||
|
self.setup_locals(locals_)
|
||||||
|
|
||||||
|
def setup_locals(self, locals_):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_local_line(self, locals, row, key):
|
||||||
|
itm = BreakReporterItem(key)
|
||||||
|
itm.setToolTip(_('A variable in the template'))
|
||||||
|
self.table.setItem(row, 0, itm)
|
||||||
|
itm = BreakReporterItem(repr(locals[key]))
|
||||||
|
itm.setToolTip(_('The value of the variable'))
|
||||||
|
self.table.setItem(row, 1, itm)
|
||||||
|
|
||||||
def get_field_value(self, field):
|
def get_field_value(self, field):
|
||||||
val = self.mi.format_field('timestamp' if field == 'date' else field)[1]
|
val = self.displayable_field_value(self.mi, field)
|
||||||
self.table.setItem(1, 1, BreakReporterItem(val))
|
self.table.setItem(self.leading_rows-1, 1, BreakReporterItem(val))
|
||||||
|
|
||||||
|
def displayable_field_value(self, mi, field):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def table_column_resized(self, col, old, new):
|
def table_column_resized(self, col, old, new):
|
||||||
self.table_column_widths = []
|
self.table_column_widths = []
|
||||||
@ -992,6 +1040,49 @@ class BreakReporter(QDialog):
|
|||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
|
|
||||||
|
class BreakReporter(BreakReporterBase):
|
||||||
|
|
||||||
|
def __init__(self, parent, mi, op_label, op_value, locals_, line_number):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui(mi, line_number, locals_, leading_rows=2)
|
||||||
|
self.table.setItem(0, 0, BreakReporterItem(op_label))
|
||||||
|
self.table.item(0,0).setToolTip(_('The name of the template language operation'))
|
||||||
|
self.table.setItem(0, 1, BreakReporterItem(op_value))
|
||||||
|
|
||||||
|
def setup_locals(self, locals):
|
||||||
|
local_names = sorted(locals.keys())
|
||||||
|
rows = len(local_names)
|
||||||
|
self.table.setRowCount(rows+2)
|
||||||
|
for i,k in enumerate(local_names, start=2):
|
||||||
|
self.add_local_line(locals, i, k)
|
||||||
|
|
||||||
|
def displayable_field_value(self, mi, field):
|
||||||
|
return self.mi.format_field('timestamp' if field == 'date' else field)[1]
|
||||||
|
|
||||||
|
|
||||||
|
class PythonBreakReporter(BreakReporterBase):
|
||||||
|
|
||||||
|
def __init__(self, parent, mi, frame):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.frame = frame
|
||||||
|
line_number = frame.f_lineno
|
||||||
|
locals = frame.f_locals
|
||||||
|
self.setup_ui(mi, line_number, locals, leading_rows=1)
|
||||||
|
|
||||||
|
def setup_locals(self, locals):
|
||||||
|
locals = self.frame.f_locals
|
||||||
|
local_names = sorted(k for k in locals.keys() if k not in ('book', 'context'))
|
||||||
|
rows = len(local_names)
|
||||||
|
self.table.setRowCount(rows+1)
|
||||||
|
|
||||||
|
for i,k in enumerate(local_names, start=1):
|
||||||
|
if k in ('book', 'context'): continue
|
||||||
|
self.add_local_line(locals, i, k)
|
||||||
|
|
||||||
|
def displayable_field_value(self, mi, field):
|
||||||
|
return repr(self.mi.get('timestamp' if field == 'date' else field))
|
||||||
|
|
||||||
|
|
||||||
class EmbeddedTemplateDialog(TemplateDialog):
|
class EmbeddedTemplateDialog(TemplateDialog):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
|
@ -1231,10 +1231,12 @@ class _Interpreter:
|
|||||||
if (self.break_reporter):
|
if (self.break_reporter):
|
||||||
self.break_reporter(prog.node_name, res, prog.line_number)
|
self.break_reporter(prog.node_name, res, prog.line_number)
|
||||||
return res
|
return res
|
||||||
|
except StopException:
|
||||||
|
raise
|
||||||
except:
|
except:
|
||||||
self.error(_("Unknown field '{0}'").format(name), prog.line_number)
|
self.error(_("Unknown field '{0}'").format(name), prog.line_number)
|
||||||
except (StopException, ValueError) as e:
|
except (StopException, ValueError):
|
||||||
raise e
|
raise
|
||||||
except:
|
except:
|
||||||
self.error(_("Unknown field '{0}'").format('internal parse error'),
|
self.error(_("Unknown field '{0}'").format('internal parse error'),
|
||||||
prog.line_number)
|
prog.line_number)
|
||||||
@ -1690,6 +1692,8 @@ class TemplateFormatter(string.Formatter):
|
|||||||
formatter=self,
|
formatter=self,
|
||||||
funcs=self._caller)
|
funcs=self._caller)
|
||||||
rslt = compiled_template(self.book, self.python_context_object)
|
rslt = compiled_template(self.book, self.python_context_object)
|
||||||
|
except StopException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
stack = traceback.extract_tb(exc_info()[2])
|
stack = traceback.extract_tb(exc_info()[2])
|
||||||
ss = stack[-1]
|
ss = stack[-1]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user