diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 40bbb2c6..81f8454a 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -19,6 +19,6 @@ Please check the following before you make a pull request:
* [ ] The header of all files contain a reference to the repository license
* [ ] The overall test coverage is increased or remains the same as before
* [ ] All tests are passing
-* [ ] All flake8 checks are passing and the style guide is followed
+* [ ] All linting checks are passing and the style guide is followed
* [ ] Documentation (as docstrings) is complete and understandable
* [ ] Only files that have been actively changed are committed
diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml
index 7a2f2844..5fd90549 100644
--- a/.github/workflows/syntax.yml
+++ b/.github/workflows/syntax.yml
@@ -1,4 +1,4 @@
-name: Flake8
+name: Linting
on:
push:
@@ -21,10 +21,14 @@ jobs:
architecture: x64
- name: Checkout Source
uses: actions/checkout@v4
- - name: Install flake8
- run: pip install -r requirements-dev.txt
- - name: Syntax Check
+ - name: Install Dependencies
+ run: pip install -r requirements.txt -r requirements-dev.txt
+ - name: Run Flake8
run: |
flake8 --version
flake8 novelwriter --count --show-source --statistics
flake8 tests --count --show-source --statistics --extend-ignore ANN
+ - name: Run Pyright
+ run: |
+ pyright --version
+ pyright
diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml
index b5f744f2..b13e19c1 100644
--- a/.github/workflows/test_linux.yml
+++ b/.github/workflows/test_linux.yml
@@ -26,7 +26,7 @@ jobs:
- name: Install Packages (apt)
run: |
sudo apt update
- sudo apt install libenchant-2-dev qttools5-dev-tools aspell-en
+ sudo apt install libenchant-2-dev qttools5-dev-tools
- name: Checkout Source
uses: actions/checkout@v4
- name: Install Dependencies (pip)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 771b6232..eb793aa9 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -537,12 +537,11 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
# XML Helpers
##
-def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
+def xmlIndent(xml: ET.Element | ET.ElementTree) -> None:
"""A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does.
"""
- if isinstance(tree, ET.ElementTree):
- tree = tree.getroot()
+ tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml
if not isinstance(tree, ET.Element):
return
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index b45021a7..bad604ff 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -43,11 +43,13 @@ from novelwriter.error import logException
logger = logging.getLogger(__name__)
+T_BuildValue = str | int | float | bool
+
# The Settings Template
# =====================
# Each entry contains a tuple on the form: (type, default)
-SETTINGS_TEMPLATE: dict[str, tuple[type, str | int | float | bool]] = {
+SETTINGS_TEMPLATE: dict[str, tuple[type, T_BuildValue]] = {
"filter.includeNovel": (bool, True),
"filter.includeNotes": (bool, False),
"filter.includeInactive": (bool, False),
@@ -378,7 +380,7 @@ class BuildSettings:
self._changed = True
return
- def setValue(self, key: str, value: str | int | float | bool) -> None:
+ def setValue(self, key: str, value: T_BuildValue) -> None:
"""Set a specific value for a build setting."""
if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
self._changed = value != self._settings[key]
@@ -502,7 +504,8 @@ class BuildSettings:
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
if isinstance(settings, dict):
for key, value in settings.items():
- self.setValue(RENAMED.get(key, key), value)
+ if isinstance(key, str) and isinstance(value, T_BuildValue):
+ self.setValue(RENAMED.get(key, key), value)
self._changed = False
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index d2e5e686..14e6553a 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -146,9 +146,9 @@ class GuiDocMerge(NDialog):
def _resetList(self) -> None:
"""Reset the content of the list box to its original state."""
logger.debug("Resetting list box content")
- sHandle = self._data.get("sHandle", None)
- itemList = self._data.get("origItems", [])
- self._loadContent(sHandle, itemList)
+ if sHandle := self._data.get("sHandle"):
+ itemList = self._data.get("origItems", [])
+ self._loadContent(sHandle, itemList)
return
##
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 3d8cb7b4..447d1696 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -192,8 +192,8 @@ class GuiDocSplit(NDialog):
@pyqtSlot()
def _reloadList(self) -> None:
"""Reload the content of the list box."""
- sHandle = self._data.get("sHandle", None)
- self._loadContent(sHandle)
+ if sHandle := self._data.get("sHandle"):
+ self._loadContent(sHandle)
return
##
diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py
index b6accdae..5a97edde 100644
--- a/novelwriter/extensions/switch.py
+++ b/novelwriter/extensions/switch.py
@@ -23,7 +23,7 @@ along with this program. If not, see .
"""
from __future__ import annotations
-from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty
+from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty # pyright: ignore
from PyQt6.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QResizeEvent
from PyQt6.QtWidgets import QAbstractButton, QWidget
@@ -60,7 +60,7 @@ class NSwitch(QAbstractButton):
def offset(self) -> int: # type: ignore
return self._offset
- @offset.setter
+ @offset.setter # type: ignore
def offset(self, offset: int) -> None:
self._offset = offset
self.update()
diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py
index 49ce6cbf..f44139a3 100644
--- a/novelwriter/formats/tokenizer.py
+++ b/novelwriter/formats/tokenizer.py
@@ -1034,6 +1034,7 @@ class Tokenizer(ABC):
def _formatComment(self, style: ComStyle, key: str, text: str) -> tuple[str, T_Formats]:
"""Apply formatting to comments and notes."""
+ rFmt = []
tTxt, tFmt = self._extractFormats(text)
tFmt.insert(0, (0, TextFmt.COL_B, style.textClass))
tFmt.append((len(tTxt), TextFmt.COL_E, ""))
diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py
index cc5005aa..57599f3b 100644
--- a/novelwriter/formats/toodt.py
+++ b/novelwriter/formats/toodt.py
@@ -598,6 +598,7 @@ class ToOdt(Tokenizer):
def _textStyle(self, hFmt: int, fClass: str = "") -> str:
"""Return a text style for a given style code."""
tKey = str(hFmt)
+ color = None
if fClass and (color := self._classes.get(fClass)):
tKey = f"{tKey}:{fClass}"
if tKey in self._autoText:
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 0e378390..40d6c657 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -473,7 +473,7 @@ class TextBlockData(QTextBlockUserData):
self._text = ""
self._offset = 0
self._metaData: list[tuple[int, int, str, str]] = []
- self._spellErrors: list[tuple[int, int,]] = []
+ self._spellErrors: list[tuple[int, int]] = []
return
@property
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index ae3a5648..46f5727e 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -681,9 +681,9 @@ class _OutlineWidget(QWidget):
hFont.setBold(True)
hFont.setUnderline(True)
+ indent = False
if root := self.listView.invisibleRootItem():
parent = root
- indent = False
for anchor, entry in data.items():
prefix, _, text = entry.partition("|")
if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
diff --git a/pyproject.toml b/pyproject.toml
index e7170be1..9fefaa8d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,6 +65,14 @@ exclude = ["docs/*"]
max_line_length = 99
ignore = ["E133", "E221", "E226", "E228", "E241", "W503"]
+[tool.pyright]
+include = ["novelwriter"]
+exclude = ["**/__pycache__"]
+
+reportIncompatibleMethodOverride = false
+
+pythonVersion = "3.10"
+
[tool.coverage.run]
branch = false
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 8499c6e8..919e83d0 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,6 @@
flake8
+flake8-annotations
flake8-pep585
flake8-pyproject
-flake8-annotations
isort
+pyright
diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
index e59f324e..da365c9d 100644
--- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd
+++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd
@@ -1,8 +1,8 @@
%%~name: New Scene
%%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT
-%%~hash: e4148ea77e78c90c334d5dc46c38a2b7904ac117
-%%~date: 2024-11-01 21:15:57/2024-11-01 21:16:01
+%%~hash: e3cdc10e73d6250cc4eb9c24fcc4fed1e72392ff
+%%~date: 2025-03-30 23:25:06/2025-03-30 23:25:11
# Novel
## Chapter
@@ -60,5 +60,3 @@ But don’t add a double space : See?
>>‘Right-aligned text’
-Some text with tesst in it.
-
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index d6a5ecae..e90fa531 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
Jane Doe
@@ -28,7 +28,7 @@
Main
-
+
-
Novel
@@ -46,7 +46,7 @@
New Chapter
-
-
+
New Scene
-
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 7b257670..7d2d4c52 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -37,6 +37,7 @@ from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
from novelwriter.gui.doceditor import GuiDocEditor, _TagAction
+from novelwriter.gui.dochighlight import TextBlockData
from novelwriter.text.counting import standardCounter
from novelwriter.types import (
QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone,
@@ -58,7 +59,7 @@ def getMenuForPos(editor: GuiDocEditor, pos: int, select: bool = False) -> QMenu
if select:
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
editor.setTextCursor(cursor)
- editor._openContextMenu(editor.cursorRect().center())
+ editor._openContextFromCursor()
for obj in editor.children():
if isinstance(obj, QMenu) and obj.objectName() == "ContextMenu":
return obj
@@ -514,9 +515,20 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
# ==============
SHARED.project.data.setSpellCheck(True)
+ cursor = docEditor.textCursor()
+ cursor.setPosition(16)
+ data = cursor.block().userData()
+ assert cursor.block().text().startswith("Lorem")
+ assert isinstance(data, TextBlockData)
+ data._spellErrors = [(0, 5)]
+
+ # No known position
+ assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, -1, [])
+
# With Suggestion
with monkeypatch.context() as mp:
- mp.setattr(docEditor._qDocument, "spellErrorAtPos", lambda *a: ("Lorem", 0, 5, ["Lorax"]))
+ mp.setattr(SHARED.spelling, "suggestWords", lambda *a: ["Lorax"])
+
ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
@@ -530,7 +542,8 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
# Without Suggestion
with monkeypatch.context() as mp:
- mp.setattr(docEditor._qDocument, "spellErrorAtPos", lambda *a: ("Lorax", 0, 5, []))
+ mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [])
+
ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
@@ -541,7 +554,8 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
# Add to Dictionary
with monkeypatch.context() as mp:
- mp.setattr(docEditor._qDocument, "spellErrorAtPos", lambda *a: ("Lorax", 0, 5, []))
+ mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [])
+
ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index c0b5e517..da2d3ba0 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -21,7 +21,6 @@ along with this program. If not, see .
from __future__ import annotations
import shutil
-import sys
from pathlib import Path
from shutil import copyfile
@@ -30,7 +29,7 @@ import pytest
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPalette
-from PyQt6.QtWidgets import QInputDialog, QMenu, QMessageBox
+from PyQt6.QtWidgets import QInputDialog, QMessageBox
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwFiles
@@ -566,32 +565,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
docEditor._wCounterDoc.run()
- # Spell Checking
- # ==============
-
- for c in "Some text with tesst in it.":
- qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
- qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
- qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
-
- currPos = docEditor.getCursorPosition()
- assert docEditor._qDocument.spellErrorAtPos(currPos) == ("", -1, -1, [])
-
- errPos = currPos - 13
- if not sys.platform.startswith("win32"):
- # Skip on Windows as spell checking is off there
- # This check will fail without an 'en' dictionary, like aspell-en
- word, cPos, cLen, suggest = docEditor._qDocument.spellErrorAtPos(errPos)
- assert word == "tesst"
- assert cPos == 15
- assert cLen == 5
- assert "test" in suggest
-
- with monkeypatch.context() as mp:
- mp.setattr(QMenu, "exec", lambda *a: None)
- docEditor.setCursorPosition(errPos)
- docEditor._openContextFromCursor()
-
# Check Files
# ===========