refactoring

This commit is contained in:
St.Huang 2018-07-14 16:16:43 +08:00
parent 4124b30954
commit 442f4b7ad3
4 changed files with 502 additions and 423 deletions

View File

@ -1,423 +0,0 @@
#-*- coding:utf-8 -*-
#
# Copyright © 20162017 sthoo <sth201807@gmail.com>
#
# Support: Report an issue at https://github.com/sth2018/FastWordQuery/issues
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version; http://www.gnu.org/copyleft/gpl.html.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict
import os
import shutil
import unicodedata
from aqt import mw
from aqt.qt import QFileDialog, QObject, QThread, pyqtSignal, pyqtSlot, QMutex
from aqt.utils import showInfo, showText, tooltip
from .constants import Endpoint, Template
from .context import config
from .lang import _
from .gui import ProgressWindow
from .service import service_manager, service_pool, QueryResult, copy_static_file
from .service.base import LocalService
from .utils import Empty, MapDict, Queue, wrap_css
__all__ = ['query_from_browser', 'query_from_editor_all_fields']
def inspect_note(note):
"""
inspect the note, and get necessary input parameters
return word_ord: field index of the word in current note
return word: the word
return maps: dicts map of current note
"""
maps = config.get_maps(note.model()['id'])
for i, m in enumerate(maps):
if m.get('word_checked', False):
word_ord = i
break
else:
# if no field is checked to be the word field, default the
# first one.
word_ord = 0
def purify_word(word):
return word.strip() if word else ''
word = purify_word(note.fields[word_ord])
return word_ord, word, maps
class QueryThread(QThread):
"""
Query Worker Thread
"""
note_flush = pyqtSignal(object)
def __init__(self, manager):
super(QueryThread, self).__init__()
self.index = 0
self.exit = False
self.finished = False
self.manager = manager
self.note_flush.connect(manager.handle_flush)
def run(self):
while True:
if self.exit or not self.manager:
break
try:
note = self.manager.queue.get(True, timeout=0.1)
except Empty:
self.exit = True
continue
try:
results, success_num, missed_css = query_all_flds(note)
if not self.exit and self.manager:
if self.manager.update(note, results, success_num, missed_css):
self.note_flush.emit(note)
except InvalidWordException:
# only show error info on single query
if self.manager.total == 1:
showInfo(_("NO_QUERY_WORD"))
if self.manager:
self.manager.queue.task_done()
self.finished = True
class QueryWorkerManager(object):
"""
Query Worker Thread Manager
"""
def __init__(self):
self.workers = []
self.queue = Queue()
self.mutex = QMutex()
self.progress = ProgressWindow(mw)
self.total = 0
self.counter = 0
self.fails = 0
self.fields = 0
self.skips = 0
self.missed_css = list()
def get_worker(self):
worker = QueryThread(self)
worker.index = len(self.workers) + 1
self.workers.append(worker)
return worker
def start(self):
self.total = self.queue.qsize()
self.progress.start(self.total, min=0)
if self.total > 1:
for x in range(0, min(config.thread_number, self.total)):
self.get_worker()
for worker in self.workers:
worker.start()
else:
worker = self.get_worker()
worker.run()
def update(self, note, results, success_num, missed_css):
self.mutex.lock()
if success_num > 0:
self.counter += 1
elif success_num == 0:
self.fails += 1
else:
self.skips += 1
val = update_note_fields(note, results)
self.fields += val
self.missed_css += missed_css
self.mutex.unlock()
if self.total > 1:
return val > 0
else:
self.handle_flush(note)
return False
def join(self):
for worker in self.workers:
while not worker.finished:
if self.progress.abort():
worker.exit = True
break
else:
self.progress.update_labels(MapDict(
type='count',
words_number = self.counter,
skips_number = self.skips,
fails_number = self.fails,
fields_number = self.fields))
mw.app.processEvents()
worker.wait(100)
self.progress.finish()
@pyqtSlot(object)
def handle_flush(self, note):
if note:
note.flush()
def query_from_browser(browser):
"""
Query word from Browser
"""
if not browser:
return
notes = [browser.mw.col.getNote(note_id)
for note_id in browser.selectedNotes()]
if len(notes) == 1:
query_from_editor_all_fields(browser.editor)
else:
query_all(notes)
# browser.model.reset()
def query_from_editor_all_fields(editor):
"""
Query word fileds from Editor
"""
if not editor or not editor.note:
return
maps = config.get_maps(editor.note.model()['id'])
nomaps = True
for each in maps:
dict_unique = each.get('dict_unique', '').strip()
ignore = each.get('ignore', True)
if dict_unique and not ignore:
nomaps = False
break
if nomaps:
from .ui import show_options
tooltip(_('PLS_SET_DICTIONARY_FIELDS'))
show_options(
editor.parentWindow,
editor.note.model()['id'],
query_from_editor_all_fields,
editor
)
else:
query_all([editor.note])
editor.setNote(editor.note, focus=True)
editor.saveNow()
def query_all(notes):
"""
Query maps word fileds
"""
if len(notes) == 0:
return
work_manager = QueryWorkerManager()
#work_manager.reset()
#progress.start(max=len(notes), min=0, immediate=True)
queue = work_manager.queue
for i, note in enumerate(notes):
queue.put(note)
work_manager.start()
work_manager.join()
#progress.finish()
promot_choose_css(work_manager.missed_css)
tooltip(u'{0} {1} {2}, {3} {4}'.format(_('UPDATED'), work_manager.counter, _('CARDS'), work_manager.fields, _('FIELDS')))
#work_manager.clean()
service_pool.clean()
def update_note_fields(note, results):
"""
Update query result to note fields, return updated fields count.
"""
if not results or not note or len(results) == 0:
return 0
count = 0
for i, q in results.items():
if isinstance(q, QueryResult) and i < len(note.fields):
count += update_note_field(note, i, q)
return count
def update_note_field(note, fld_index, fld_result):
"""
Update single field, if result is valid then return 1, else return 0
"""
result, js, jsfile = fld_result.result, fld_result.js, fld_result.jsfile
# js process: add to template of the note model
add_to_tmpl(note, js=js, jsfile=jsfile)
# if not result:
# return
if not config.force_update and not result:
return 0
value = result if result else ''
if note.fields[fld_index] != value:
note.fields[fld_index] = value
return 1
return 0
def promot_choose_css(missed_css):
'''
Choose missed css file and copy to user folder
'''
checked = set()
for css in missed_css:
filename = u'_' + css['file']
if not os.path.exists(filename) and not css['file'] in checked:
checked.add(css['file'])
showInfo(
Template.miss_css.format(
dict = css['title'],
css = css['file']
)
)
try:
filepath = css['dict_path'][:css['dict_path'].rindex(os.path.sep)+1]
filepath = QFileDialog.getOpenFileName(
directory = filepath,
caption = u'Choose css file',
filter = u'CSS (*.css)'
)
if filepath:
shutil.copy(filepath, filename)
wrap_css(filename)
except KeyError:
pass
def add_to_tmpl(note, **kwargs):
# templates
'''
[{u'name': u'Card 1', u'qfmt': u'{{Front}}\n\n', u'did': None, u'bafmt': u'',
u'afmt': u'{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}\n\n{{12}}\n\n{{44}}\n\n', u'ord': 0, u'bqfmt': u''}]
'''
# showInfo(str(kwargs))
afmt = note.model()['tmpls'][0]['afmt']
if kwargs:
jsfile, js = kwargs.get('jsfile', None), kwargs.get('js', None)
if js and js.strip():
addings = js.strip()
if addings not in afmt:
if not addings.startswith(u'<script') and not addings.endswith(u'/script>'):
addings = u'\r\n<script>{}</script>'.format(addings)
afmt += addings
if jsfile:
new_jsfile = u'_' + \
jsfile if not jsfile.startswith(u'_') else jsfile
copy_static_file(jsfile, new_jsfile)
addings = u'\r\n<script src="{}"></script>'.format(new_jsfile)
afmt += addings
note.model()['tmpls'][0]['afmt'] = afmt
class InvalidWordException(Exception):
"""Invalid word exception"""
def strip_combining(txt):
"Return txt with all combining characters removed."
norm = unicodedata.normalize('NFKD', txt)
return u"".join([c for c in norm if not unicodedata.combining(c)])
def query_all_flds(note):
"""
Query all fields of single note
"""
word_ord, word, maps = inspect_note(note)
if not word:
raise InvalidWordException
if config.ignore_accents:
word = strip_combining(unicode(word))
# progress.update_title(u'Querying [[ %s ]]' % word)
services = {}
tasks = []
for i, each in enumerate(maps):
if i == word_ord:
continue
if i == len(note.fields):
break
#ignore field
ignore = each.get('ignore', False)
if ignore:
continue
#skip valued
skip = each.get('skip_valued', False)
if skip and len(note.fields[i]) != 0:
continue
#normal
dict_unique = each.get('dict_unique', '').strip()
dict_fld_ord = each.get('dict_fld_ord', -1)
fld_ord = each.get('fld_ord', -1)
if dict_unique and dict_fld_ord != -1 and fld_ord != -1:
s = services.get(dict_unique, None)
if s is None:
s = service_pool.get(dict_unique)
if s and s.support:
services[dict_unique] = s
if s and s.support:
tasks.append({'k': dict_unique, 'w': word, 'f': dict_fld_ord, 'i': fld_ord})
success_num = 0
result = defaultdict(QueryResult)
for task in tasks:
#try:
service = services.get(task['k'], None)
qr = service.active(task['f'], task['w'])
if qr:
result.update({task['i']: qr})
success_num += 1
#except:
# showInfo(_("NO_QUERY_WORD"))
# pass
missed_css = list()
for service in services.values():
if isinstance(service, LocalService):
for css in service.missed_css:
missed_css.append({
'dict_path': service.dict_path,
'title': service.title,
'file': css
})
service_pool.put(service)
return result, -1 if len(tasks) == 0 else success_num, missed_css

View File

@ -0,0 +1,116 @@
#-*- coding:utf-8 -*-
#
# Copyright © 20162017 sthoo <sth201807@gmail.com>
#
# Support: Report an issue at https://github.com/sth2018/FastWordQuery/issues
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version; http://www.gnu.org/copyleft/gpl.html.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict
import os
import shutil
import unicodedata
from aqt import mw
from aqt.utils import showInfo, showText, tooltip
from .worker import QueryWorkerManager
from .common import promot_choose_css
from ..constants import Endpoint, Template
from ..context import config
from ..lang import _
from ..gui import ProgressWindow
from ..service import service_manager, service_pool, QueryResult, copy_static_file
from ..service.base import LocalService
from ..utils import Empty, MapDict, Queue, wrap_css
__all__ = ['query_from_browser', 'query_from_editor_all_fields']
def query_from_browser(browser):
"""
Query word from Browser
"""
if not browser:
return
notes = [browser.mw.col.getNote(note_id)
for note_id in browser.selectedNotes()]
if len(notes) == 1:
query_from_editor_all_fields(browser.editor)
else:
query_all(notes)
# browser.model.reset()
def query_from_editor_all_fields(editor):
"""
Query word fileds from Editor
"""
if not editor or not editor.note:
return
maps = config.get_maps(editor.note.model()['id'])
nomaps = True
for each in maps:
dict_unique = each.get('dict_unique', '').strip()
ignore = each.get('ignore', True)
if dict_unique and not ignore:
nomaps = False
break
if nomaps:
from ..gui import show_options
tooltip(_('PLS_SET_DICTIONARY_FIELDS'))
show_options(
editor.parentWindow,
editor.note.model()['id'],
query_from_editor_all_fields,
editor
)
else:
query_all([editor.note])
editor.setNote(editor.note, focus=True)
editor.saveNow()
def query_all(notes):
"""
Query maps word fileds
"""
if len(notes) == 0:
return
work_manager = QueryWorkerManager()
#work_manager.reset()
#progress.start(max=len(notes), min=0, immediate=True)
queue = work_manager.queue
for note in notes:
queue.put(note)
work_manager.start()
work_manager.join()
#progress.finish()
promot_choose_css(work_manager.missed_css)
tooltip(u'{0} {1} {2}, {3} {4}'.format(_('UPDATED'), work_manager.counter, _(
'CARDS'), work_manager.fields, _('FIELDS')))
#work_manager.clean()
service_pool.clean()

235
src/fastwq/query/common.py Normal file
View File

@ -0,0 +1,235 @@
#-*- coding:utf-8 -*-
#
# Copyright © 20162017 sthoo <sth201807@gmail.com>
#
# Support: Report an issue at https://github.com/sth2018/FastWordQuery/issues
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version; http://www.gnu.org/copyleft/gpl.html.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict
import os
import shutil
import unicodedata
from PyQt4 import QtGui
from aqt.utils import showInfo
from ..constants import Template
from ..context import config
from ..service import service_pool, QueryResult, copy_static_file
from ..service.base import LocalService
from ..utils import wrap_css
__all__ = [
'InvalidWordException', 'update_note_fields',
'update_note_field', 'promot_choose_css', 'add_to_tmpl',
'query_all_flds'
]
class InvalidWordException(Exception):
"""Invalid word exception"""
def inspect_note(note):
"""
inspect the note, and get necessary input parameters
return word_ord: field index of the word in current note
return word: the word
return maps: dicts map of current note
"""
maps = config.get_maps(note.model()['id'])
for i, m in enumerate(maps):
if m.get('word_checked', False):
word_ord = i
break
else:
# if no field is checked to be the word field, default the
# first one.
word_ord = 0
def purify_word(word):
return word.strip() if word else ''
word = purify_word(note.fields[word_ord])
return word_ord, word, maps
def strip_combining(txt):
"Return txt with all combining characters removed."
norm = unicodedata.normalize('NFKD', txt)
return u"".join([c for c in norm if not unicodedata.combining(c)])
def update_note_fields(note, results):
"""
Update query result to note fields, return updated fields count.
"""
if not results or not note or len(results) == 0:
return 0
count = 0
for i, q in results.items():
if isinstance(q, QueryResult) and i < len(note.fields):
count += update_note_field(note, i, q)
return count
def update_note_field(note, fld_index, fld_result):
"""
Update single field, if result is valid then return 1, else return 0
"""
result, js, jsfile = fld_result.result, fld_result.js, fld_result.jsfile
# js process: add to template of the note model
add_to_tmpl(note, js=js, jsfile=jsfile)
# if not result:
# return
if not config.force_update and not result:
return 0
value = result if result else ''
if note.fields[fld_index] != value:
note.fields[fld_index] = value
return 1
return 0
def promot_choose_css(missed_css):
'''
Choose missed css file and copy to user folder
'''
checked = set()
for css in missed_css:
filename = u'_' + css['file']
if not os.path.exists(filename) and not css['file'] in checked:
checked.add(css['file'])
showInfo(
Template.miss_css.format(
dict=css['title'],
css=css['file']
)
)
try:
filepath = css['dict_path'][:css['dict_path'].rindex(
os.path.sep)+1]
filepath = QtGui.QFileDialog.getOpenFileName(
directory=filepath,
caption=u'Choose css file',
filter=u'CSS (*.css)'
)
if filepath:
shutil.copy(filepath, filename)
wrap_css(filename)
except KeyError:
pass
def add_to_tmpl(note, **kwargs):
# templates
'''
[{u'name': u'Card 1', u'qfmt': u'{{Front}}\n\n', u'did': None, u'bafmt': u'',
u'afmt': u'{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}\n\n{{12}}\n\n{{44}}\n\n', u'ord': 0, u'bqfmt': u''}]
'''
# showInfo(str(kwargs))
afmt = note.model()['tmpls'][0]['afmt']
if kwargs:
jsfile, js = kwargs.get('jsfile', None), kwargs.get('js', None)
if js and js.strip():
addings = js.strip()
if addings not in afmt:
if not addings.startswith(u'<script') and not addings.endswith(u'/script>'):
addings = u'\r\n<script>{}</script>'.format(addings)
afmt += addings
if jsfile:
new_jsfile = u'_' + \
jsfile if not jsfile.startswith(u'_') else jsfile
copy_static_file(jsfile, new_jsfile)
addings = u'\r\n<script src="{}"></script>'.format(new_jsfile)
afmt += addings
note.model()['tmpls'][0]['afmt'] = afmt
def query_all_flds(note):
"""
Query all fields of single note
"""
word_ord, word, maps = inspect_note(note)
if not word:
raise InvalidWordException
if config.ignore_accents:
word = strip_combining(unicode(word))
# progress.update_title(u'Querying [[ %s ]]' % word)
services = {}
tasks = []
for i, each in enumerate(maps):
if i == word_ord:
continue
if i == len(note.fields):
break
#ignore field
ignore = each.get('ignore', False)
if ignore:
continue
#skip valued
skip = each.get('skip_valued', False)
if skip and len(note.fields[i]) != 0:
continue
#normal
dict_unique = each.get('dict_unique', '').strip()
dict_fld_ord = each.get('dict_fld_ord', -1)
fld_ord = each.get('fld_ord', -1)
if dict_unique and dict_fld_ord != -1 and fld_ord != -1:
s = services.get(dict_unique, None)
if s is None:
s = service_pool.get(dict_unique)
if s and s.support:
services[dict_unique] = s
if s and s.support:
tasks.append({'k': dict_unique, 'w': word,
'f': dict_fld_ord, 'i': fld_ord})
success_num = 0
result = defaultdict(QueryResult)
for task in tasks:
#try:
service = services.get(task['k'], None)
qr = service.active(task['f'], task['w'])
if qr:
result.update({task['i']: qr})
success_num += 1
#except:
# showInfo(_("NO_QUERY_WORD"))
# pass
missed_css = list()
for service in services.values():
if isinstance(service, LocalService):
for css in service.missed_css:
missed_css.append({
'dict_path': service.dict_path,
'title': service.title,
'file': css
})
service_pool.put(service)
return result, -1 if len(tasks) == 0 else success_num, missed_css

151
src/fastwq/query/worker.py Normal file
View File

@ -0,0 +1,151 @@
#-*- coding:utf-8 -*-
#
# Copyright © 20162017 sthoo <sth201807@gmail.com>
#
# Support: Report an issue at https://github.com/sth2018/FastWordQuery/issues
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version; http://www.gnu.org/copyleft/gpl.html.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt4 import QtCore
from aqt import mw
from aqt.utils import showInfo
from ..context import config
from ..lang import _
from ..gui import ProgressWindow
from ..utils import Empty, MapDict, Queue
from .common import InvalidWordException, query_all_flds, update_note_fields
__all__ = ['QueryWorkerManager']
class QueryThread(QtCore.QThread):
"""
Query Worker Thread
"""
note_flush = QtCore.pyqtSignal(object)
def __init__(self, manager):
super(QueryThread, self).__init__()
self.index = 0
self.exit = False
self.finished = False
self.manager = manager
self.note_flush.connect(manager.handle_flush)
def run(self):
while True:
if self.exit or not self.manager:
break
try:
note = self.manager.queue.get(True, timeout=0.1)
except Empty:
self.exit = True
continue
try:
results, success_num, missed_css = query_all_flds(note)
if not self.exit and self.manager:
if self.manager.update(note, results, success_num, missed_css):
self.note_flush.emit(note)
except InvalidWordException:
# only show error info on single query
if self.manager.total == 1:
showInfo(_("NO_QUERY_WORD"))
if self.manager:
self.manager.queue.task_done()
self.finished = True
class QueryWorkerManager(object):
"""
Query Worker Thread Manager
"""
def __init__(self):
self.workers = []
self.queue = Queue()
self.mutex = QtCore.QMutex()
self.progress = ProgressWindow(mw)
self.total = 0
self.counter = 0
self.fails = 0
self.fields = 0
self.skips = 0
self.missed_css = list()
def get_worker(self):
worker = QueryThread(self)
worker.index = len(self.workers) + 1
self.workers.append(worker)
return worker
def start(self):
self.total = self.queue.qsize()
self.progress.start(self.total, min=0)
if self.total > 1:
for _ in range(0, min(config.thread_number, self.total)):
self.get_worker()
for worker in self.workers:
worker.start()
else:
worker = self.get_worker()
worker.run()
def update(self, note, results, success_num, missed_css):
self.mutex.lock()
if success_num > 0:
self.counter += 1
elif success_num == 0:
self.fails += 1
else:
self.skips += 1
val = update_note_fields(note, results)
self.fields += val
self.missed_css += missed_css
self.mutex.unlock()
if self.total > 1:
return val > 0
else:
self.handle_flush(note)
return False
def join(self):
for worker in self.workers:
while not worker.finished:
if self.progress.abort():
worker.exit = True
break
else:
self.progress.update_labels(MapDict(
type='count',
words_number=self.counter,
skips_number=self.skips,
fails_number=self.fails,
fields_number=self.fields))
mw.app.processEvents()
worker.wait(100)
self.progress.finish()
@QtCore.pyqtSlot(object)
def handle_flush(self, note):
if note:
note.flush()