diff --git a/src/calibre/gui2/tweak_book/function_replace.py b/src/calibre/gui2/tweak_book/function_replace.py index a94b5ebb79..29abcbdb92 100644 --- a/src/calibre/gui2/tweak_book/function_replace.py +++ b/src/calibre/gui2/tweak_book/function_replace.py @@ -6,11 +6,12 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -import re, io, weakref +import re, io, weakref, sys +from cStringIO import StringIO from PyQt5.Qt import ( pyqtSignal, QVBoxLayout, QHBoxLayout, QPlainTextEdit, QLabel, QFontMetrics, - QSize) + QSize, Qt) from calibre.ebooks.oeb.polish.utils import apply_func_to_match_groups from calibre.gui2 import error_dialog @@ -24,7 +25,7 @@ from calibre.utils.titlecase import titlecase user_functions = JSONConfig('editor-search-replace-functions') -def compile_code(src): +def compile_code(src, name=''): if not isinstance(src, unicode): match = re.search(r'coding[:=]\s*([-\w.]+)', src[:200]) enc = match.group(1) if match else 'utf-8' @@ -35,9 +36,10 @@ def compile_code(src): src = re.sub(r'^#.*coding\s*[:=]\s*([-\w.]+)', '#', src, flags=re.MULTILINE) # Translate newlines to \n src = io.StringIO(src, newline=None).getvalue() + code = compile(src, name, 'exec') namespace = {} - exec src in namespace + exec code in namespace return namespace class Function(object): @@ -47,7 +49,7 @@ class Function(object): self.is_builtin = source is None self.name = name if func is None: - self.mod = compile_code(source) + self.mod = compile_code(source, name) self.func = self.mod['replace'] else: self.func = func @@ -61,6 +63,7 @@ class Function(object): self.match_index = 0 self.boss = get_boss() self.data = {} + self.debug_buf = StringIO() def __hash__(self): return hash(self.name) @@ -73,7 +76,11 @@ class Function(object): def __call__(self, match): self.match_index += 1 - return self.func(match, self.match_index, self.context_name, self.boss.current_metadata, dictionaries, self.data, functions()) + oo, oe, sys.stdout, sys.stderr = sys.stdout, sys.stderr, self.debug_buf, self.debug_buf + try: + return self.func(match, self.match_index, self.context_name, self.boss.current_metadata, dictionaries, self.data, functions()) + finally: + sys.stdout, sys.stderr = oo, oe @property def source(self): @@ -82,6 +89,29 @@ class Function(object): return json.loads(P('editor-functions.json', data=True, allow_user_override=False))[self.name] return self._source +class DebugOutput(Dialog): + + def __init__(self, parent=None): + Dialog.__init__(self, 'Debug output', 'sr-function-debug-output') + self.setAttribute(Qt.WA_DeleteOnClose, False) + + def setup_ui(self): + self.l = l = QVBoxLayout(self) + self.text = t = QPlainTextEdit(self) + l.addWidget(t) + l.addWidget(self.bb) + self.bb.setStandardButtons(self.bb.Close) + + def show_log(self, name, text): + self.setWindowTitle(_('Debug output from %s') % name) + self.text.setPlainText(self.windowTitle() + '\n\n' + text) + self.show() + self.raise_() + + def sizeHint(self): + fm = QFontMetrics(self.text.font()) + return QSize(fm.averageCharWidth() * 120, 400) + def builtin_functions(): for name, obj in globals().iteritems(): if name.startswith('replace_') and callable(obj): @@ -183,7 +213,7 @@ class FunctionEditor(Dialog): self.la2 = la = QLabel(_( 'For help with creating functions, see the User Manual') % - 'http://manual.calibre-ebook.com/edit.html#function-mode') + 'http://manual.calibre-ebook.com/function_mode.html') la.setOpenExternalLinks(True) l.addWidget(la) @@ -207,7 +237,7 @@ class FunctionEditor(Dialog): 'You must specify a name for this function.'), show=True) source = self.source try: - mod = compile_code(source) + mod = compile_code(source, self.func_name) except Exception as err: return error_dialog(self, _('Invalid python code'), _( 'The code you created is not valid python code, with error: %s') % err, show=True) diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index f6e1f3fd3d..3e5d9ba93f 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -1185,6 +1185,13 @@ def get_search_function(search): raise NoSuchFunction(ans) return ans +def show_function_debug_output(func): + if isinstance(func, Function): + val = func.debug_buf.getvalue().strip() + if val: + from calibre.gui2.tweak_book.boss import get_boss + get_boss().gui.sr_debug_output.show_log(func.name, val) + def run_search( searches, action, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file, show_current_diff, add_savepoint, rewind_savepoint, set_modified): @@ -1258,6 +1265,7 @@ def run_search( if callable(repl): repl.init_env(current_editor_name) if editor.replace(p, repl, saved_match='gui'): + show_function_debug_output(repl) return True return no_replace(_( 'Currently selected text does not match the search query.')) @@ -1302,6 +1310,7 @@ def run_search( else: num = len(p.findall(raw)) count += num + show_function_debug_output(repl) for n in updates: raw = raw_data[n] diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 924bab7d8b..d2ab26b1aa 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -39,6 +39,7 @@ from calibre.gui2.tweak_book.toc import TOCViewer from calibre.gui2.tweak_book.char_select import CharSelect from calibre.gui2.tweak_book.live_css import LiveCSS from calibre.gui2.tweak_book.manage_fonts import ManageFonts +from calibre.gui2.tweak_book.function_replace import DebugOutput from calibre.gui2.tweak_book.editor.widget import register_text_editor_actions from calibre.gui2.tweak_book.editor.insert_resource import InsertImage from calibre.utils.icu import character_name, sort_key @@ -238,6 +239,7 @@ class Main(MainWindow): self.image_browser = InsertImage(self, for_browsing=True) self.insert_char = CharSelect(self) self.manage_fonts = ManageFonts(self) + self.sr_debug_output = DebugOutput(self) self.create_actions() self.create_toolbars()