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 # ===========