From 4e41f6b003e67dabdbcf368a286ecd95f019984d Mon Sep 17 00:00:00 2001 From: Russell Teabeault Date: Sun, 10 Jan 2021 20:23:52 -0700 Subject: [PATCH 1/2] Add support for spaCy. Fixes #162 - Previous attempt was to create an AnkiSpacy addon that was a package manager for installing spacy and its models and notifying other addons. This posed several issues (mainly in windows). See https://github.com/rteabeault/AnkiSpacy/issues/7 - This solution is essentially a continuation of the @ianki solution here https://github.com/kaegi/MorphMan/pull/193 - It uses a python executable path to execute spaCy to discover what models are installed. - It uses the same python path to run a subprocess that listens on stdin and uses spaCy to parse the passed text. - An unfortunate side affect of this is there is currently not a good way to kill the subprocess after a recalc. This may not be an issue in practice but in the future it may be good to have a open/close for morphemizers. These could be used to initialize the subprocess and then close it after. - Created a MorphemizerRegistry that contains all registered morphemizers. Adding and removing morphemizers fires events. The MorphemizerComboBox listens to these events to keep itself updated. - A `fake_aqt` was being added to sys.modules for tests but this was done in all_tests.py. This meant you could not run the tests individually. Moved all modifications of sys.modules into fake_aqt and added some additional sys.modules needed for new tests. --- .gitignore | 3 +- README.md | 98 +++ __init__.py | 1 + morph/UI/morphemizerComboBox.py | 41 +- morph/browser/extractMorphemes.py | 3 +- morph/browser/massTagger.py | 4 +- morph/browser/viewMorphemes.py | 5 +- morph/config.py | 1 + morph/deps/mecab/mecab | Bin morph/deps/spacy/__init__.py | 54 ++ morph/deps/spacy/extract_morphemes.py | 53 ++ morph/deps/spacy/morphemizer.py | 57 ++ morph/main.py | 11 +- morph/manager.py | 22 +- morph/morphemizer.py | 39 +- morph/morphemizer_registry.py | 55 ++ morph/newMorphHelper.py | 3 +- morph/preferencesDialog.py | 25 +- morph/readability.py | 7 +- morph/readability_ui.py | 903 +++++++++++++++++--------- morph/subprocess_util.py | 9 + morph/text_utils.py | 4 +- test/all_tests.py | 4 - test/fake_aqt.py | 25 +- test/test_MorphemizerComboBox.py | 14 +- test/test_mecab_morphemizer.py | 4 +- test/test_morphemizer_registry.py | 24 + test/test_space_morphemizer.py | 4 +- 28 files changed, 1056 insertions(+), 417 deletions(-) mode change 100644 => 100755 morph/deps/mecab/mecab create mode 100644 morph/deps/spacy/__init__.py create mode 100644 morph/deps/spacy/extract_morphemes.py create mode 100644 morph/deps/spacy/morphemizer.py create mode 100644 morph/morphemizer_registry.py create mode 100644 morph/subprocess_util.py create mode 100644 test/test_morphemizer_registry.py diff --git a/.gitignore b/.gitignore index c480176f..8379bb65 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode .DS_Store meta.json -.idea \ No newline at end of file +.idea +*.iml diff --git a/README.md b/README.md index 6c0c9f08..dcdcded0 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,110 @@ MorphMan supports the following languages: - **Japanese**: You must additionally install the _[Japanese Support](https://ankiweb.net/shared/info/3918629684)_ Anki addon - **Chinese**: For Anki 2.0, please use [Jieba-Morph](https://github.com/NinKenDo64/Jieba-Morph). Chinese is included in Morphman for Anki 2.1 - **CJK Characters**: Morphemizer that splits sentence into characters and filters for Chinese-Japanese-Korean logographic/idiographic characters. +- **spaCy**: [SpaCy](https://spacy.io/) is a free open-source library for Natural Language Processing in Python. See section below for more information. - more languages can be added on request if morpheme-splitting-tools are available for it See Matt VS Japan's [video tutorial](https://www.youtube.com/watch?v=dVReg8_XnyA) and accompanying [blog post](https://massimmersionapproach.com/table-of-contents/anki/morphman). See the [MorphMan wiki](https://github.com/kaegi/MorphMan/wiki) for more information. +## spaCy +[SpaCy](https://spacy.io/) is a free open-source library for Natural Language Processing in Python. +Machine learning models for a variety of languages are available, including Chinese, Danish, Dutch, +English, French, German, Greek, Italian, Japanese, Lithuanian, Norwegian Bokmål, Polish, +Portuguese, Romanian, and Spanish. Additionally, spaCy provides the tools to train additional +language models if you desire. + +### Requirements +* Current installation of python 3. (Currently tested on python 3.8.5) +* spaCy installed +* One or more desired language models installed and linked. + +### Installation + +1. Install python if it is not already. See the +[python download page](https://www.python.org/downloads/) for more information. + +2. Determine the path to you python executable and add it to `config.py`. + On Unix/MacOs this can be done with the `which` command using a terminal + ``` + > which python + > /Users/someperson/workspace/spacy_test/venv/bin/python + ``` + + For Windows you can usually find with the `where` command using the command prompt. + ``` + C:\>where python + C:\Users\someperson\AppData\Local\Microsoft\WindowsApps\python.exe + ``` + + Once you have that open config.py in your MorphMan installation and set + `path_python` to the path value. + + Change + ``` + 'path_python': None + ``` + to your path + ``` + 'path_python': '/Users/someperson/somepython/bin/python', + ``` + +3. Install spaCy. + + Unix/MacOs + ``` + python -m pip install spacy + ``` + + Windows + ``` + py -m pip install spacy + ``` + + For more information installing spaCy see the + [installation instructions](https://spacy.io/usage). + +4. Install and link the desired spaCy models. + You must install the spaCy model and then make sure it is linked. For example, if you wanted + to use the German model `de_core_news_sm`. You would do the following. + ``` + python -m spacy download de_core_news_sm + python -m spacy link de_core_news_sm + ``` + + "spaCy - `link_name`" is the name that will show up in MorphMan when selecting a morphemizer. + For example, if we did + ``` + python -m spacy link de_core_news_sm de + ``` + + then we should see "spaCy - de" as a morphemizer option. + + You can verify what models are installed for spaCy + ``` + python -m spacy info + + ============================== Info about spaCy ============================== + + spaCy version 2.3.5 + Location /Users/someperson/python3.8/site-packages/spacy + Platform macOS-10.15.7-x86_64-i386-64bit + Python version 3.8.5 + Models ja, de + ``` + + Here you can see two models have been installed and linked, `ja` and `de`. + + For more information installing spaCy models see the + [installation instructions](https://spacy.io/usage/models). + +### Debugging +If you find you are having issues getting MoprhMan to recognize your installed models there may +be valuable log output in morphman log file. By default this log file should be in the root of your +Anki profile directory and called `morphman.log`. Please use the output of this log file when +opening any issues. + # Development - Set up local environment: - The best is to use a Python virtual environment and install prebuilt Anki wheels: diff --git a/__init__.py b/__init__.py index 9a1688d2..8347cf09 100644 --- a/__init__.py +++ b/__init__.py @@ -90,6 +90,7 @@ def main(): from .morph.browser import alreadyKnownTagger from .morph import newMorphHelper from .morph import stats + from .morph import morphemizer_registry anki.stats.CollectionStats.easeGraph = \ diff --git a/morph/UI/morphemizerComboBox.py b/morph/UI/morphemizerComboBox.py index 87edb643..fcf70d55 100644 --- a/morph/UI/morphemizerComboBox.py +++ b/morph/UI/morphemizerComboBox.py @@ -1,31 +1,34 @@ - +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QComboBox - class MorphemizerComboBox(QComboBox): + name_role = Qt.UserRole + 1 + morphemizer_role = Qt.UserRole + 2 - def setMorphemizers(self, morphemizers): - if type(morphemizers) == list: - self.morphemizers = morphemizers - else: - self.morphemizers = [] + def __init__(self, morphemizerRegistry=None, parent=None): + super(MorphemizerComboBox, self).__init__(parent) - for morphemizer in self.morphemizers: - self.addItem(morphemizer.getDescription()) + if morphemizerRegistry: + self.setMorphemizerRegistry(morphemizerRegistry) self.setCurrentIndex(0) + def setMorphemizerRegistry(self, morphemizerRegistry): + morphemizerRegistry.morphemizer_added.connect(self._add_morphemizer) + morphemizerRegistry.morphemizer_removed.connect(self._remove_morphemizer) + + for morphemizer in morphemizerRegistry.getMorphemizers(): + self._add_morphemizer(morphemizer) + def getCurrent(self): - try: - return self.morphemizers[self.currentIndex()] - except IndexError: - return None + return self.currentData() def setCurrentByName(self, name): - active = False - for i, morphemizer in enumerate(self.morphemizers): - if morphemizer.getName() == name: - active = i - if active: - self.setCurrentIndex(active) + self.setCurrentIndex(self.findData(name, role=self.name_role)) + + def _add_morphemizer(self, morphemizer): + self.addItem(morphemizer.getDescription(), morphemizer) + self.setItemData(self.findData(morphemizer), morphemizer.getName(), self.name_role) + def _remove_morphemizer(self, morphemizer): + self.removeItem(self.findText(morphemizer.getDescription())) diff --git a/morph/browser/extractMorphemes.py b/morph/browser/extractMorphemes.py index 711d0840..06032a52 100644 --- a/morph/browser/extractMorphemes.py +++ b/morph/browser/extractMorphemes.py @@ -3,7 +3,6 @@ from anki.hooks import addHook from anki.utils import stripHTML from ..morphemes import AnkiDeck, MorphDb, getMorphemes -from ..morphemizer import getMorphemizerByName from ..util import addBrowserNoteSelectionCmd, mw, getFilter, infoMsg, QFileDialog, runOnce from ..preferences import get_preference as cfg @@ -21,7 +20,7 @@ def per(st, n): if note_cfg is None: return st - morphemizer = getMorphemizerByName(note_cfg['Morphemizer']) + morphemizer = mw.morphemizerRegistry.getMorphemizer(note_cfg['Morphemizer']) for f in note_cfg['Fields']: ms = getMorphemes(morphemizer, stripHTML(n[f]), n.tags) loc = AnkiDeck(n.id, f, n[f], n.guid, mats) diff --git a/morph/browser/massTagger.py b/morph/browser/massTagger.py index 34d3677c..5056b4a9 100644 --- a/morph/browser/massTagger.py +++ b/morph/browser/massTagger.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- +from aqt import mw from aqt.utils import tooltip from anki.hooks import addHook from anki.utils import stripHTML from ..morphemes import getMorphemes, MorphDb -from ..morphemizer import getMorphemizerByName from ..util import addBrowserNoteSelectionCmd, getFilter, infoMsg, QInputDialog, QFileDialog, QLineEdit, runOnce from ..preferences import get_preference as cfg from anki.lang import _ @@ -31,7 +31,7 @@ def per(st, n): # :: State -> Note -> State note_cfg = getFilter(n) if note_cfg is None: return st - morphemizer = getMorphemizerByName(note_cfg['Morphemizer']) + morphemizer = mw.morphemizerRegistry.getMorphemizer(note_cfg['Morphemizer']) for field in note_cfg['Fields']: for m in getMorphemes(morphemizer, stripHTML(n[field]), n.tags): if m in st['db'].db: diff --git a/morph/browser/viewMorphemes.py b/morph/browser/viewMorphemes.py index d4a084c7..48885d6c 100644 --- a/morph/browser/viewMorphemes.py +++ b/morph/browser/viewMorphemes.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from anki.hooks import addHook from anki.utils import stripHTML +from aqt import mw + from ..morphemes import getMorphemes, ms2str -from ..morphemizer import getMorphemizerByName from ..util import addBrowserNoteSelectionCmd, getFilter, infoMsg, runOnce from ..preferences import get_preference as cfg @@ -15,7 +16,7 @@ def per(st, n): if notecfg is None: return st - morphemizer = getMorphemizerByName(notecfg['Morphemizer']) + morphemizer = mw.morphemizerRegistry.getMorphemizer(notecfg['Morphemizer']) for f in notecfg['Fields']: ms = getMorphemes(morphemizer, stripHTML(n[f]), n.tags) st['morphemes'] += ms diff --git a/morph/config.py b/morph/config.py index d7e9effe..189f8fc1 100644 --- a/morph/config.py +++ b/morph/config.py @@ -14,6 +14,7 @@ 'path_seen': os.path.join(mw.pm.profileFolder(), 'dbs', 'seen.db'), 'path_log': os.path.join(mw.pm.profileFolder(), 'morphman.log'), 'path_stats': os.path.join(mw.pm.profileFolder(), 'morphman.stats'), + 'path_python': None, # change the thresholds for various stages of maturity, in days 'threshold_mature': 21, # 21 days is what Anki uses diff --git a/morph/deps/mecab/mecab b/morph/deps/mecab/mecab old mode 100644 new mode 100755 diff --git a/morph/deps/spacy/__init__.py b/morph/deps/spacy/__init__.py new file mode 100644 index 00000000..65aba765 --- /dev/null +++ b/morph/deps/spacy/__init__.py @@ -0,0 +1,54 @@ +import re +import subprocess + +from .morphemizer import SpacyMorphemizer +from ...preferences import get_preference +from ...subprocess_util import platform_subprocess_args +from ...util import printf + + +def init_spacy(morphemizerRegistry): + printf("Initializing Spacy!") + + python_path = get_preference('path_python') + + if python_path: + models = _spacy_models(python_path) + if models: + for model in models: + printf(f"Creating morphemizer for spacy model {model}.") + register_morphemizer(morphemizerRegistry, model) + else: + printf("No models were installed for spaCy.") + else: + printf('Python path not specified in config.py.') + + +def _parse_morphemizers(info_command_result): + m = re.search('^Models\\s+(.*)$', info_command_result, re.MULTILINE) + return [x.strip() for x in m.group(1).split(',')] + + +def _spacy_models(python_path): + cmd = [python_path, '-m', 'spacy', 'info'] + printf(f"Collecting spacy model info: {cmd}") + + result = subprocess.run( + [python_path, '-m', 'spacy', 'info'], + capture_output=True, + **platform_subprocess_args()) + + if result.returncode != 0: + printf('Command to find spaCy models failed. Please ensure python is installed at the path ' + 'given in config.py under the "path_python" key and spaCy is installed in that ' + 'python installation') + return None + else: + printf(result) + output = result.stdout.decode('utf-8') + printf(f"spaCy info returned the following: {output}") + return _parse_morphemizers(output) + + +def register_morphemizer(morphemizerRegistry, model): + morphemizerRegistry.addMorphemizer(SpacyMorphemizer(model)) diff --git a/morph/deps/spacy/extract_morphemes.py b/morph/deps/spacy/extract_morphemes.py new file mode 100644 index 00000000..6bd5055f --- /dev/null +++ b/morph/deps/spacy/extract_morphemes.py @@ -0,0 +1,53 @@ +import argparse +import json +import sys + +import spacy + + +POS_BLACKLIST = [ + 'SPACE', + 'PUNCT', + 'NUM', +] + + +def process_input(model): + nlp = spacy.load(model) + for line in sys.stdin: + doc = nlp(line) + result = list(map(lambda t: _createMorpheme(t, doc), filter(_filter_tokens, doc))) + print(json.dumps(result)) + sys.stdout.flush() + + +def _createMorpheme(token, doc): + reading = token.lemma_ + if "reading_forms" in doc.user_data: + reading_forms = doc.user_data["reading_forms"] + if token.i < len(reading_forms): + reading = "" if reading_forms[token.i] is None else reading_forms[token.i] + + return { + 'norm': token.lemma_, + 'base': token.norm_, + 'inflected': token.text, + 'read': reading, + 'pos': token.pos_, + 'subPos': "*" + } + + +def _filter_tokens(token): + return not token.pos_ in POS_BLACKLIST + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="""Listens on stdin and process given text with spaCy. After starting pass in + line delimited text to process with spaCy using the given model. The output is a json + object containing norm, base, inflected, read, pos, and subpos.""" + ) + parser.add_argument('--model', required=True, help="Model to use for processing text. ") + args = parser.parse_args() + process_input(args.model) diff --git a/morph/deps/spacy/morphemizer.py b/morph/deps/spacy/morphemizer.py new file mode 100644 index 00000000..fdec7514 --- /dev/null +++ b/morph/deps/spacy/morphemizer.py @@ -0,0 +1,57 @@ +# import spacy +import json +#################################################################################################### +# spacy Morphemizer +#################################################################################################### +import os +import subprocess + +from ...morphemes import Morpheme +from ...morphemizer import Morphemizer +from ...preferences import get_preference +from ...subprocess_util import platform_subprocess_args +from ...util import printf + + +class SpacyMorphemizer(Morphemizer): + """ + Morphemizer based on the spaCy NLP + """ + + def __init__(self, model_name): + super(SpacyMorphemizer, self).__init__() + self.model_name = model_name + self.proc = None + + def _getMorphemesFromExpr(self, e): + if not self.proc: + path = os.path.join(os.path.dirname(__file__), 'extract_morphemes.py') + cmd = [get_preference('path_python'), path, '--model', self.model_name] + printf(f"Spawning process for spacy: {cmd}") + self.proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + **platform_subprocess_args() + ) + + self.proc.stdin.write(e.encode('utf-8') + b'\n') + self.proc.stdin.flush() + morphs = json.loads(self.proc.stdout.readline()) + results = [ + Morpheme( + m['norm'], + m['base'], + m['inflected'], + m['read'], + m['pos'], + m['subPos']) for m in morphs] + + return results + + def getDescription(self): + return f'spaCy - {self.model_name}' + + def getName(self): + return self.model_name diff --git a/morph/main.py b/morph/main.py index e0db4e5e..bfeef21f 100644 --- a/morph/main.py +++ b/morph/main.py @@ -16,7 +16,6 @@ from . import stats from . import util from .morphemes import MorphDb, AnkiDeck, getMorphemes -from .morphemizer import getMorphemizerByName from .util import printf, mw, errorMsg, getFilterByMidAndTags, getReadEnabledModels, getModifyEnabledModels from .preferences import get_preference as cfg, get_preferences from .util_external import memoize @@ -85,7 +84,7 @@ def notesToUpdate(last_updated, included_mids): # returns list of (nid, mid, flds, guid, tags, maxmat) of # cards to analyze # ignoring cards that are leeches - # + # # leeches are cards have tag "Leech". Anki guarantees a space before and after # # the logic of the query is: @@ -167,7 +166,7 @@ def mkAllDb(all_db=None): N_enabled_notes += 1 mName = mid_cfg['Morphemizer'] - morphemizer = getMorphemizerByName(mName) + morphemizer = mw.morphemizerRegistry.getMorphemizer(mName) C = partial(cfg, model_id=mid) @@ -286,7 +285,7 @@ def updateNotes(allDb): skip_comprehension_cards = cfg('Option_SkipComprehensionCards') skip_fresh_cards = cfg('Option_SkipFreshVocabCards') - + # Find all morphs that changed maturity and the notes that refer to them. last_maturities = allDb.meta.get('last_maturities', {}) new_maturities = {} @@ -398,7 +397,7 @@ def updateNotes(allDb): if priorityDb.frequency(focusMorph) > 0: isPriority = True usefulness += C('priority.db weight') - + if frequency_has_morphemes: focusMorphIndex = frequency_map.get(focusMorph, -1) else: @@ -466,7 +465,7 @@ def updateNotes(allDb): mmi = 100000 * N_k + 1000 * lenDiff + int(round(usefulness)) if C('set due based on mmi'): nid2mmi[nid] = mmi - + # set type agnostic fields setField(mid, fs, field_unknown_count, '%d' % N_k) setField(mid, fs, field_unmature_count, '%d' % N_m) diff --git a/morph/manager.py b/morph/manager.py index 72f66493..00faac8a 100644 --- a/morph/manager.py +++ b/morph/manager.py @@ -9,7 +9,6 @@ from . import adaptiveSubs from .morphemes import MorphDb -from .morphemizer import getAllMorphemizers from .util import errorMsg, infoMsg, mw, mkBtn from .preferences import get_preference as cfg @@ -36,7 +35,7 @@ def getProgressWidget(): class AdaptiveSubWin(QDialog): - def __init__(self, parent=None): + def __init__(self, morphemizerRegistry, parent=None): super(AdaptiveSubWin, self).__init__(parent) self.setWindowTitle('Adaptive Subs') self.grid = grid = QGridLayout(self) @@ -45,8 +44,7 @@ def __init__(self, parent=None): self.matureFmt = QLineEdit('%(target)s') self.knownFmt = QLineEdit('%(target)s [%(native)s]') self.unknownFmt = QLineEdit('%(native)s [%(N_k)s] [%(unknowns)s]') - self.morphemizerComboBox = MorphemizerComboBox() - self.morphemizerComboBox.setMorphemizers(morphemizers=getAllMorphemizers()) + self.morphemizerComboBox = MorphemizerComboBox(morphemizerRegistry) self.vbox.addWidget(QLabel('Mature Format')) self.vbox.addWidget(self.matureFmt) @@ -92,7 +90,7 @@ def onGo(self): class MorphMan(QDialog): - def __init__(self, parent=None): + def __init__(self, morphemizerRegistry, parent=None): super(MorphMan, self).__init__(parent) self.mw = parent self.setWindowTitle('Morph Man 3 Manager') @@ -119,8 +117,7 @@ def __init__(self, parent=None): # Creation # language class/morphemizer self.db = None - self.morphemizerComboBox = MorphemizerComboBox() - self.morphemizerComboBox.setMorphemizers(getAllMorphemizers()) + self.morphemizerComboBox = MorphemizerComboBox(morphemizerRegistry) vbox.addSpacing(40) vbox.addWidget(self.morphemizerComboBox) @@ -140,16 +137,19 @@ def __init__(self, parent=None): self.analysisDisplay = QTextEdit() # Exporting - self.adaptiveSubs = mkBtn('Adaptive Subs', self.adaptiveSubsMethod, vbox) + self.adaptiveSubs = mkBtn( + 'Adaptive Subs', + lambda: self.adaptiveSubsMethod(morphemizerRegistry), + vbox) # layout grid.addLayout(vbox, 0, 0) grid.addWidget(self.morphDisplay, 0, 1) grid.addWidget(self.analysisDisplay, 0, 2) - def adaptiveSubsMethod(self): + def adaptiveSubsMethod(self, morphemizerRegistry): self.hide() - asw = AdaptiveSubWin(self.mw) + asw = AdaptiveSubWin(morphemizerRegistry, self.mw) asw.show() def loadA(self): @@ -249,5 +249,5 @@ def updateDisplay(self): def main(): - mw.mm = MorphMan(mw) + mw.mm = MorphMan(mw.morphemizerRegistry, mw) mw.mm.show() diff --git a/morph/morphemizer.py b/morph/morphemizer.py index 31d4681a..4429b063 100644 --- a/morph/morphemizer.py +++ b/morph/morphemizer.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- import re -from .morphemes import Morpheme -from .deps.zhon.hanzi import characters -from .mecab_wrapper import getMorphemesMecab, getMecabIdentity from .deps.jieba import posseg +from .deps.zhon.hanzi import characters +from .mecab_wrapper import getMecabIdentity +from .mecab_wrapper import getMorphemesMecab +from .morphemes import Morpheme + class LRUCache: def __init__(self, capacity): @@ -38,18 +40,18 @@ def set(self, key, value): class Morphemizer: def __init__(self): self.lru = LRUCache(1000000) - + def getMorphemesFromExpr(self, expression): # type: (str) -> [Morpheme] morphs = self.lru.get(expression) if morphs: return morphs - + morphs = self._getMorphemesFromExpr(expression) self.lru.set(expression, morphs) return morphs - + def _getMorphemesFromExpr(self, expression): # type: (str) -> [Morpheme] """ @@ -68,31 +70,6 @@ def getName(self): # type: () -> str return self.__class__.__name__ - -#################################################################################################### -# Morphemizer Helpers -#################################################################################################### - -morphemizers = None -morphemizers_by_name = {} - -def getAllMorphemizers(): - # type: () -> [Morphemizer] - global morphemizers, morphemizers_by_name - if morphemizers is None: - morphemizers = [SpaceMorphemizer(), MecabMorphemizer(), JiebaMorphemizer(), CjkCharMorphemizer()] - - for m in morphemizers: - morphemizers_by_name[m.getName()] = m - - return morphemizers - -def getMorphemizerByName(name): - # type: (str) -> Optional(Morphemizer) - getAllMorphemizers() - return morphemizers_by_name.get(name, None) - - #################################################################################################### # Mecab Morphemizer #################################################################################################### diff --git a/morph/morphemizer_registry.py b/morph/morphemizer_registry.py new file mode 100644 index 00000000..554e75b6 --- /dev/null +++ b/morph/morphemizer_registry.py @@ -0,0 +1,55 @@ +from PyQt5.QtCore import QObject, pyqtSignal +from anki.hooks import addHook +from aqt import mw + +from .deps.spacy import init_spacy +from .morphemizer import SpaceMorphemizer, MecabMorphemizer, JiebaMorphemizer, CjkCharMorphemizer + + +class MorphemizerRegistry(QObject): + morphemizer_added = pyqtSignal(object) + morphemizer_removed = pyqtSignal(object) + + def __init__(self): + super().__init__() + self.morphemizers = {} + + def addMorphemizer(self, morphemizer): + if morphemizer.getName() not in self.morphemizers: + self.morphemizers[morphemizer.getName()] = morphemizer + self.morphemizer_added.emit(morphemizer) + + def addMorphemizers(self, morphemizers): + for morphemizer in morphemizers: + self.addMorphemizer(morphemizer) + + def removeMorphemizer(self, name): + morphemizer = self.morphemizers.pop(name, None) + if morphemizer: + self.morphemizer_removed.emit(morphemizer) + return morphemizer + + def getMorphemizers(self): + return list(self.morphemizers.values()) + + def getMorphemizer(self, name): + return self.morphemizers.get(name) + + +def createMorphemizerRegistry(): + morphemizerRegistry = MorphemizerRegistry() + morphemizerRegistry.addMorphemizer(SpaceMorphemizer()) + morphemizerRegistry.addMorphemizer(MecabMorphemizer()) + morphemizerRegistry.addMorphemizer(JiebaMorphemizer()) + morphemizerRegistry.addMorphemizer(CjkCharMorphemizer()) + + init_spacy(morphemizerRegistry) + + return morphemizerRegistry + + +def _initializeMorphemizerRegistry(): + mw.morphemizerRegistry = createMorphemizerRegistry() + + +addHook('profileLoaded', _initializeMorphemizerRegistry) diff --git a/morph/newMorphHelper.py b/morph/newMorphHelper.py index 3f9a04f5..00679b24 100644 --- a/morph/newMorphHelper.py +++ b/morph/newMorphHelper.py @@ -248,7 +248,6 @@ def highlight(txt: str, field, filter: str, ctx) -> str: return txt from .util import getFilter - from .morphemizer import getMorphemizerByName from .morphemes import getMorphemes # must avoid formatting a smaller morph that is contained in a bigger morph @@ -271,7 +270,7 @@ def nonSpanSub(sub, repl, string): filter = getFilter(note) if filter is None: return txt - morphemizer = getMorphemizerByName(filter['Morphemizer']) + morphemizer = mw.morphemizerRegistry.getMorphemizer(filter['Morphemizer']) if morphemizer is None: return txt diff --git a/morph/preferencesDialog.py b/morph/preferencesDialog.py index c5615c66..57bca384 100644 --- a/morph/preferencesDialog.py +++ b/morph/preferencesDialog.py @@ -1,25 +1,23 @@ -from PyQt5.QtCore import * -from PyQt5.QtWidgets import * -from PyQt5.QtGui import * +# only for jedi-auto-completion +import aqt.main from anki.lang import _ - from aqt.utils import tooltip +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * -from .util import mw, mkBtn from .preferences import get_preference, update_preferences -from .morphemizer import getAllMorphemizers from .UI import MorphemizerComboBox - -# only for jedi-auto-completion -import aqt.main +from .util import mkBtn, mw assert isinstance(mw, aqt.main.AnkiQt) class PreferencesDialog(QDialog): - def __init__(self, parent=None): + def __init__(self, morphemizerRegistry, parent=None): super(PreferencesDialog, self).__init__(parent) - + self.morphemizerRegistry = morphemizerRegistry + self.setModal(True) self.rowGui = [] self.resize(950, 600) @@ -274,8 +272,7 @@ def setTableRow(self, rowIndex, data): modelComboBox.addItem(model) modelComboBox.setCurrentIndex(active) - morphemizerComboBox = MorphemizerComboBox() - morphemizerComboBox.setMorphemizers(getAllMorphemizers()) + morphemizerComboBox = MorphemizerComboBox(self.morphemizerRegistry) morphemizerComboBox.setCurrentByName(data['Morphemizer']) readItem = QStandardItem() @@ -400,5 +397,5 @@ def onDown(self): def main(): - mw.mm = PreferencesDialog(mw) + mw.mm = PreferencesDialog(mw.morphemizerRegistry, mw) mw.mm.show() diff --git a/morph/readability.py b/morph/readability.py index c41e8df6..f44ff8e9 100644 --- a/morph/readability.py +++ b/morph/readability.py @@ -22,7 +22,6 @@ from PyQt5 import QtWebSockets, QtNetwork from .morphemes import Morpheme, MorphDb, getMorphemes, altIncludesMorpheme -from .morphemizer import getAllMorphemizers from .preferences import get_preference as cfg, update_preferences from .util import mw from anki.utils import stripHTML @@ -372,7 +371,7 @@ def onReject(self): pass class AnalyzerDialog(QDialog): - def __init__(self, parent=None): + def __init__(self, morphemizerRegistry, parent=None): super(AnalyzerDialog, self).__init__(parent) self.mw = parent @@ -380,7 +379,7 @@ def __init__(self, parent=None): self.ui.setupUi(self) # Init morphemizer - self.ui.morphemizerComboBox.setMorphemizers(getAllMorphemizers()) + self.ui.morphemizerComboBox.setMorphemizerRegistry(morphemizerRegistry) self.ui.morphemizerComboBox.setCurrentByName(cfg('DefaultMorphemizer')) self.ui.morphemizerComboBox.currentIndexChanged.connect(lambda idx: self.save_morphemizer()) @@ -1392,6 +1391,6 @@ def output_study_result(source, study_result, old_line_readability, new_line_rea mw.progress.finish() def main(): - mw.mm = AnalyzerDialog(mw) + mw.mm = AnalyzerDialog(mw.morphemizerRegistry, mw) mw.mm.show() diff --git a/morph/readability_ui.py b/morph/readability_ui.py index d93e6990..6d111cd7 100644 --- a/morph/readability_ui.py +++ b/morph/readability_ui.py @@ -1,301 +1,602 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'morph/readability.ui' -# -# Created by: PyQt5 UI code generator 5.15.2 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_ReadabilityDialog(object): - def setupUi(self, ReadabilityDialog): - ReadabilityDialog.setObjectName("ReadabilityDialog") - ReadabilityDialog.resize(901, 730) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(ReadabilityDialog.sizePolicy().hasHeightForWidth()) - ReadabilityDialog.setSizePolicy(sizePolicy) - self.verticalLayout = QtWidgets.QVBoxLayout(ReadabilityDialog) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setContentsMargins(6, 6, 6, 6) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.frame = QtWidgets.QFrame(ReadabilityDialog) - self.frame.setMinimumSize(QtCore.QSize(730, 90)) - self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame.setObjectName("frame") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.inputDirectoryLabel = QtWidgets.QLabel(self.frame) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.inputDirectoryLabel.setFont(font) - self.inputDirectoryLabel.setObjectName("inputDirectoryLabel") - self.verticalLayout_2.addWidget(self.inputDirectoryLabel) - self.horizontalLayout_5 = QtWidgets.QHBoxLayout() - self.horizontalLayout_5.setObjectName("horizontalLayout_5") - self.inputPathButton = QtWidgets.QPushButton(self.frame) - self.inputPathButton.setObjectName("inputPathButton") - self.horizontalLayout_5.addWidget(self.inputPathButton) - self.inputPathEdit = QtWidgets.QLineEdit(self.frame) - self.inputPathEdit.setObjectName("inputPathEdit") - self.horizontalLayout_5.addWidget(self.inputPathEdit) - self.verticalLayout_2.addLayout(self.horizontalLayout_5) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.dictionaryLabel = QtWidgets.QLabel(self.frame) - self.dictionaryLabel.setObjectName("dictionaryLabel") - self.horizontalLayout_4.addWidget(self.dictionaryLabel) - self.morphemizerComboBox = MorphemizerComboBox(self.frame) - self.morphemizerComboBox.setObjectName("morphemizerComboBox") - self.horizontalLayout_4.addWidget(self.morphemizerComboBox) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_4.addItem(spacerItem) - self.minFrequencyLabel = QtWidgets.QLabel(self.frame) - self.minFrequencyLabel.setObjectName("minFrequencyLabel") - self.horizontalLayout_4.addWidget(self.minFrequencyLabel) - self.minFrequencySpinBox = QtWidgets.QSpinBox(self.frame) - self.minFrequencySpinBox.setMaximum(200000) - self.minFrequencySpinBox.setObjectName("minFrequencySpinBox") - self.horizontalLayout_4.addWidget(self.minFrequencySpinBox) - self.targetLabel = QtWidgets.QLabel(self.frame) - self.targetLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.targetLabel.setObjectName("targetLabel") - self.horizontalLayout_4.addWidget(self.targetLabel) - self.targetSpinBox = QtWidgets.QDoubleSpinBox(self.frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.targetSpinBox.sizePolicy().hasHeightForWidth()) - self.targetSpinBox.setSizePolicy(sizePolicy) - self.targetSpinBox.setLayoutDirection(QtCore.Qt.LeftToRight) - self.targetSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.targetSpinBox.setDecimals(1) - self.targetSpinBox.setProperty("value", 98.0) - self.targetSpinBox.setObjectName("targetSpinBox") - self.horizontalLayout_4.addWidget(self.targetSpinBox) - self.verticalLayout_2.addLayout(self.horizontalLayout_4) - self.horizontalLayout_2.addWidget(self.frame) - self.frame_2 = QtWidgets.QFrame(ReadabilityDialog) - self.frame_2.setMinimumSize(QtCore.QSize(130, 0)) - self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_2.setObjectName("frame_2") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_2) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.analyzeButton = QtWidgets.QPushButton(self.frame_2) - self.analyzeButton.setDefault(True) - self.analyzeButton.setObjectName("analyzeButton") - self.verticalLayout_4.addWidget(self.analyzeButton) - self.closeButton = QtWidgets.QPushButton(self.frame_2) - self.closeButton.setDefault(False) - self.closeButton.setObjectName("closeButton") - self.verticalLayout_4.addWidget(self.closeButton) - self.horizontalLayout_2.addWidget(self.frame_2) - spacerItem1 = QtWidgets.QSpacerItem(0, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem1) - self.verticalLayout.addLayout(self.horizontalLayout_2) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) - self.horizontalLayout.setContentsMargins(6, 6, 6, 6) - self.horizontalLayout.setObjectName("horizontalLayout") - self.generalSettingsGroupBox = QtWidgets.QGroupBox(ReadabilityDialog) - self.generalSettingsGroupBox.setMinimumSize(QtCore.QSize(730, 180)) - self.generalSettingsGroupBox.setObjectName("generalSettingsGroupBox") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.generalSettingsGroupBox) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.masterFreqLabel = QtWidgets.QLabel(self.generalSettingsGroupBox) - self.masterFreqLabel.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.masterFreqLabel.setObjectName("masterFreqLabel") - self.verticalLayout_3.addWidget(self.masterFreqLabel) - self.horizontalLayout_MasterFrequency = QtWidgets.QHBoxLayout() - self.horizontalLayout_MasterFrequency.setObjectName("horizontalLayout_MasterFrequency") - self.masterFreqButton = QtWidgets.QPushButton(self.generalSettingsGroupBox) - self.masterFreqButton.setObjectName("masterFreqButton") - self.horizontalLayout_MasterFrequency.addWidget(self.masterFreqButton) - self.masterFreqEdit = QtWidgets.QLineEdit(self.generalSettingsGroupBox) - self.masterFreqEdit.setObjectName("masterFreqEdit") - self.horizontalLayout_MasterFrequency.addWidget(self.masterFreqEdit) - self.verticalLayout_3.addLayout(self.horizontalLayout_MasterFrequency) - self.knownMorphsLabel = QtWidgets.QLabel(self.generalSettingsGroupBox) - self.knownMorphsLabel.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.knownMorphsLabel.setObjectName("knownMorphsLabel") - self.verticalLayout_3.addWidget(self.knownMorphsLabel) - self.horizontalLayout_KnownMorphs = QtWidgets.QHBoxLayout() - self.horizontalLayout_KnownMorphs.setObjectName("horizontalLayout_KnownMorphs") - self.knownMorphsButton = QtWidgets.QPushButton(self.generalSettingsGroupBox) - self.knownMorphsButton.setObjectName("knownMorphsButton") - self.horizontalLayout_KnownMorphs.addWidget(self.knownMorphsButton) - self.knownMorphsEdit = QtWidgets.QLineEdit(self.generalSettingsGroupBox) - self.knownMorphsEdit.setObjectName("knownMorphsEdit") - self.horizontalLayout_KnownMorphs.addWidget(self.knownMorphsEdit) - self.verticalLayout_3.addLayout(self.horizontalLayout_KnownMorphs) - self.outputFreqLabel = QtWidgets.QLabel(self.generalSettingsGroupBox) - self.outputFreqLabel.setObjectName("outputFreqLabel") - self.verticalLayout_3.addWidget(self.outputFreqLabel) - self.horizontalLayout_Output = QtWidgets.QHBoxLayout() - self.horizontalLayout_Output.setObjectName("horizontalLayout_Output") - self.outputFrequencyButton = QtWidgets.QPushButton(self.generalSettingsGroupBox) - self.outputFrequencyButton.setObjectName("outputFrequencyButton") - self.horizontalLayout_Output.addWidget(self.outputFrequencyButton) - self.outputFrequencyEdit = QtWidgets.QLineEdit(self.generalSettingsGroupBox) - self.outputFrequencyEdit.setObjectName("outputFrequencyEdit") - self.horizontalLayout_Output.addWidget(self.outputFrequencyEdit) - self.verticalLayout_3.addLayout(self.horizontalLayout_Output) - self.horizontalLayout.addWidget(self.generalSettingsGroupBox) - self.OutputsGroupBox = QtWidgets.QGroupBox(ReadabilityDialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.OutputsGroupBox.sizePolicy().hasHeightForWidth()) - self.OutputsGroupBox.setSizePolicy(sizePolicy) - self.OutputsGroupBox.setMinimumSize(QtCore.QSize(150, 0)) - self.OutputsGroupBox.setObjectName("OutputsGroupBox") - self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.OutputsGroupBox) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.studyPlanCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) - self.studyPlanCheckBox.setChecked(False) - self.studyPlanCheckBox.setObjectName("studyPlanCheckBox") - self.verticalLayout_5.addWidget(self.studyPlanCheckBox) - self.frequencyListCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) - self.frequencyListCheckBox.setChecked(False) - self.frequencyListCheckBox.setObjectName("frequencyListCheckBox") - self.verticalLayout_5.addWidget(self.frequencyListCheckBox) - self.wordReportCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) - self.wordReportCheckBox.setChecked(False) - self.wordReportCheckBox.setObjectName("wordReportCheckBox") - self.verticalLayout_5.addWidget(self.wordReportCheckBox) - self.groupByDirCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) - self.groupByDirCheckBox.setChecked(False) - self.groupByDirCheckBox.setObjectName("groupByDirCheckBox") - self.verticalLayout_5.addWidget(self.groupByDirCheckBox) - self.processLinesCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) - self.processLinesCheckBox.setChecked(False) - self.processLinesCheckBox.setObjectName("processLinesCheckBox") - self.verticalLayout_5.addWidget(self.processLinesCheckBox) - self.advancedSettingsButton = QtWidgets.QPushButton(self.OutputsGroupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.advancedSettingsButton.sizePolicy().hasHeightForWidth()) - self.advancedSettingsButton.setSizePolicy(sizePolicy) - self.advancedSettingsButton.setObjectName("advancedSettingsButton") - self.verticalLayout_5.addWidget(self.advancedSettingsButton) - self.horizontalLayout.addWidget(self.OutputsGroupBox, 0, QtCore.Qt.AlignTop) - self.verticalLayout.addLayout(self.horizontalLayout) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setContentsMargins(6, -1, 6, -1) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.tabWidget = QtWidgets.QTabWidget(ReadabilityDialog) - self.tabWidget.setObjectName("tabWidget") - self.tabOutputLog = QtWidgets.QWidget() - self.tabOutputLog.setObjectName("tabOutputLog") - self.verticalLayout_21 = QtWidgets.QVBoxLayout(self.tabOutputLog) - self.verticalLayout_21.setObjectName("verticalLayout_21") - self.outputText = QtWidgets.QPlainTextEdit(self.tabOutputLog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.outputText.sizePolicy().hasHeightForWidth()) - self.outputText.setSizePolicy(sizePolicy) - self.outputText.setReadOnly(True) - self.outputText.setObjectName("outputText") - self.verticalLayout_21.addWidget(self.outputText) - self.tabWidget.addTab(self.tabOutputLog, "") - self.tabReadabilityReport = QtWidgets.QWidget() - self.tabReadabilityReport.setObjectName("tabReadabilityReport") - self.verticalLayout_31 = QtWidgets.QVBoxLayout(self.tabReadabilityReport) - self.verticalLayout_31.setObjectName("verticalLayout_31") - self.readabilityTable = CustomTableWidget(self.tabReadabilityReport) - self.readabilityTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self.readabilityTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems) - self.readabilityTable.setRowCount(0) - self.readabilityTable.setColumnCount(0) - self.readabilityTable.setObjectName("readabilityTable") - self.verticalLayout_31.addWidget(self.readabilityTable) - self.tabWidget.addTab(self.tabReadabilityReport, "") - self.tabStudyPlan = QtWidgets.QWidget() - self.tabStudyPlan.setObjectName("tabStudyPlan") - self.verticalLayout_41 = QtWidgets.QVBoxLayout(self.tabStudyPlan) - self.verticalLayout_41.setObjectName("verticalLayout_41") - self.studyPlanTable = CustomTableWidget(self.tabStudyPlan) - self.studyPlanTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self.studyPlanTable.setObjectName("studyPlanTable") - self.studyPlanTable.setColumnCount(0) - self.studyPlanTable.setRowCount(0) - self.verticalLayout_41.addWidget(self.studyPlanTable) - self.tabWidget.addTab(self.tabStudyPlan, "") - self.horizontalLayout_3.addWidget(self.tabWidget) - self.verticalLayout.addLayout(self.horizontalLayout_3) - - self.retranslateUi(ReadabilityDialog) - self.tabWidget.setCurrentIndex(1) - QtCore.QMetaObject.connectSlotsByName(ReadabilityDialog) - - def retranslateUi(self, ReadabilityDialog): - _translate = QtCore.QCoreApplication.translate - ReadabilityDialog.setWindowTitle(_translate("ReadabilityDialog", "Morph Man Readability Analyzer")) - self.inputDirectoryLabel.setText(_translate("ReadabilityDialog", "Input Directory")) - self.inputPathButton.setText(_translate("ReadabilityDialog", "...")) - self.inputPathEdit.setToolTip(_translate("ReadabilityDialog", "Set the Input Directory path to be analyzed.")) - self.dictionaryLabel.setText(_translate("ReadabilityDialog", "Morphemizer:")) - self.morphemizerComboBox.setToolTip(_translate("ReadabilityDialog", "Select the Morphemizer to use for parsing the inputs.")) - self.minFrequencyLabel.setText(_translate("ReadabilityDialog", "Minimum Frequency")) - self.minFrequencySpinBox.setToolTip(_translate("ReadabilityDialog", "The \'Minimum Frequency\' words to include, corresponding to the \'Master Frequency List\' in the Study Plan.\n" -"Morphemes meeting the \'Minimum Frequency\' will be added to the plan until the \'Target %\' is reached.")) - self.targetLabel.setText(_translate("ReadabilityDialog", "Target")) - self.targetSpinBox.setToolTip(_translate("ReadabilityDialog", "The target \'Readability %\' for the study plan.\n" -"Morphemes meeting the \'Minimum Frequency\' will be added to the plan until the \'Target %\' is reached.")) - self.targetSpinBox.setSuffix(_translate("ReadabilityDialog", "%")) - self.analyzeButton.setText(_translate("ReadabilityDialog", "Analyze!")) - self.closeButton.setText(_translate("ReadabilityDialog", "Close")) - self.generalSettingsGroupBox.setTitle(_translate("ReadabilityDialog", "General Settings")) - self.masterFreqLabel.setText(_translate("ReadabilityDialog", "Master Frequency List")) - self.masterFreqButton.setText(_translate("ReadabilityDialog", "...")) - self.masterFreqEdit.setToolTip(_translate("ReadabilityDialog", "Specity a Master Frequency List\n" -"The expected format is that of a instance_freq_report.txt file.")) - self.knownMorphsLabel.setText(_translate("ReadabilityDialog", "Known Morphs DB")) - self.knownMorphsButton.setText(_translate("ReadabilityDialog", "...")) - self.knownMorphsEdit.setToolTip(_translate("ReadabilityDialog", "Path to use as your \'Known\' morphs database.")) - self.outputFreqLabel.setText(_translate("ReadabilityDialog", "Output Directory")) - self.outputFrequencyButton.setText(_translate("ReadabilityDialog", "...")) - self.outputFrequencyEdit.setToolTip(_translate("ReadabilityDialog", "Path where all outputs are written.")) - self.OutputsGroupBox.setTitle(_translate("ReadabilityDialog", "Outputs")) - self.studyPlanCheckBox.setToolTip(_translate("ReadabilityDialog", "Generates a Study Plan for the Input Directory based on your \'Minimum Frequency\' and \'Target %\' settings.\n" -"\n" -"Outputs:\n" -" - study_plan.txt")) - self.studyPlanCheckBox.setText(_translate("ReadabilityDialog", "Build Study Plan")) - self.frequencyListCheckBox.setToolTip(_translate("ReadabilityDialog", "Set MorphMan\'s frequency list based on the study plan & Master Frequency List.\n" -"\n" -"Outputs:\n" -" - frequency.txt\n" -"\n" -"The frequency list takes effect when you Recalc morphemes (Ctrl+M)")) - self.frequencyListCheckBox.setText(_translate("ReadabilityDialog", "Set Frequency List")) - self.wordReportCheckBox.setToolTip(_translate("ReadabilityDialog", "Generate frequency reports for the Input files.\n" -"\n" -"Outputs:\n" -" - instance_freq_report.txt\n" -" - morph_freq_report.txt")) - self.wordReportCheckBox.setText(_translate("ReadabilityDialog", "Write Word Report")) - self.groupByDirCheckBox.setToolTip(_translate("ReadabilityDialog", "Group the \'Readability Report\' and \'Study Plan\' by directory instead of by file.")) - self.groupByDirCheckBox.setText(_translate("ReadabilityDialog", "Group By Directory")) - self.processLinesCheckBox.setToolTip(_translate("ReadabilityDialog", "Calculate line-by-line readability statistics.")) - self.processLinesCheckBox.setText(_translate("ReadabilityDialog", "Line Stats (slower)")) - self.advancedSettingsButton.setToolTip(_translate("ReadabilityDialog", "Advanced Settings")) - self.advancedSettingsButton.setText(_translate("ReadabilityDialog", "Advanced Settings")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabOutputLog), _translate("ReadabilityDialog", "Output Log")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabReadabilityReport), _translate("ReadabilityDialog", "Readability Report")) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabStudyPlan), _translate("ReadabilityDialog", "Study Plan")) -from .UI import MorphemizerComboBox -from .customTableWidget import CustomTableWidget +# -*- coding: utf-8 -*- + + + +# Form implementation generated from reading ui file 'morph/readability.ui' + +# + +# Created by: PyQt5 UI code generator 5.15.2 + +# + +# WARNING: Any manual changes made to this file will be lost when pyuic5 is + +# run again. Do not edit this file unless you know what you are doing. + + + + + +from PyQt5 import QtCore, QtGui, QtWidgets + + + + + +class Ui_ReadabilityDialog(object): + + def setupUi(self, ReadabilityDialog): + + ReadabilityDialog.setObjectName("ReadabilityDialog") + + ReadabilityDialog.resize(901, 730) + + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + + sizePolicy.setHorizontalStretch(0) + + sizePolicy.setVerticalStretch(0) + + sizePolicy.setHeightForWidth(ReadabilityDialog.sizePolicy().hasHeightForWidth()) + + ReadabilityDialog.setSizePolicy(sizePolicy) + + self.verticalLayout = QtWidgets.QVBoxLayout(ReadabilityDialog) + + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + + self.verticalLayout.setSpacing(0) + + self.verticalLayout.setObjectName("verticalLayout") + + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + + self.horizontalLayout_2.setContentsMargins(6, 6, 6, 6) + + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + + self.frame = QtWidgets.QFrame(ReadabilityDialog) + + self.frame.setMinimumSize(QtCore.QSize(730, 90)) + + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + + self.frame.setObjectName("frame") + + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + + self.verticalLayout_2.setObjectName("verticalLayout_2") + + self.inputDirectoryLabel = QtWidgets.QLabel(self.frame) + + font = QtGui.QFont() + + font.setBold(True) + + font.setWeight(75) + + self.inputDirectoryLabel.setFont(font) + + self.inputDirectoryLabel.setObjectName("inputDirectoryLabel") + + self.verticalLayout_2.addWidget(self.inputDirectoryLabel) + + self.horizontalLayout_5 = QtWidgets.QHBoxLayout() + + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + + self.inputPathButton = QtWidgets.QPushButton(self.frame) + + self.inputPathButton.setObjectName("inputPathButton") + + self.horizontalLayout_5.addWidget(self.inputPathButton) + + self.inputPathEdit = QtWidgets.QLineEdit(self.frame) + + self.inputPathEdit.setObjectName("inputPathEdit") + + self.horizontalLayout_5.addWidget(self.inputPathEdit) + + self.verticalLayout_2.addLayout(self.horizontalLayout_5) + + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + + self.dictionaryLabel = QtWidgets.QLabel(self.frame) + + self.dictionaryLabel.setObjectName("dictionaryLabel") + + self.horizontalLayout_4.addWidget(self.dictionaryLabel) + + self.morphemizerComboBox = MorphemizerComboBox(parent=self.frame) + + self.morphemizerComboBox.setObjectName("morphemizerComboBox") + + self.horizontalLayout_4.addWidget(self.morphemizerComboBox) + + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + + self.horizontalLayout_4.addItem(spacerItem) + + self.minFrequencyLabel = QtWidgets.QLabel(self.frame) + + self.minFrequencyLabel.setObjectName("minFrequencyLabel") + + self.horizontalLayout_4.addWidget(self.minFrequencyLabel) + + self.minFrequencySpinBox = QtWidgets.QSpinBox(self.frame) + + self.minFrequencySpinBox.setMaximum(200000) + + self.minFrequencySpinBox.setObjectName("minFrequencySpinBox") + + self.horizontalLayout_4.addWidget(self.minFrequencySpinBox) + + self.targetLabel = QtWidgets.QLabel(self.frame) + + self.targetLabel.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + + self.targetLabel.setObjectName("targetLabel") + + self.horizontalLayout_4.addWidget(self.targetLabel) + + self.targetSpinBox = QtWidgets.QDoubleSpinBox(self.frame) + + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + + sizePolicy.setHorizontalStretch(0) + + sizePolicy.setVerticalStretch(0) + + sizePolicy.setHeightForWidth(self.targetSpinBox.sizePolicy().hasHeightForWidth()) + + self.targetSpinBox.setSizePolicy(sizePolicy) + + self.targetSpinBox.setLayoutDirection(QtCore.Qt.LeftToRight) + + self.targetSpinBox.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + + self.targetSpinBox.setDecimals(1) + + self.targetSpinBox.setProperty("value", 98.0) + + self.targetSpinBox.setObjectName("targetSpinBox") + + self.horizontalLayout_4.addWidget(self.targetSpinBox) + + self.verticalLayout_2.addLayout(self.horizontalLayout_4) + + self.horizontalLayout_2.addWidget(self.frame) + + self.frame_2 = QtWidgets.QFrame(ReadabilityDialog) + + self.frame_2.setMinimumSize(QtCore.QSize(130, 0)) + + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + + self.frame_2.setObjectName("frame_2") + + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_2) + + self.verticalLayout_4.setObjectName("verticalLayout_4") + + self.analyzeButton = QtWidgets.QPushButton(self.frame_2) + + self.analyzeButton.setDefault(True) + + self.analyzeButton.setObjectName("analyzeButton") + + self.verticalLayout_4.addWidget(self.analyzeButton) + + self.closeButton = QtWidgets.QPushButton(self.frame_2) + + self.closeButton.setDefault(False) + + self.closeButton.setObjectName("closeButton") + + self.verticalLayout_4.addWidget(self.closeButton) + + self.horizontalLayout_2.addWidget(self.frame_2) + + spacerItem1 = QtWidgets.QSpacerItem(0, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + + self.horizontalLayout_2.addItem(spacerItem1) + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + self.horizontalLayout = QtWidgets.QHBoxLayout() + + self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + + self.horizontalLayout.setContentsMargins(6, 6, 6, 6) + + self.horizontalLayout.setObjectName("horizontalLayout") + + self.generalSettingsGroupBox = QtWidgets.QGroupBox(ReadabilityDialog) + + self.generalSettingsGroupBox.setMinimumSize(QtCore.QSize(730, 180)) + + self.generalSettingsGroupBox.setObjectName("generalSettingsGroupBox") + + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.generalSettingsGroupBox) + + self.verticalLayout_3.setObjectName("verticalLayout_3") + + self.masterFreqLabel = QtWidgets.QLabel(self.generalSettingsGroupBox) + + self.masterFreqLabel.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) + + self.masterFreqLabel.setObjectName("masterFreqLabel") + + self.verticalLayout_3.addWidget(self.masterFreqLabel) + + self.horizontalLayout_MasterFrequency = QtWidgets.QHBoxLayout() + + self.horizontalLayout_MasterFrequency.setObjectName("horizontalLayout_MasterFrequency") + + self.masterFreqButton = QtWidgets.QPushButton(self.generalSettingsGroupBox) + + self.masterFreqButton.setObjectName("masterFreqButton") + + self.horizontalLayout_MasterFrequency.addWidget(self.masterFreqButton) + + self.masterFreqEdit = QtWidgets.QLineEdit(self.generalSettingsGroupBox) + + self.masterFreqEdit.setObjectName("masterFreqEdit") + + self.horizontalLayout_MasterFrequency.addWidget(self.masterFreqEdit) + + self.verticalLayout_3.addLayout(self.horizontalLayout_MasterFrequency) + + self.knownMorphsLabel = QtWidgets.QLabel(self.generalSettingsGroupBox) + + self.knownMorphsLabel.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) + + self.knownMorphsLabel.setObjectName("knownMorphsLabel") + + self.verticalLayout_3.addWidget(self.knownMorphsLabel) + + self.horizontalLayout_KnownMorphs = QtWidgets.QHBoxLayout() + + self.horizontalLayout_KnownMorphs.setObjectName("horizontalLayout_KnownMorphs") + + self.knownMorphsButton = QtWidgets.QPushButton(self.generalSettingsGroupBox) + + self.knownMorphsButton.setObjectName("knownMorphsButton") + + self.horizontalLayout_KnownMorphs.addWidget(self.knownMorphsButton) + + self.knownMorphsEdit = QtWidgets.QLineEdit(self.generalSettingsGroupBox) + + self.knownMorphsEdit.setObjectName("knownMorphsEdit") + + self.horizontalLayout_KnownMorphs.addWidget(self.knownMorphsEdit) + + self.verticalLayout_3.addLayout(self.horizontalLayout_KnownMorphs) + + self.outputFreqLabel = QtWidgets.QLabel(self.generalSettingsGroupBox) + + self.outputFreqLabel.setObjectName("outputFreqLabel") + + self.verticalLayout_3.addWidget(self.outputFreqLabel) + + self.horizontalLayout_Output = QtWidgets.QHBoxLayout() + + self.horizontalLayout_Output.setObjectName("horizontalLayout_Output") + + self.outputFrequencyButton = QtWidgets.QPushButton(self.generalSettingsGroupBox) + + self.outputFrequencyButton.setObjectName("outputFrequencyButton") + + self.horizontalLayout_Output.addWidget(self.outputFrequencyButton) + + self.outputFrequencyEdit = QtWidgets.QLineEdit(self.generalSettingsGroupBox) + + self.outputFrequencyEdit.setObjectName("outputFrequencyEdit") + + self.horizontalLayout_Output.addWidget(self.outputFrequencyEdit) + + self.verticalLayout_3.addLayout(self.horizontalLayout_Output) + + self.horizontalLayout.addWidget(self.generalSettingsGroupBox) + + self.OutputsGroupBox = QtWidgets.QGroupBox(ReadabilityDialog) + + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + + sizePolicy.setHorizontalStretch(0) + + sizePolicy.setVerticalStretch(0) + + sizePolicy.setHeightForWidth(self.OutputsGroupBox.sizePolicy().hasHeightForWidth()) + + self.OutputsGroupBox.setSizePolicy(sizePolicy) + + self.OutputsGroupBox.setMinimumSize(QtCore.QSize(150, 0)) + + self.OutputsGroupBox.setObjectName("OutputsGroupBox") + + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.OutputsGroupBox) + + self.verticalLayout_5.setObjectName("verticalLayout_5") + + self.studyPlanCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) + + self.studyPlanCheckBox.setChecked(False) + + self.studyPlanCheckBox.setObjectName("studyPlanCheckBox") + + self.verticalLayout_5.addWidget(self.studyPlanCheckBox) + + self.frequencyListCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) + + self.frequencyListCheckBox.setChecked(False) + + self.frequencyListCheckBox.setObjectName("frequencyListCheckBox") + + self.verticalLayout_5.addWidget(self.frequencyListCheckBox) + + self.wordReportCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) + + self.wordReportCheckBox.setChecked(False) + + self.wordReportCheckBox.setObjectName("wordReportCheckBox") + + self.verticalLayout_5.addWidget(self.wordReportCheckBox) + + self.groupByDirCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) + + self.groupByDirCheckBox.setChecked(False) + + self.groupByDirCheckBox.setObjectName("groupByDirCheckBox") + + self.verticalLayout_5.addWidget(self.groupByDirCheckBox) + + self.processLinesCheckBox = QtWidgets.QCheckBox(self.OutputsGroupBox) + + self.processLinesCheckBox.setChecked(False) + + self.processLinesCheckBox.setObjectName("processLinesCheckBox") + + self.verticalLayout_5.addWidget(self.processLinesCheckBox) + + self.advancedSettingsButton = QtWidgets.QPushButton(self.OutputsGroupBox) + + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + + sizePolicy.setHorizontalStretch(0) + + sizePolicy.setVerticalStretch(0) + + sizePolicy.setHeightForWidth(self.advancedSettingsButton.sizePolicy().hasHeightForWidth()) + + self.advancedSettingsButton.setSizePolicy(sizePolicy) + + self.advancedSettingsButton.setObjectName("advancedSettingsButton") + + self.verticalLayout_5.addWidget(self.advancedSettingsButton) + + self.horizontalLayout.addWidget(self.OutputsGroupBox, 0, QtCore.Qt.AlignTop) + + self.verticalLayout.addLayout(self.horizontalLayout) + + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + + self.horizontalLayout_3.setContentsMargins(6, -1, 6, -1) + + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + + self.tabWidget = QtWidgets.QTabWidget(ReadabilityDialog) + + self.tabWidget.setObjectName("tabWidget") + + self.tabOutputLog = QtWidgets.QWidget() + + self.tabOutputLog.setObjectName("tabOutputLog") + + self.verticalLayout_21 = QtWidgets.QVBoxLayout(self.tabOutputLog) + + self.verticalLayout_21.setObjectName("verticalLayout_21") + + self.outputText = QtWidgets.QPlainTextEdit(self.tabOutputLog) + + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + sizePolicy.setHorizontalStretch(0) + + sizePolicy.setVerticalStretch(0) + + sizePolicy.setHeightForWidth(self.outputText.sizePolicy().hasHeightForWidth()) + + self.outputText.setSizePolicy(sizePolicy) + + self.outputText.setReadOnly(True) + + self.outputText.setObjectName("outputText") + + self.verticalLayout_21.addWidget(self.outputText) + + self.tabWidget.addTab(self.tabOutputLog, "") + + self.tabReadabilityReport = QtWidgets.QWidget() + + self.tabReadabilityReport.setObjectName("tabReadabilityReport") + + self.verticalLayout_31 = QtWidgets.QVBoxLayout(self.tabReadabilityReport) + + self.verticalLayout_31.setObjectName("verticalLayout_31") + + self.readabilityTable = CustomTableWidget(self.tabReadabilityReport) + + self.readabilityTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + + self.readabilityTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems) + + self.readabilityTable.setRowCount(0) + + self.readabilityTable.setColumnCount(0) + + self.readabilityTable.setObjectName("readabilityTable") + + self.verticalLayout_31.addWidget(self.readabilityTable) + + self.tabWidget.addTab(self.tabReadabilityReport, "") + + self.tabStudyPlan = QtWidgets.QWidget() + + self.tabStudyPlan.setObjectName("tabStudyPlan") + + self.verticalLayout_41 = QtWidgets.QVBoxLayout(self.tabStudyPlan) + + self.verticalLayout_41.setObjectName("verticalLayout_41") + + self.studyPlanTable = CustomTableWidget(self.tabStudyPlan) + + self.studyPlanTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + + self.studyPlanTable.setObjectName("studyPlanTable") + + self.studyPlanTable.setColumnCount(0) + + self.studyPlanTable.setRowCount(0) + + self.verticalLayout_41.addWidget(self.studyPlanTable) + + self.tabWidget.addTab(self.tabStudyPlan, "") + + self.horizontalLayout_3.addWidget(self.tabWidget) + + self.verticalLayout.addLayout(self.horizontalLayout_3) + + + + self.retranslateUi(ReadabilityDialog) + + self.tabWidget.setCurrentIndex(1) + + QtCore.QMetaObject.connectSlotsByName(ReadabilityDialog) + + + + def retranslateUi(self, ReadabilityDialog): + + _translate = QtCore.QCoreApplication.translate + + ReadabilityDialog.setWindowTitle(_translate("ReadabilityDialog", "Morph Man Readability Analyzer")) + + self.inputDirectoryLabel.setText(_translate("ReadabilityDialog", "Input Directory")) + + self.inputPathButton.setText(_translate("ReadabilityDialog", "...")) + + self.inputPathEdit.setToolTip(_translate("ReadabilityDialog", "Set the Input Directory path to be analyzed.")) + + self.dictionaryLabel.setText(_translate("ReadabilityDialog", "Morphemizer:")) + + self.morphemizerComboBox.setToolTip(_translate("ReadabilityDialog", "Select the Morphemizer to use for parsing the inputs.")) + + self.minFrequencyLabel.setText(_translate("ReadabilityDialog", "Minimum Frequency")) + + self.minFrequencySpinBox.setToolTip(_translate("ReadabilityDialog", "The \'Minimum Frequency\' words to include, corresponding to the \'Master Frequency List\' in the Study Plan.\n" + +"Morphemes meeting the \'Minimum Frequency\' will be added to the plan until the \'Target %\' is reached.")) + + self.targetLabel.setText(_translate("ReadabilityDialog", "Target")) + + self.targetSpinBox.setToolTip(_translate("ReadabilityDialog", "The target \'Readability %\' for the study plan.\n" + +"Morphemes meeting the \'Minimum Frequency\' will be added to the plan until the \'Target %\' is reached.")) + + self.targetSpinBox.setSuffix(_translate("ReadabilityDialog", "%")) + + self.analyzeButton.setText(_translate("ReadabilityDialog", "Analyze!")) + + self.closeButton.setText(_translate("ReadabilityDialog", "Close")) + + self.generalSettingsGroupBox.setTitle(_translate("ReadabilityDialog", "General Settings")) + + self.masterFreqLabel.setText(_translate("ReadabilityDialog", "Master Frequency List")) + + self.masterFreqButton.setText(_translate("ReadabilityDialog", "...")) + + self.masterFreqEdit.setToolTip(_translate("ReadabilityDialog", "Specity a Master Frequency List\n" + +"The expected format is that of a instance_freq_report.txt file.")) + + self.knownMorphsLabel.setText(_translate("ReadabilityDialog", "Known Morphs DB")) + + self.knownMorphsButton.setText(_translate("ReadabilityDialog", "...")) + + self.knownMorphsEdit.setToolTip(_translate("ReadabilityDialog", "Path to use as your \'Known\' morphs database.")) + + self.outputFreqLabel.setText(_translate("ReadabilityDialog", "Output Directory")) + + self.outputFrequencyButton.setText(_translate("ReadabilityDialog", "...")) + + self.outputFrequencyEdit.setToolTip(_translate("ReadabilityDialog", "Path where all outputs are written.")) + + self.OutputsGroupBox.setTitle(_translate("ReadabilityDialog", "Outputs")) + + self.studyPlanCheckBox.setToolTip(_translate("ReadabilityDialog", "Generates a Study Plan for the Input Directory based on your \'Minimum Frequency\' and \'Target %\' settings.\n" + +"\n" + +"Outputs:\n" + +" - study_plan.txt")) + + self.studyPlanCheckBox.setText(_translate("ReadabilityDialog", "Build Study Plan")) + + self.frequencyListCheckBox.setToolTip(_translate("ReadabilityDialog", "Set MorphMan\'s frequency list based on the study plan & Master Frequency List.\n" + +"\n" + +"Outputs:\n" + +" - frequency.txt\n" + +"\n" + +"The frequency list takes effect when you Recalc morphemes (Ctrl+M)")) + + self.frequencyListCheckBox.setText(_translate("ReadabilityDialog", "Set Frequency List")) + + self.wordReportCheckBox.setToolTip(_translate("ReadabilityDialog", "Generate frequency reports for the Input files.\n" + +"\n" + +"Outputs:\n" + +" - instance_freq_report.txt\n" + +" - morph_freq_report.txt")) + + self.wordReportCheckBox.setText(_translate("ReadabilityDialog", "Write Word Report")) + + self.groupByDirCheckBox.setToolTip(_translate("ReadabilityDialog", "Group the \'Readability Report\' and \'Study Plan\' by directory instead of by file.")) + + self.groupByDirCheckBox.setText(_translate("ReadabilityDialog", "Group By Directory")) + + self.processLinesCheckBox.setToolTip(_translate("ReadabilityDialog", "Calculate line-by-line readability statistics.")) + + self.processLinesCheckBox.setText(_translate("ReadabilityDialog", "Line Stats (slower)")) + + self.advancedSettingsButton.setToolTip(_translate("ReadabilityDialog", "Advanced Settings")) + + self.advancedSettingsButton.setText(_translate("ReadabilityDialog", "Advanced Settings")) + + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabOutputLog), _translate("ReadabilityDialog", "Output Log")) + + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabReadabilityReport), _translate("ReadabilityDialog", "Readability Report")) + + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabStudyPlan), _translate("ReadabilityDialog", "Study Plan")) + +from .UI import MorphemizerComboBox + +from .customTableWidget import CustomTableWidget + diff --git a/morph/subprocess_util.py b/morph/subprocess_util.py new file mode 100644 index 00000000..f3e95fb1 --- /dev/null +++ b/morph/subprocess_util.py @@ -0,0 +1,9 @@ +from sys import platform + + +def platform_subprocess_args(): + args = {} + if platform == 'win32': + args['shell'] = True + + return args diff --git a/morph/text_utils.py b/morph/text_utils.py index d13d7f70..5fcd0b9a 100644 --- a/morph/text_utils.py +++ b/morph/text_utils.py @@ -1,8 +1,8 @@ import re +from aqt import mw from anki.utils import stripHTML from .morphemes import getMorphemes -from .morphemizer import getMorphemizerByName from .preferences import get_preference as cfg from .util import getFilterByMidAndTags, allDb @@ -22,7 +22,7 @@ def bold_unknowns(mid, text, tags=None): return text mName = mid_cfg['Morphemizer'] - morphemizer = getMorphemizerByName(mName) + morphemizer = mw.morphemizerRegistry.getMorphemizer(mName) ms = getMorphemes(morphemizer, stripHTML(text)) # Merge helper verbs with their verb's inflections diff --git a/test/all_tests.py b/test/all_tests.py index f371250d..73d52c7f 100644 --- a/test/all_tests.py +++ b/test/all_tests.py @@ -1,10 +1,6 @@ import glob -import sys import unittest -from test import fake_aqt -sys.modules['aqt'] = fake_aqt - def create_test_suite(): test_file_strings = glob.glob('test/test_*.py') module_strings = ['test.'+str[5:len(str)-3] for str in test_file_strings] diff --git a/test/fake_aqt.py b/test/fake_aqt.py index c5ba09a0..8b8e58d7 100644 --- a/test/fake_aqt.py +++ b/test/fake_aqt.py @@ -1,5 +1,20 @@ +import sys from unittest.mock import MagicMock +aqt = MagicMock() +aqt.mw = MagicMock() +aqt.mw.pm.profileFolder = None +aqt.mw.col = None +aqt.mw.toolbar.draw = lambda: None + +aqt.browser = MagicMock() + +sys.modules['aqt'] = aqt +sys.modules['aqt.browser'] = aqt.browser +sys.modules['aqt.qt'] = MagicMock() +sys.modules['aqt.utils'] = MagicMock() + + class FakeCollection: def __init__(self, config): self.config = config if config is not None else {} @@ -10,13 +25,7 @@ def get_config(self, key): def set_config(self, key, value): self.config[key] = value -browser = MagicMock() -mw = MagicMock() -mw.pm.profileFolder = None -mw.col = None -mw.toolbar.draw = lambda: None - def init_collection(profile_folder='somewhere', config=None): - mw.pm.profileFolder = MagicMock(return_value=profile_folder) - mw.col = FakeCollection(config) + aqt.mw.pm.profileFolder = MagicMock(return_value=profile_folder) + aqt.mw.col = FakeCollection(config) diff --git a/test/test_MorphemizerComboBox.py b/test/test_MorphemizerComboBox.py index 7c26c84c..23aecb61 100644 --- a/test/test_MorphemizerComboBox.py +++ b/test/test_MorphemizerComboBox.py @@ -2,17 +2,24 @@ from PyQt5.QtWidgets import QApplication from morph.UI import MorphemizerComboBox -from morph.morphemizer import getAllMorphemizers +from morph.morphemizer_registry import MorphemizerRegistry +from morph.morphemizer import SpaceMorphemizer, MecabMorphemizer, JiebaMorphemizer, \ + CjkCharMorphemizer class TestMorphemizerComboBox(unittest.TestCase): def setUp(self): self.app = QApplication([]) + self.morphemizerManager = MorphemizerRegistry() def test_set_and_get_current(self): - combobox = MorphemizerComboBox() - combobox.setMorphemizers(getAllMorphemizers()) + self.morphemizerManager.addMorphemizer(SpaceMorphemizer()) + self.morphemizerManager.addMorphemizer(MecabMorphemizer()) + self.morphemizerManager.addMorphemizer(JiebaMorphemizer()) + self.morphemizerManager.addMorphemizer(CjkCharMorphemizer()) + + combobox = MorphemizerComboBox(self.morphemizerManager) combobox.setCurrentByName('MecabMorphemizer') self.assertEqual(combobox.currentText(), 'Japanese MorphMan') @@ -21,7 +28,6 @@ def test_set_and_get_current(self): def test_empty_morphemizer_list(self): combobox = MorphemizerComboBox() - combobox.setMorphemizers([]) combobox.setCurrentByName('AnyBecauseNothingExists') current = combobox.getCurrent() diff --git a/test/test_mecab_morphemizer.py b/test/test_mecab_morphemizer.py index f3cd182f..2faed17a 100644 --- a/test/test_mecab_morphemizer.py +++ b/test/test_mecab_morphemizer.py @@ -1,10 +1,10 @@ -from morph.morphemizer import getMorphemizerByName +from morph.morphemizer import MecabMorphemizer import unittest class TestMecabMorphemizer(unittest.TestCase): def setUp(self): - self.morphemizer = getMorphemizerByName("MecabMorphemizer") + self.morphemizer = MecabMorphemizer() def test_morpheme_generation(self): sentence_1 = "こんにちは。私の名前はシャンです。" diff --git a/test/test_morphemizer_registry.py b/test/test_morphemizer_registry.py new file mode 100644 index 00000000..c0e01ea8 --- /dev/null +++ b/test/test_morphemizer_registry.py @@ -0,0 +1,24 @@ +from morph.morphemizer import Morphemizer +from morph.morphemizer_registry import MorphemizerRegistry +import unittest + +class TestMorphemizerRegistry(unittest.TestCase): + def setUp(self): + self.registry = MorphemizerRegistry() + + def test_get_morphemizer_by_name(self): + morphemizer = TestMorphemizer() + self.registry.addMorphemizer(morphemizer) + self.assertEqual(self.registry.getMorphemizer('TestMorphemizer'), morphemizer) + + def test_add_morphemizer_emits_added_event(self): + morphemizer = TestMorphemizer() + self.registry.morphemizer_added.connect(lambda m: self.assertEqual(m, morphemizer)) + self.registry.addMorphemizer(morphemizer) + +class TestMorphemizer(Morphemizer): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_space_morphemizer.py b/test/test_space_morphemizer.py index 6a752dc6..c5478357 100644 --- a/test/test_space_morphemizer.py +++ b/test/test_space_morphemizer.py @@ -1,10 +1,10 @@ -from morph.morphemizer import getMorphemizerByName +from morph.morphemizer import SpaceMorphemizer import unittest class TestSpaceMorphemizer(unittest.TestCase): def setUp(self): - self.morphemizer = getMorphemizerByName("SpaceMorphemizer") + self.morphemizer = SpaceMorphemizer() def test_morpheme_generation(self): sentence_1 = "Tu es quelqu'un de bien." From 51beacae86c4ec675173cbb13abf340fbdec8a7e Mon Sep 17 00:00:00 2001 From: Russell Teabeault Date: Sun, 10 Jan 2021 21:15:40 -0700 Subject: [PATCH 2/2] Fix test failures. --- morph/preferences.py | 2 +- test/all_tests.py | 2 ++ test/fake_aqt.py | 12 ++---------- test/test_ignore_brackets_morphemizer.py | 1 + test/test_preferences.py | 1 + 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/morph/preferences.py b/morph/preferences.py index 5b9e8c2b..54ed3d8c 100644 --- a/morph/preferences.py +++ b/morph/preferences.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import importlib + from aqt import mw # retrieving the configuration using get_config is very expensive operation @@ -9,7 +10,6 @@ def init_preferences(): '''Called when new profiles are loaded''' - # Reset the cached configs. global config_data, config_py diff --git a/test/all_tests.py b/test/all_tests.py index 73d52c7f..b73a6f2d 100644 --- a/test/all_tests.py +++ b/test/all_tests.py @@ -1,6 +1,8 @@ import glob import unittest +from test import fake_aqt + def create_test_suite(): test_file_strings = glob.glob('test/test_*.py') module_strings = ['test.'+str[5:len(str)-3] for str in test_file_strings] diff --git a/test/fake_aqt.py b/test/fake_aqt.py index 8b8e58d7..0938cf4d 100644 --- a/test/fake_aqt.py +++ b/test/fake_aqt.py @@ -2,18 +2,10 @@ from unittest.mock import MagicMock aqt = MagicMock() -aqt.mw = MagicMock() -aqt.mw.pm.profileFolder = None -aqt.mw.col = None -aqt.mw.toolbar.draw = lambda: None - -aqt.browser = MagicMock() - sys.modules['aqt'] = aqt sys.modules['aqt.browser'] = aqt.browser -sys.modules['aqt.qt'] = MagicMock() -sys.modules['aqt.utils'] = MagicMock() - +sys.modules['aqt.qt'] = aqt.qt +sys.modules['aqt.utils'] = aqt.utils class FakeCollection: def __init__(self, config): diff --git a/test/test_ignore_brackets_morphemizer.py b/test/test_ignore_brackets_morphemizer.py index 13b73375..e1e43f2e 100644 --- a/test/test_ignore_brackets_morphemizer.py +++ b/test/test_ignore_brackets_morphemizer.py @@ -1,6 +1,7 @@ import unittest from test import fake_aqt + from morph.morphemes import replaceBracketContents from morph.preferences import init_preferences, get_preference, update_preferences diff --git a/test/test_preferences.py b/test/test_preferences.py index eb929c02..45a9abb6 100644 --- a/test/test_preferences.py +++ b/test/test_preferences.py @@ -2,6 +2,7 @@ import unittest from test import fake_aqt + from morph.preferences import init_preferences, get_preference, update_preferences class TestPreferences(unittest.TestCase):