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]