From b17ff26950da1b0da5b05650242ead016bd4213f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 18 Dec 2014 18:43:03 +0530 Subject: [PATCH] Some work on completion for the editor --- .../gui2/tweak_book/completion/basic.py | 132 ++++++++++++++++++ .../gui2/tweak_book/completion/worker.py | 56 +++++++- 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/tweak_book/completion/basic.py diff --git a/src/calibre/gui2/tweak_book/completion/basic.py b/src/calibre/gui2/tweak_book/completion/basic.py new file mode 100644 index 0000000000..15330e9708 --- /dev/null +++ b/src/calibre/gui2/tweak_book/completion/basic.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2014, Kovid Goyal ' + +from threading import Event +from collections import namedtuple + +from PyQt5.Qt import QObject, pyqtSignal, Qt + +from calibre.ebooks.oeb.polish.container import OEB_STYLES, OEB_FONTS +from calibre.gui2 import is_gui_thread +from calibre.gui2.tweak_book import current_container +from calibre.utils.ipc import eintr_retry_call +from calibre.utils.matcher import Matcher + +Request = namedtuple('Request', 'id type data query') + +names_cache = frozenset() + +def control(func): + func.function_type = 'control' + return func + +def data(func): + func.function_type = 'data' + return func + +@control +def clear_caches(cache_type, data_conn): + global names_cache + if cache_type is None or cache_type == 'names': + names_cache = frozenset() + +@data +def names_data(request_data): + c = current_container() + return c.mime_map, {n for n, is_linear in c.spine_names} + +class DataError(Exception): + + def __init__(self, tb, msg=None): + Exception.__init__(self, msg or _('Failed to get completion data')) + self.tb = tb + +def get_data(data_conn, data_type, data=None): + eintr_retry_call(data_conn.send, Request(None, data_type, data, None)) + result, tb = eintr_retry_call(data_conn.recv) + if tb: + raise DataError(tb) + return result + +class Name(unicode): + + def __new__(self, name, mime_type, spine_names): + ans = unicode.__new__(self, name) + ans.mime_type = mime_type + ans.in_spine = name in spine_names + return ans + +@control +def complete_names(names_type, data_conn): + global names_cache + if not names_cache: + mime_map, spine_names = get_data(data_conn, 'names_data') + names_cache = frozenset(Name(name, mt, spine_names) for name, mt in mime_map.iteritems()) + ans = names_cache + if names_type == 'text_link': + ans = frozenset(n for n in names_cache if n.in_spine) + elif names_type == 'stylesheet': + ans = frozenset(n for n in names_cache if n.mime_type in OEB_STYLES) + elif names_type == 'image': + ans = frozenset(n for n in names_cache if n.mime_type.startswith('image/')) + elif names_type == 'font': + ans = frozenset(n for n in names_cache if n.mime_type in OEB_FONTS) + return ans, {} + +_current_matcher = (None, None, None) + +def handle_control_request(request, data_conn): + global _current_matcher + ans = control_funcs[request.type](request.data, data_conn) + if ans is not None: + items, matcher_kwargs = ans + fingerprint = hash(items) + if fingerprint != _current_matcher[0] or matcher_kwargs != _current_matcher[1]: + _current_matcher = (fingerprint, matcher_kwargs, Matcher(items, **matcher_kwargs)) + ans = _current_matcher[-1](request.query or '', limit=50) + return ans + +class HandleDataRequest(QObject): + + # Ensure data is obtained in the GUI thread + + call = pyqtSignal(object, object, object) + + def __init__(self): + QObject.__init__(self) + self.called = Event() + self.call.connect(self.run_func, Qt.QueuedConnection) + + def run_func(self, func, data): + try: + self.result, self.tb = func(data), None + except Exception: + import traceback + self.result, self.tb = None, traceback.format_exc() + finally: + self.called.set() + + def __call__(self, request): + func = data_funcs[request.type] + if is_gui_thread(): + try: + return func(request.data), None + except Exception: + import traceback + return None, traceback.format_exc() + self.called.clear() + self.call.emit(func, request.data) + self.called.wait() + try: + return self.result, self.tb + finally: + del self.result, self.tb +handle_data_request = HandleDataRequest() + +control_funcs = {name:func for name, func in globals().iteritems() if getattr(func, 'function_type', None) == 'control'} +data_funcs = {name:func for name, func in globals().iteritems() if getattr(func, 'function_type', None) == 'data'} diff --git a/src/calibre/gui2/tweak_book/completion/worker.py b/src/calibre/gui2/tweak_book/completion/worker.py index 794346b743..b8d17e273f 100644 --- a/src/calibre/gui2/tweak_book/completion/worker.py +++ b/src/calibre/gui2/tweak_book/completion/worker.py @@ -10,6 +10,7 @@ import cPickle, os, sys from threading import Thread, Event from Queue import Queue from contextlib import closing +from collections import namedtuple from calibre.constants import iswindows from calibre.utils.ipc import eintr_retry_call @@ -38,6 +39,9 @@ class CompletionWorker(Thread): p.stdin.flush(), p.stdin.close() self.control_conn = eintr_retry_call(self.listener.accept) self.data_conn = eintr_retry_call(self.listener.accept) + self.data_thread = t = Thread(name='CWData', target=self.handle_data_requests) + t.daemon = True + t.start() self.connected.set() def send(self, data, conn=None): @@ -59,6 +63,29 @@ class CompletionWorker(Thread): def wait_for_connection(self, timeout=None): self.connected.wait(timeout) + def handle_data_requests(self): + from calibre.gui2.tweak_book.completion.basic import handle_data_request + while True: + try: + req = self.recv(self.data_conn) + except EOFError: + break + except Exception: + import traceback + traceback.print_exc() + break + if req is None or self.shutting_down: + break + result, tb = handle_data_request(req) + try: + self.send((result, tb), self.data_conn) + except EOFError: + break + except Exception: + import traceback + traceback.print_exc() + break + def run(self): self.launch_worker_process() while True: @@ -69,8 +96,13 @@ class CompletionWorker(Thread): def shutdown(self): self.shutting_down = True self.main_queue.put(None) + for conn in (self.control_conn, self.data_conn): + try: + conn.close() + except Exception: + pass p = self.worker_process - if p.returncode is None: + if p.poll() is None: self.worker_process.terminate() t = self.reap_thread = Thread(target=p.wait) t.daemon = True @@ -96,6 +128,28 @@ def run_main(func): with closing(Client(address, authkey=key)) as control_conn, closing(Client(address, authkey=key)) as data_conn: func(control_conn, data_conn) +Result = namedtuple('Result', 'request_id ans traceback') + +def main(control_conn, data_conn): + from calibre.gui2.tweak_book.completion.basic import handle_control_request + while True: + try: + request = eintr_retry_call(control_conn.recv) + except EOFError: + break + if request is None: + break + try: + ans, tb = handle_control_request(request, data_conn), None + except Exception: + import traceback + ans, tb = None, traceback.format_exc() + result = Result(request.id, ans, tb) + try: + eintr_retry_call(control_conn.send, result) + except EOFError: + break + def test_main(control_conn, data_conn): obj = control_conn.recv() control_conn.send(obj)