diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index b3167cc7c9..45784fc833 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -7,6 +7,7 @@ __license__ = 'GPL v3' import json, os, traceback, re from functools import partial +import sys from qt.core import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, QFont, QApplication, QTextCharFormat, QColor, QCursor, @@ -389,6 +390,18 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): self.builtins = (builtin_functions if builtin_functions else 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 self.table_column_widths = None try: @@ -451,16 +464,6 @@ class TemplateDialog(QDialog, Ui_TemplateDialog): '{}'.format( 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.set_up_font_boxes() 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)): w = QLineEdit(tv) w.setReadOnly(True) + w.setText(mi[r].title) tv.setCellWidget(r, 0, w) w = QLineEdit(tv) w.setReadOnly(True) 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): m = self.textbox.createStandardContextMenu() @@ -678,6 +689,8 @@ def evaluate(book, context): self.breakpoint_line_box_label.setEnabled(new_state != 0) if new_state == 0: self.display_values(str(self.textbox.toPlainText())) + else: + self.set_waiting_message() def go_button_pressed(self): self.display_values(str(self.textbox.toPlainText())) @@ -760,14 +773,17 @@ def evaluate(book, context): c = app.clipboard() c.setText(str(self.icon_files.currentText())) + @property + def is_python(self): + return self.textbox.toPlainText().startswith('python:') + def textbox_changed(self): cur_text = str(self.textbox.toPlainText()) - if cur_text.startswith('python:'): + if self.is_python: if self.highlighting_gpm is True: self.highlighter.initialize_rules(self.builtins, True) self.highlighting_gpm = False - self.break_box.setChecked(False) - self.break_box.setEnabled(False) + self.break_box.setEnabled(True) elif not self.highlighting_gpm: self.highlighter.initialize_rules(self.builtins, False) self.highlighting_gpm = True @@ -776,8 +792,33 @@ def evaluate(book, context): self.last_text = cur_text self.highlighter.regenerate_paren_positions() self.text_cursor_changed() - if self.break_box.checkState() == Qt.CheckState.Unchecked: + if not self.break_box.isChecked(): 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 != '': + 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 == '': + return self.trace_lines + return None def display_values(self, txt): tv = self.template_value @@ -787,14 +828,21 @@ def evaluate(book, context): w = tv.cellWidget(r, 0) w.setText(mi.title) w.setCursorPosition(0) - v = SafeFormat().safe_format(txt, mi, _('EXCEPTION:'), - mi, global_vars=self.global_vars, - 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) + if self.break_box.isChecked() and r == break_on_mi and self.is_python: + sys.settrace(self.trace_calls) + else: + sys.settrace(None) + try: + v = SafeFormat().safe_format(txt, mi, _('EXCEPTION:'), + mi, global_vars=self.global_vars, + 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): cursor = self.textbox.textCursor() @@ -896,20 +944,21 @@ class BreakReporterItem(QTableWidgetItem): def __init__(self, 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): - super().__init__(parent) + def setup_ui(self, mi, line_number, locals_, leading_rows): self.mi = mi + self.leading_rows = leading_rows self.setModal(True) l = QVBoxLayout(self) t = self.table = QTableWidget(self) + t.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) t.setColumnCount(2) t.setHorizontalHeaderLabels((_('Name'), _('Value'))) - t.setRowCount(2) + t.setRowCount(leading_rows) l.addWidget(t) self.table_column_widths = None @@ -935,35 +984,34 @@ class BreakReporter(QDialog): bb.accepted.connect(self.accept) bb.rejected.connect(self.reject) self.setLayout(l) - 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() - 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.setToolTip('Choose a book metadata field to display') - self.mi_combo.setCurrentIndex(-1) self.mi_combo.currentTextChanged.connect(self.get_field_value) - for i,k in enumerate(local_names): - 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.mi_combo.setCurrentIndex(self.mi_combo.findText('title')) 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): - val = self.mi.format_field('timestamp' if field == 'date' else field)[1] - self.table.setItem(1, 1, BreakReporterItem(val)) + val = self.displayable_field_value(self.mi, field) + 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): self.table_column_widths = [] @@ -992,6 +1040,49 @@ class BreakReporter(QDialog): 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): def __init__(self, parent): diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 9c43746e3f..473e4624d4 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -1231,10 +1231,12 @@ class _Interpreter: if (self.break_reporter): self.break_reporter(prog.node_name, res, prog.line_number) return res + except StopException: + raise except: self.error(_("Unknown field '{0}'").format(name), prog.line_number) - except (StopException, ValueError) as e: - raise e + except (StopException, ValueError): + raise except: self.error(_("Unknown field '{0}'").format('internal parse error'), prog.line_number) @@ -1690,6 +1692,8 @@ class TemplateFormatter(string.Formatter): formatter=self, funcs=self._caller) rslt = compiled_template(self.book, self.python_context_object) + except StopException: + raise except Exception as e: stack = traceback.extract_tb(exc_info()[2]) ss = stack[-1]