Add Pyright to syntax workflow and fix errors (#2279)

This commit is contained in:
Veronica Berglyd Olsen
2025-03-30 23:57:41 +02:00
committed by GitHub
18 changed files with 64 additions and 62 deletions
+1 -1
View File
@@ -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 header of all files contain a reference to the repository license
* [ ] The overall test coverage is increased or remains the same as before * [ ] The overall test coverage is increased or remains the same as before
* [ ] All tests are passing * [ ] 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 * [ ] Documentation (as docstrings) is complete and understandable
* [ ] Only files that have been actively changed are committed * [ ] Only files that have been actively changed are committed
+8 -4
View File
@@ -1,4 +1,4 @@
name: Flake8 name: Linting
on: on:
push: push:
@@ -21,10 +21,14 @@ jobs:
architecture: x64 architecture: x64
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install flake8 - name: Install Dependencies
run: pip install -r requirements-dev.txt run: pip install -r requirements.txt -r requirements-dev.txt
- name: Syntax Check - name: Run Flake8
run: | run: |
flake8 --version flake8 --version
flake8 novelwriter --count --show-source --statistics flake8 novelwriter --count --show-source --statistics
flake8 tests --count --show-source --statistics --extend-ignore ANN flake8 tests --count --show-source --statistics --extend-ignore ANN
- name: Run Pyright
run: |
pyright --version
pyright
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- name: Install Packages (apt) - name: Install Packages (apt)
run: | run: |
sudo apt update 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 - name: Checkout Source
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Dependencies (pip) - name: Install Dependencies (pip)
+2 -3
View File
@@ -537,12 +537,11 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
# XML Helpers # 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 """A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does. library. It behaves more closely to how the one from lxml does.
""" """
if isinstance(tree, ET.ElementTree): tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml
tree = tree.getroot()
if not isinstance(tree, ET.Element): if not isinstance(tree, ET.Element):
return return
+6 -3
View File
@@ -43,11 +43,13 @@ from novelwriter.error import logException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T_BuildValue = str | int | float | bool
# The Settings Template # The Settings Template
# ===================== # =====================
# Each entry contains a tuple on the form: (type, default) # 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.includeNovel": (bool, True),
"filter.includeNotes": (bool, False), "filter.includeNotes": (bool, False),
"filter.includeInactive": (bool, False), "filter.includeInactive": (bool, False),
@@ -378,7 +380,7 @@ class BuildSettings:
self._changed = True self._changed = True
return 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.""" """Set a specific value for a build setting."""
if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]): if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
self._changed = value != self._settings[key] self._changed = value != self._settings[key]
@@ -502,7 +504,8 @@ class BuildSettings:
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()} self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
if isinstance(settings, dict): if isinstance(settings, dict):
for key, value in settings.items(): 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 self._changed = False
+3 -3
View File
@@ -146,9 +146,9 @@ class GuiDocMerge(NDialog):
def _resetList(self) -> None: def _resetList(self) -> None:
"""Reset the content of the list box to its original state.""" """Reset the content of the list box to its original state."""
logger.debug("Resetting list box content") logger.debug("Resetting list box content")
sHandle = self._data.get("sHandle", None) if sHandle := self._data.get("sHandle"):
itemList = self._data.get("origItems", []) itemList = self._data.get("origItems", [])
self._loadContent(sHandle, itemList) self._loadContent(sHandle, itemList)
return return
## ##
+2 -2
View File
@@ -192,8 +192,8 @@ class GuiDocSplit(NDialog):
@pyqtSlot() @pyqtSlot()
def _reloadList(self) -> None: def _reloadList(self) -> None:
"""Reload the content of the list box.""" """Reload the content of the list box."""
sHandle = self._data.get("sHandle", None) if sHandle := self._data.get("sHandle"):
self._loadContent(sHandle) self._loadContent(sHandle)
return return
## ##
+2 -2
View File
@@ -23,7 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations 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.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QResizeEvent
from PyQt6.QtWidgets import QAbstractButton, QWidget from PyQt6.QtWidgets import QAbstractButton, QWidget
@@ -60,7 +60,7 @@ class NSwitch(QAbstractButton):
def offset(self) -> int: # type: ignore def offset(self) -> int: # type: ignore
return self._offset return self._offset
@offset.setter @offset.setter # type: ignore
def offset(self, offset: int) -> None: def offset(self, offset: int) -> None:
self._offset = offset self._offset = offset
self.update() self.update()
+1
View File
@@ -1034,6 +1034,7 @@ class Tokenizer(ABC):
def _formatComment(self, style: ComStyle, key: str, text: str) -> tuple[str, T_Formats]: def _formatComment(self, style: ComStyle, key: str, text: str) -> tuple[str, T_Formats]:
"""Apply formatting to comments and notes.""" """Apply formatting to comments and notes."""
rFmt = []
tTxt, tFmt = self._extractFormats(text) tTxt, tFmt = self._extractFormats(text)
tFmt.insert(0, (0, TextFmt.COL_B, style.textClass)) tFmt.insert(0, (0, TextFmt.COL_B, style.textClass))
tFmt.append((len(tTxt), TextFmt.COL_E, "")) tFmt.append((len(tTxt), TextFmt.COL_E, ""))
+1
View File
@@ -598,6 +598,7 @@ class ToOdt(Tokenizer):
def _textStyle(self, hFmt: int, fClass: str = "") -> str: def _textStyle(self, hFmt: int, fClass: str = "") -> str:
"""Return a text style for a given style code.""" """Return a text style for a given style code."""
tKey = str(hFmt) tKey = str(hFmt)
color = None
if fClass and (color := self._classes.get(fClass)): if fClass and (color := self._classes.get(fClass)):
tKey = f"{tKey}:{fClass}" tKey = f"{tKey}:{fClass}"
if tKey in self._autoText: if tKey in self._autoText:
+1 -1
View File
@@ -473,7 +473,7 @@ class TextBlockData(QTextBlockUserData):
self._text = "" self._text = ""
self._offset = 0 self._offset = 0
self._metaData: list[tuple[int, int, str, str]] = [] self._metaData: list[tuple[int, int, str, str]] = []
self._spellErrors: list[tuple[int, int,]] = [] self._spellErrors: list[tuple[int, int]] = []
return return
@property @property
+1 -1
View File
@@ -681,9 +681,9 @@ class _OutlineWidget(QWidget):
hFont.setBold(True) hFont.setBold(True)
hFont.setUnderline(True) hFont.setUnderline(True)
indent = False
if root := self.listView.invisibleRootItem(): if root := self.listView.invisibleRootItem():
parent = root parent = root
indent = False
for anchor, entry in data.items(): for anchor, entry in data.items():
prefix, _, text = entry.partition("|") prefix, _, text = entry.partition("|")
if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"): if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
+8
View File
@@ -65,6 +65,14 @@ exclude = ["docs/*"]
max_line_length = 99 max_line_length = 99
ignore = ["E133", "E221", "E226", "E228", "E241", "W503"] ignore = ["E133", "E221", "E226", "E228", "E241", "W503"]
[tool.pyright]
include = ["novelwriter"]
exclude = ["**/__pycache__"]
reportIncompatibleMethodOverride = false
pythonVersion = "3.10"
[tool.coverage.run] [tool.coverage.run]
branch = false branch = false
+2 -1
View File
@@ -1,5 +1,6 @@
flake8 flake8
flake8-annotations
flake8-pep585 flake8-pep585
flake8-pyproject flake8-pyproject
flake8-annotations
isort isort
pyright
@@ -1,8 +1,8 @@
%%~name: New Scene %%~name: New Scene
%%~path: 000000000000d/000000000000f %%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: e4148ea77e78c90c334d5dc46c38a2b7904ac117 %%~hash: e3cdc10e73d6250cc4eb9c24fcc4fed1e72392ff
%%~date: 2024-11-01 21:15:57/2024-11-01 21:16:01 %%~date: 2025-03-30 23:25:06/2025-03-30 23:25:11
# Novel # Novel
## Chapter ## Chapter
@@ -60,5 +60,3 @@ But dont add a double space : See?
>>Right-aligned text >>Right-aligned text
Some text with tesst in it.
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.6b1" hexVersion="0x020600b1" fileVersion="1.5" fileRevision="4" timeStamp="2024-11-23 23:35:44"> <novelWriterXML appVersion="2.7a3" hexVersion="0x020700a3" fileVersion="1.5" fileRevision="4" timeStamp="2025-03-30 23:23:22">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="4"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="5">
<name>New Project</name> <name>New Project</name>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry> <entry key="i000007" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
</importance> </importance>
</settings> </settings>
<content items="12" novelWords="179" notesWords="27"> <content items="12" novelWords="173" notesWords="27">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" /> <meta expanded="yes" />
<name status="s000000" import="i000004">Novel</name> <name status="s000000" import="i000004">Novel</name>
@@ -46,7 +46,7 @@
<name status="s000000" import="i000004" active="yes">New Chapter</name> <name status="s000000" import="i000004" active="yes">New Chapter</name>
</item> </item>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="1003" wordCount="172" paraCount="17" cursorPos="1259" /> <meta expanded="no" heading="H1" charCount="976" wordCount="166" paraCount="16" cursorPos="1243" />
<name status="s000000" import="i000004" active="yes">New Scene</name> <name status="s000000" import="i000004" active="yes">New Scene</name>
</item> </item>
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT"> <item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
+18 -4
View File
@@ -37,6 +37,7 @@ from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
from novelwriter.gui.doceditor import GuiDocEditor, _TagAction from novelwriter.gui.doceditor import GuiDocEditor, _TagAction
from novelwriter.gui.dochighlight import TextBlockData
from novelwriter.text.counting import standardCounter from novelwriter.text.counting import standardCounter
from novelwriter.types import ( from novelwriter.types import (
QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone, QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone,
@@ -58,7 +59,7 @@ def getMenuForPos(editor: GuiDocEditor, pos: int, select: bool = False) -> QMenu
if select: if select:
cursor.select(QTextCursor.SelectionType.WordUnderCursor) cursor.select(QTextCursor.SelectionType.WordUnderCursor)
editor.setTextCursor(cursor) editor.setTextCursor(cursor)
editor._openContextMenu(editor.cursorRect().center()) editor._openContextFromCursor()
for obj in editor.children(): for obj in editor.children():
if isinstance(obj, QMenu) and obj.objectName() == "ContextMenu": if isinstance(obj, QMenu) and obj.objectName() == "ContextMenu":
return obj return obj
@@ -514,9 +515,20 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
# ============== # ==============
SHARED.project.data.setSpellCheck(True) 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 Suggestion
with monkeypatch.context() as mp: 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) ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()] 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 # Without Suggestion
with monkeypatch.context() as mp: 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) ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()] 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 # Add to Dictionary
with monkeypatch.context() as mp: 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) ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()] actions = [x.text() for x in ctxMenu.actions() if x.text()]
+1 -28
View File
@@ -21,7 +21,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
import shutil import shutil
import sys
from pathlib import Path from pathlib import Path
from shutil import copyfile from shutil import copyfile
@@ -30,7 +29,7 @@ import pytest
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPalette 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 import CONFIG, SHARED
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
@@ -566,32 +565,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
docEditor._wCounterDoc.run() 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 # Check Files
# =========== # ===========