Fix handling of empty spellcheck language string (#1098)

This commit is contained in:
Veronica Berglyd Olsen
2022-08-17 22:34:45 +02:00
committed by GitHub
6 changed files with 110 additions and 33 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Install Packages (apt) - name: Install Packages (apt)
run: | run: |
sudo apt update sudo apt update
sudo apt install libenchant-dev qttools5-dev-tools sudo apt install libenchant-dev qttools5-dev-tools aspell-en
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Dependencies (pip) - name: Install Dependencies (pip)
+1 -1
View File
@@ -360,7 +360,7 @@ class Config:
# Check the availability of optional packages # Check the availability of optional packages
self._checkOptionalPackages() self._checkOptionalPackages()
if self.spellLanguage is None: if not self.spellLanguage:
self.spellLanguage = "en" self.spellLanguage = "en"
# Look for a PDF version of the manual # Look for a PDF version of the manual
+30 -14
View File
@@ -26,6 +26,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os import os
import logging import logging
from collections import namedtuple
from novelwriter.error import logException from novelwriter.error import logException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -46,36 +48,47 @@ class NWSpellEnchant():
return return
## ##
# Getters and Setters # Properties
## ##
@property
def spellLanguage(self): def spellLanguage(self):
return self._spellLanguage return self._spellLanguage
##
# Setters
##
def setLanguage(self, theLang, projectDict=None): def setLanguage(self, theLang, projectDict=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. crash. Note that enchant will allow loading an empty string as
a tag, but this will fail later on. See issue #1096.
""" """
self._theBroker = None
self._theDict = None
self._spellLanguage = None
try: try:
import enchant import enchant
if self._theBroker is not None:
logger.debug("Deleting old pyenchant broker")
del self._theBroker
self._theBroker = enchant.Broker() if theLang and enchant.dict_exists(theLang):
self._theDict = self._theBroker.request_dict(theLang) self._theBroker = enchant.Broker()
self._spellLanguage = theLang self._theDict = self._theBroker.request_dict(theLang)
logger.debug("Enchant spell checking for language '%s' loaded", theLang) self._spellLanguage = theLang
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
else:
logger.warning("Enchant found no dictionary for language '%s'", theLang)
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'", theLang)
self._theDict = FakeEnchant()
self._spellLanguage = None
self._readProjectDictionary(projectDict) if self._theDict is None:
for pWord in self._projDict: self._theDict = FakeEnchant()
self._theDict.add_to_session(pWord) else:
self._readProjectDictionary(projectDict)
for pWord in self._projDict:
self._theDict.add_to_session(pWord)
return return
@@ -189,6 +202,9 @@ 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):
self.tag = ""
self.provider = namedtuple("provider", "name")
self.provider.name = ""
return return
def check(self, theWord): def check(self, theWord):
+1 -1
View File
@@ -718,7 +718,7 @@ class GuiDocEditor(QTextEdit):
), nwAlert.INFO) ), nwAlert.INFO)
theMode = False theMode = False
if self.spEnchant.spellLanguage() is None: if self.spEnchant.spellLanguage is None:
theMode = False theMode = False
self._spellCheck = theMode self._spellCheck = theMode
+76 -15
View File
@@ -26,29 +26,57 @@ import pytest
from mock import causeOSError from mock import causeOSError
from tools import readFile, writeFile from tools import readFile, writeFile
from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.core.spellcheck import FakeEnchant, NWSpellEnchant
@pytest.mark.core @pytest.mark.core
def testCoreSpell_Enchant(monkeypatch, tmpDir): def testCoreSpell_FakeEnchant(monkeypatch):
"""Test the pyenchant spell checker """Test the FakeEnchant spell checker fallback.
""" """
wList = os.path.join(tmpDir, "wordlist.txt") # Make package import fail
writeFile(wList, "a_word\nb_word\nc_word\n")
# Block the enchant package (and trigger the default class)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setitem(sys.modules, "enchant", None) mp.setitem(sys.modules, "enchant", None)
spChk = NWSpellEnchant() spChk = NWSpellEnchant()
spChk.setLanguage("en_US", "")
assert isinstance(spChk._theDict, FakeEnchant)
spChk.setLanguage("en", wList) # Request a non-existent dictionary
assert spChk.setLanguage("", "") is None spChk = NWSpellEnchant()
assert spChk.checkWord("") is True spChk.setLanguage("whatchamajig", "")
assert spChk.suggestWords("") == [] assert isinstance(spChk._theDict, FakeEnchant)
# Request an emety language string
# See issue https://github.com/vkbo/novelWriter/issues/1096
spChk = NWSpellEnchant()
spChk.setLanguage("", "")
assert isinstance(spChk._theDict, FakeEnchant)
# FakeEnchant should handle requests
fkChk = FakeEnchant()
assert fkChk.tag == ""
assert fkChk.provider.name == ""
assert fkChk.check("whatchamajig") is True
assert fkChk.suggest("whatchamajig") == []
assert fkChk.add_to_session("whatchamajig") is None
# END Test testCoreSpell_FakeEnchant
@pytest.mark.core
def testCoreSpell_Enchant(monkeypatch, fncDir):
"""Test the pyenchant spell checker.
"""
wList = os.path.join(fncDir, "wordlist.txt")
writeFile(wList, "a_word\nb_word\nc_word\n")
# Break the enchant package, and check error handling
with monkeypatch.context() as mp:
mp.setitem(sys.modules, "enchant", None)
spChk = NWSpellEnchant()
assert spChk.listDictionaries() == [] assert spChk.listDictionaries() == []
assert spChk.describeDict() == ("", "") assert spChk.describeDict() == ("", "")
# Break the enchant package, and check error handling # Set the dict to None, and check dictionary call error handling
spChk = NWSpellEnchant() spChk = NWSpellEnchant()
spChk.theDict = None spChk.theDict = None
assert spChk.checkWord("word") is True assert spChk.checkWord("word") is True
@@ -57,8 +85,9 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
# Load the proper enchant package (twice) # Load the proper enchant package (twice)
spChk = NWSpellEnchant() spChk = NWSpellEnchant()
spChk.setLanguage("en", wList) spChk.setLanguage("en_US", wList)
spChk.setLanguage("en", wList) spChk.setLanguage("en_US", wList)
assert spChk.spellLanguage == "en_US"
# Add a word to the user's dictionary # Add a word to the user's dictionary
assert spChk._readProjectDictionary("stuff") is False assert spChk._readProjectDictionary("stuff") is False
@@ -98,7 +127,39 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
assert len(dList) > 0 assert len(dList) > 0
aTag, aName = spChk.describeDict() aTag, aName = spChk.describeDict()
assert aTag == "en" assert aTag == "en_US"
assert aName != "" assert aName != ""
# END Test testCoreSpell_Enchant # END Test testCoreSpell_Enchant
@pytest.mark.core
def testCoreSpell_SessionWords(fncDir):
"""Test the handling of the custom word list in the spell checker.
New project sessions should not inherit the project word list from
other sessions, so this test checks that they don't bleed through.
"""
wList1 = os.path.join(fncDir, "wordlist1.txt")
wList2 = os.path.join(fncDir, "wordlist2.txt")
writeFile(wList1, "a_word\nb_word\nc_word\n")
writeFile(wList2, "d_word\ne_word\nf_word\n")
spChk = NWSpellEnchant()
spChk.setLanguage("en_US", wList1)
assert spChk.checkWord("a_word") is True
assert spChk.checkWord("b_word") is True
assert spChk.checkWord("c_word") is True
assert spChk.checkWord("d_word") is False
assert spChk.checkWord("e_word") is False
assert spChk.checkWord("f_word") is False
spChk.setLanguage("en_US", wList2)
assert spChk.checkWord("a_word") is False
assert spChk.checkWord("b_word") is False
assert spChk.checkWord("c_word") is False
assert spChk.checkWord("d_word") is True
assert spChk.checkWord("e_word") is True
assert spChk.checkWord("f_word") is True
# END Test testCoreSpell_SessionWords
+1 -1
View File
@@ -439,7 +439,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx") testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx")
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx") compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx")
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 13])
projFile = os.path.join(fncProj, "content", "031b4af5197ec.nwd") projFile = os.path.join(fncProj, "content", "031b4af5197ec.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd")