Merge patches from 1.6.3 (#1101)

This commit is contained in:
Veronica Berglyd Olsen
2022-08-17 23:32:43 +02:00
committed by GitHub
7 changed files with 158 additions and 38 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
@@ -358,7 +358,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):
+21 -5
View File
@@ -712,7 +712,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
@@ -1991,12 +1991,14 @@ class GuiDocEditor(QTextEdit):
tCheck = tInsert tCheck = tInsert
if tCheck in self.mainConf.fmtPadBefore: if tCheck in self.mainConf.fmtPadBefore:
nDelete = max(nDelete, 1) if self.allowSpaceBeforeColon(theText, tCheck):
tInsert = self._typPadChar + tInsert nDelete = max(nDelete, 1)
tInsert = self._typPadChar + tInsert
if tCheck in self.mainConf.fmtPadAfter: if tCheck in self.mainConf.fmtPadAfter:
nDelete = max(nDelete, 1) if self.allowSpaceBeforeColon(theText, tCheck):
tInsert = tInsert + self._typPadChar nDelete = max(nDelete, 1)
tInsert = tInsert + self._typPadChar
if nDelete > 0: if nDelete > 0:
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete) theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
@@ -2004,6 +2006,20 @@ class GuiDocEditor(QTextEdit):
return return
@staticmethod
def _allowSpaceBeforeColon(text, char):
"""Special checker function only used by the insert space
feature for French, Spanish, etc, so it doesn't insert a
space before colons in meta data lines. See issue #1090.
"""
if char == ":" and len(text) > 1:
if text[0] == "@":
return False
if text[0] == "%":
if text[1:].lstrip()[:9].lower() == "synopsis:":
return False
return True
def _updateHeaders(self, checkPos=False, checkLevel=False): def _updateHeaders(self, checkPos=False, checkLevel=False):
"""Update the headers record and return True if anything """Update the headers record and return True if anything
changed, if a check flag was provided. changed, if a check flag was provided.
+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
+28 -1
View File
@@ -1190,7 +1190,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
@pytest.mark.gui @pytest.mark.gui
def testGuiEditor_WordCounters(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumText): def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
"""Test saving text from the editor. """Test saving text from the editor.
""" """
# Block message box # Block message box
@@ -1488,3 +1488,30 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
# END Test testGuiEditor_Search # END Test testGuiEditor_Search
@pytest.mark.gui
def testGuiEditor_StaticMethods():
"""Test the document editor's static methods.
"""
# Check the method that decides if it is allowed to insert a space
# before a colon using the French, Spanish, etc language feature
assert GuiDocEditor._allowSpaceBeforeColon("", "") is True
assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True
assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True
# END Test testGuiEditor_StaticMethods
+1 -1
View File
@@ -472,7 +472,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
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, ignoreStart=XML_IGNORE) assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "<spellCheck"))
projFile = os.path.join(fncProj, "content", "000000000000f.nwd") projFile = os.path.join(fncProj, "content", "000000000000f.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd") testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd")