Change format of wordlist.txt file to json

This commit is contained in:
Veronica Berglyd Olsen
2023-06-13 17:31:16 +02:00
parent 9ac61105f3
commit 0003f8c24d
8 changed files with 200 additions and 178 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ class nwFiles:
INDEX_FILE = "index.json" INDEX_FILE = "index.json"
OPTS_FILE = "options.json" OPTS_FILE = "options.json"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
SESS_STATS = "sessionStats.log" DICT_FILE = "userdict.json"
SESS_FILE = "sessions.jsonl" SESS_FILE = "sessions.jsonl"
# END Class nwFiles # END Class nwFiles
+124 -101
View File
@@ -1,7 +1,6 @@
""" """
novelWriter Spell Check Classes novelWriter Spell Check Classes
================================= =================================
Wrapper classes for spell checking tools
File History: File History:
Created: 2019-06-11 [0.1.5] Created: 2019-06-11 [0.1.5]
@@ -22,29 +21,37 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import json
import logging import logging
from collections import namedtuple from typing import TYPE_CHECKING, Iterator
from pathlib import Path from pathlib import Path
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwFiles
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWSpellEnchant: class NWSpellEnchant:
"""Core: Enchant Spell Checking Wrapper
def __init__(self): This is a rapper class for Enchant to keep the API consistent
between spell check tools.
self._theDict = None """
self._projDict = set()
self._projectDict = None
self._spellLanguage = None
self._theBroker = None
def __init__(self, project: NWProject):
self._project = project
self._dictObj = FakeEnchant()
self._userDict = UserDictionary(project)
self._language = None
self._broker = None
logger.debug("Enchant spell checking activated") logger.debug("Enchant spell checking activated")
return return
## ##
@@ -52,43 +59,43 @@ class NWSpellEnchant:
## ##
@property @property
def spellLanguage(self): def spellLanguage(self) -> str | None:
return self._spellLanguage return self._language
## ##
# Setters # Setters
## ##
def setLanguage(self, theLang, projectDict=None): def setLanguage(self, language: str | None):
"""Load a dictionary for the language specified in the config. """Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't If that fails, we load a mock dictionary so that lookups don't
crash. Note that enchant will allow loading an empty string as crash. Note that enchant will allow loading an empty string as
a tag, but this will fail later on. See issue #1096. a tag, but this will fail later on. See issue #1096.
""" """
self._theBroker = None self._dictObj = FakeEnchant()
self._theDict = None self._broker = None
self._spellLanguage = None self._language = None
try: try:
import enchant import enchant
if theLang and enchant.dict_exists(theLang): if language and enchant.dict_exists(language):
self._theBroker = enchant.Broker() self._broker = enchant.Broker()
self._theDict = self._theBroker.request_dict(theLang) self._dictObj = self._broker.request_dict(language)
self._spellLanguage = theLang self._language = language
logger.debug("Enchant spell checking for language '%s' loaded", theLang) logger.debug("Enchant spell checking for language '%s' loaded", language)
else: else:
logger.warning("Enchant found no dictionary for language '%s'", theLang) logger.warning("Enchant found no dictionary for language '%s'", language)
except Exception: except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang) logger.error("Failed to load enchant spell checking for language '%s'", language)
if self._theDict is None: if self._dictObj is None:
self._theDict = FakeEnchant() self._dictObj = FakeEnchant()
else: else:
self._readProjectDictionary(projectDict) self._userDict.load()
for pWord in self._projDict: for pWord in self._userDict:
self._theDict.add_to_session(pWord) self._dictObj.add_to_session(pWord)
return return
@@ -96,47 +103,38 @@ class NWSpellEnchant:
# Methods # Methods
## ##
def checkWord(self, theWord): def checkWord(self, word: str) -> bool:
"""Wrapper function for pyenchant. """Wrapper function for pyenchant."""
"""
try: try:
return self._theDict.check(theWord) return bool(self._dictObj.check(word))
except Exception: except Exception:
return True return True
def suggestWords(self, theWord): def suggestWords(self, word: str) -> list[str]:
"""Wrapper function for pyenchant. """Wrapper function for pyenchant."""
"""
try: try:
return self._theDict.suggest(theWord) return self._dictObj.suggest(word)
except Exception: except Exception:
return [] return []
def addWord(self, newWord): def addWord(self, word: str) -> bool:
"""Add a word to the project dictionary. """Add a word to the project dictionary."""
""" word = word.strip()
if not word:
return False
try: try:
self._theDict.add_to_session(newWord) self._dictObj.add_to_session(word)
except Exception: except Exception:
return False return False
if self._projectDict is not None and newWord not in self._projDict: added = self._userDict.add(word)
newWord = newWord.strip() if added:
try: self._userDict.save()
with open(self._projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self._projDict.add(newWord)
except Exception:
logger.error("Failed to add word to project word list %s", str(self._projectDict))
logException()
return False
return True
return False return added
def listDictionaries(self): def listDictionaries(self) -> list[tuple[str, str]]:
"""Wrapper function for pyenchant. """Wrapper function for pyenchant."""
"""
retList = [] retList = []
try: try:
import enchant import enchant
@@ -147,73 +145,98 @@ class NWSpellEnchant:
return retList return retList
def describeDict(self): def describeDict(self) -> tuple[str, str]:
"""Return the tag and provider of the currently loaded """Return the tag and provider of the currently loaded
dictionary. dictionary.
""" """
try: try:
spTag = self._theDict.tag tag = self._dictObj.tag
spName = self._theDict.provider.name name = self._dictObj.provider.name # type: ignore
except Exception: except Exception:
logger.error("Failed to extract information about the dictionary") logger.error("Failed to extract information about the dictionary")
logException() logException()
spTag = "" tag = ""
spName = "" name = ""
return spTag, spName return tag, name
##
# Internal Functions
##
def _readProjectDictionary(self, projectDict):
"""Read the content of the project dictionary, and add it to the
lookup lists.
"""
self._projDict = set()
self._projectDict = projectDict
if not isinstance(projectDict, Path):
return False
if not projectDict.exists():
return False
try:
logger.debug("Loading project word list")
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self._projDict:
self._projDict.add(theLine)
logger.debug("Project word list contains %d words", len(self._projDict))
except Exception:
logger.error("Failed to load project word list")
logException()
return False
return True
# END Class NWSpellEnchant # END Class NWSpellEnchant
class FakeEnchant: class FakeEnchant:
"""Fallback for when Enchant is selected, but not installed. """Fallback for when Enchant is selected, but not installed."""
"""
def __init__(self): def __init__(self):
class FakeProvider:
name = ""
self.tag = "" self.tag = ""
self.provider = namedtuple("provider", "name") self.provider = FakeProvider()
self.provider.name = ""
return return
def check(self, theWord): def check(self, word: str) -> bool:
return True return True
def suggest(self, theWord): def suggest(self, word) -> list[str]:
return [] return []
def add_to_session(self, theWord): def add_to_session(self, word: str):
return return
# END Class FakeEnchant # END Class FakeEnchant
class UserDictionary:
def __init__(self, project: NWProject):
self._project = project
self._words = set()
self._path = None
return
def __contains__(self, word: str) -> bool:
return word in self._words
def __iter__(self) -> Iterator[str]:
return iter(self._words)
def add(self, word: str) -> bool:
"""Add a word to the dictionary, and return True if it was
added, or False if it already existed.
"""
if word in self._words:
return False
self._words.add(word)
return True
def load(self):
"""Load the user's dictionary."""
self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
if not isinstance(self._path, Path):
return
try:
with open(self._path, mode="r", encoding="utf-8") as fObj:
data = json.load(fObj)
self._words = set(data.get("novelWriter.userDict", []))
except Exception:
logger.error("Failed to load user dictionary")
logException()
return
def save(self):
"""Save the user's dictionary."""
if self._path is None:
self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
if not isinstance(self._path, Path):
return
try:
with open(self._path, mode="w", encoding="utf-8") as fObj:
data = {"novelWriter.userDict": list(self._words)}
json.dump(data, fObj, indent=2)
except Exception:
logger.error("Failed to save user dictionary")
logException()
return
# END Class UserDictionary
+31 -1
View File
@@ -36,6 +36,7 @@ from novelwriter.common import minmax
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.document import NWDocument from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
from novelwriter.core.spellcheck import UserDictionary
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
@@ -253,7 +254,7 @@ class NWStorage:
(baseMeta / nwFiles.BUILDS_FILE, f"meta/{nwFiles.BUILDS_FILE}"), (baseMeta / nwFiles.BUILDS_FILE, f"meta/{nwFiles.BUILDS_FILE}"),
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"), (baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"), (baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
(baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"), (baseMeta / nwFiles.DICT_FILE, f"meta/{nwFiles.DICT_FILE}"),
(baseMeta / nwFiles.SESS_FILE, f"meta/{nwFiles.SESS_FILE}"), (baseMeta / nwFiles.SESS_FILE, f"meta/{nwFiles.SESS_FILE}"),
] ]
for contItem in baseCont.iterdir(): for contItem in baseCont.iterdir():
@@ -378,6 +379,10 @@ class NWStorage:
if sessLog.is_file(): if sessLog.is_file():
self._convertOldLogFile(sessLog, path / "meta" / nwFiles.SESS_FILE) self._convertOldLogFile(sessLog, path / "meta" / nwFiles.SESS_FILE)
wordList = path / "meta" / "wordlist.txt"
if wordList.is_file():
self._convertOldWordList(wordList)
remove = [ remove = [
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1 path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
path / "meta" / "mainOptions.json", # Replaced in 0.5 path / "meta" / "mainOptions.json", # Replaced in 0.5
@@ -413,6 +418,31 @@ class NWStorage:
return return
def _convertOldWordList(self, wordList: Path) -> bool:
"""Convert the old word list plain text file to new format."""
if not wordList.exists():
# Nothing to convert
return True
userDict = UserDictionary(self._project)
try:
with open(wordList, mode="r", encoding="utf-8") as fObj:
for line in fObj:
word = line.strip()
if word:
userDict.add(word)
# Dave dictionary and clean up old file
userDict.save()
wordList.unlink()
except Exception:
logger.error("Failed to convert old word list file")
logException()
return False
return True
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> bool: def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> bool:
"""Convert the old text log file format to the new JSON Lines """Convert the old text log file format to the new JSON Lines
format. format.
+35 -63
View File
@@ -1,7 +1,6 @@
""" """
novelWriter GUI User Wordlist novelWriter GUI User Wordlist
=============================== ===============================
Class holding the user's wordlist dialog
File History: File History:
Created: 2021-02-12 [1.2rc1] Created: 2021-02-12 [1.2rc1]
@@ -22,28 +21,31 @@ General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import logging import logging
from pathlib import Path from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget, QAbstractItemView, QDialog, QDialogButtonBox, QHBoxLayout, QLabel,
QAbstractItemView, QPushButton, QLineEdit, QLabel QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout
) )
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.enum import nwAlert from novelwriter.enum import nwAlert
from novelwriter.error import logException from novelwriter.core.spellcheck import UserDictionary
from novelwriter.constants import nwFiles
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiWordList(QDialog): class GuiWordList(QDialog):
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiWordList") logger.debug("Create: GuiWordList")
@@ -120,67 +122,49 @@ class GuiWordList(QDialog):
# Slots # Slots
## ##
def _doAdd(self): def _doAdd(self) -> bool:
"""Add a new word to the word list. """Add a new word to the word list."""
""" word = self.newEntry.text().strip()
newWord = self.newEntry.text().strip() if word == "":
if newWord == "":
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"Cannot add a blank word." "Cannot add a blank word."
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
if self.listBox.findItems(newWord, Qt.MatchExactly): if self.listBox.findItems(word, Qt.MatchExactly):
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"The word '{0}' is already in the word list." "The word '{0}' is already in the word list."
).format(newWord), nwAlert.ERROR) ).format(word), nwAlert.ERROR)
return False return False
self.listBox.addItem(newWord) self.listBox.addItem(word)
self.newEntry.setText("") self.newEntry.setText("")
return True return True
def _doDelete(self): def _doDelete(self):
"""Delete the selected item. """Delete the selected item."""
"""
selItem = self.listBox.selectedItems() selItem = self.listBox.selectedItems()
if selItem: if selItem:
self.listBox.takeItem(self.listBox.row(selItem[0])) self.listBox.takeItem(self.listBox.row(selItem[0]))
return return
def _doSave(self): def _doSave(self):
"""Save the new word list and close. """Save the new word list and close."""
"""
self._saveGuiSettings() self._saveGuiSettings()
userDict = UserDictionary(self.theProject)
dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT) for i in range(self.listBox.count()):
if not isinstance(dctFile, Path): item = self.listBox.item(i)
return False if isinstance(item, QListWidgetItem):
word = item.text().strip()
tmpFile = dctFile.with_suffix(".tmp") if word:
try: userDict.add(word)
with open(tmpFile, mode="w", encoding="utf-8") as outFile: userDict.save()
for i in range(self.listBox.count()):
item = self.listBox.item(i)
if item is not None:
outFile.write(item.text() + "\n")
tmpFile.replace(dctFile)
except Exception:
logger.error("Could not save new word list")
logException()
self.reject()
return False
self.accept() self.accept()
return True return True
def _doClose(self): def _doClose(self):
"""Close without saving the word list. """Close without saving the word list."""
"""
self._saveGuiSettings() self._saveGuiSettings()
self.reject() self.reject()
return return
@@ -190,29 +174,17 @@ class GuiWordList(QDialog):
## ##
def _loadWordList(self): def _loadWordList(self):
"""Load the project's word list, if it exists. """Load the project's word list, if it exists."""
""" userDict = UserDictionary(self.theProject)
wordList = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT) userDict.load()
if not isinstance(wordList, Path):
return False
self.listBox.clear() self.listBox.clear()
if not wordList.exists(): for word in userDict:
logger.debug("No project dictionary file found") if word:
return False self.listBox.addItem(word)
return
with open(wordList, mode="r", encoding="utf-8") as inFile:
for inLine in inFile:
theWord = inLine.strip()
if len(theWord) == 0:
continue
self.listBox.addItem(theWord)
return True
def _saveGuiSettings(self): def _saveGuiSettings(self):
"""Save GUI settings. """Save GUI settings."""
"""
winWidth = CONFIG.rpxInt(self.width()) winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
+3 -4
View File
@@ -52,7 +52,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass
from novelwriter.common import minmax, transferCase from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.core.index import countWords from novelwriter.core.index import countWords
from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.core.spellcheck import NWSpellEnchant
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -131,7 +131,7 @@ class GuiDocEditor(QTextEdit):
self.docSearch = GuiDocEditSearch(self) self.docSearch = GuiDocEditSearch(self)
# Syntax # Syntax
self.spEnchant = NWSpellEnchant() self.spEnchant = NWSpellEnchant(self.theProject)
self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant) self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant)
# Context Menu # Context Menu
@@ -703,8 +703,7 @@ class GuiDocEditor(QTextEdit):
else: else:
theLang = self.theProject.data.spellLang theLang = self.theProject.data.spellLang
projDict = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT) self.spEnchant.setLanguage(theLang)
self.spEnchant.setLanguage(theLang, projDict)
_, theProvider = self.spEnchant.describeDict() _, theProvider = self.spEnchant.describeDict()
self.spellDictionaryChanged.emit(str(theLang), str(theProvider)) self.spellDictionaryChanged.emit(str(theLang), str(theProvider))
+4 -4
View File
@@ -37,18 +37,18 @@ def testCoreSpell_FakeEnchant(monkeypatch):
mp.setitem(sys.modules, "enchant", None) mp.setitem(sys.modules, "enchant", None)
spChk = NWSpellEnchant() spChk = NWSpellEnchant()
spChk.setLanguage("en_US", "") spChk.setLanguage("en_US", "")
assert isinstance(spChk._theDict, FakeEnchant) assert isinstance(spChk._dictObj, FakeEnchant)
# Request a non-existent dictionary # Request a non-existent dictionary
spChk = NWSpellEnchant() spChk = NWSpellEnchant()
spChk.setLanguage("whatchamajig", "") spChk.setLanguage("whatchamajig", "")
assert isinstance(spChk._theDict, FakeEnchant) assert isinstance(spChk._dictObj, FakeEnchant)
# Request an emety language string # Request an emety language string
# See issue https://github.com/vkbo/novelWriter/issues/1096 # See issue https://github.com/vkbo/novelWriter/issues/1096
spChk = NWSpellEnchant() spChk = NWSpellEnchant()
spChk.setLanguage("", "") spChk.setLanguage("", "")
assert isinstance(spChk._theDict, FakeEnchant) assert isinstance(spChk._dictObj, FakeEnchant)
# FakeEnchant should handle requests # FakeEnchant should handle requests
fkChk = FakeEnchant() fkChk = FakeEnchant()
@@ -96,7 +96,7 @@ def testCoreSpell_Enchant(monkeypatch, fncPath):
assert spChk._readProjectDictionary(None) is False assert spChk._readProjectDictionary(None) is False
assert spChk._readProjectDictionary(wList) is True assert spChk._readProjectDictionary(wList) is True
assert spChk._projectDict == wList assert spChk._userDictPath == wList
# Cannot write to file # Cannot write to file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
+1 -2
View File
@@ -27,7 +27,6 @@ from PyQt5.QtWidgets import QDialog, QAction
from tools import buildTestProject, writeFile, readFile, getGuiItem from tools import buildTestProject, writeFile, readFile, getGuiItem
from mocked import causeOSError from mocked import causeOSError
from novelwriter.constants import nwFiles
from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.wordlist import GuiWordList
@@ -43,7 +42,7 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath):
# Open project # Open project
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
dictFile = projPath / "meta" / nwFiles.PROJ_DICT dictFile = projPath / "meta" / "wordlist.txt"
# Load the dialog # Load the dialog
nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger) nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger)
+1 -2
View File
@@ -28,7 +28,6 @@ from tools import getGuiItem, writeFile, buildTestProject
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction, QFileDialog from PyQt5.QtWidgets import QAction, QFileDialog
from novelwriter.constants import nwFiles
from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.tools.writingstats import GuiWritingStats
@@ -40,7 +39,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
qtbot.wait(100) qtbot.wait(100)
assert nwGUI.saveProject() assert nwGUI.saveProject()
sessFile = projPath / "meta" / nwFiles.SESS_STATS sessFile = projPath / "meta" / "sessionStats.log"
# Open the Writing Stats dialog # Open the Writing Stats dialog
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)