Add linting of type annotations (#1809)

This commit is contained in:
Veronica Berglyd Olsen
2024-04-09 17:05:45 +02:00
committed by GitHub
22 changed files with 65 additions and 47 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
[flake8] [flake8]
ignore = E133,E221,E226,E228,E241,W503 ignore = E133,E221,E226,E228,E241,W503,ANN101,ANN102,ANN401
per-file-ignores=tests/*:ANN
max-line-length = 99 max-line-length = 99
exclude = docs/* exclude = docs/*
+2 -2
View File
@@ -22,9 +22,9 @@ jobs:
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install flake8 - name: Install flake8
run: pip install flake8 flake8-pep585 run: pip install -r requirements-dev.txt
- name: Syntax Check - name: Syntax Check
run: | run: |
flake8 --version flake8 --version
flake8 novelwriter --count --show-source --statistics flake8 novelwriter --count --show-source --statistics
flake8 tests --count --show-source --statistics flake8 tests --count --show-source --statistics --extend-ignore ANN
+10 -3
View File
@@ -23,16 +23,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import sys
import getopt import getopt
import logging import logging
import sys
from typing import TYPE_CHECKING
from PyQt5.QtWidgets import QApplication, QErrorMessage from PyQt5.QtWidgets import QApplication, QErrorMessage
from novelwriter.error import exceptionHandler, logException
from novelwriter.config import Config from novelwriter.config import Config
from novelwriter.error import exceptionHandler, logException
from novelwriter.shared import SharedData from novelwriter.shared import SharedData
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
# Package Meta # Package Meta
# ============ # ============
@@ -60,7 +65,7 @@ CONFIG = Config()
SHARED = SharedData() SHARED = SharedData()
def main(sysArgs: list | None = None): def main(sysArgs: list | None = None) -> GuiMain | None:
"""Parse command line, set up logging, and launch main GUI.""" """Parse command line, set up logging, and launch main GUI."""
if sysArgs is None: if sysArgs is None:
sysArgs = sys.argv[1:] sysArgs = sys.argv[1:]
@@ -239,4 +244,6 @@ def main(sysArgs: list | None = None):
sys.exit(nwApp.exec()) sys.exit(nwApp.exec())
return None
# END Function main # END Function main
+5 -2
View File
@@ -449,7 +449,7 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
indentations = ["\n"] indentations = ["\n"]
def indentChildren(elem, level): def indentChildren(elem: ET.Element, level: int) -> None:
chLevel = level + 1 chLevel = level + 1
try: try:
chIndent = indentations[chLevel] chIndent = indentations[chLevel]
@@ -472,6 +472,8 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
if last is not None: if last is not None:
last.tail = indentations[level] last.tail = indentations[level]
return
if len(tree): if len(tree):
indentChildren(tree, 0) indentChildren(tree, 0)
tree.tail = "\n" tree.tail = "\n"
@@ -534,8 +536,9 @@ class NWConfigParser(ConfigParser):
helper functions, and support for lists. helper functions, and support for lists.
""" """
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
return
def rdStr(self, section: str, option: str, default: str) -> str: def rdStr(self, section: str, option: str, default: str) -> str:
"""Read string value.""" """Read string value."""
+1 -1
View File
@@ -382,7 +382,7 @@ class BuildSettings:
postponed = [] postponed = []
def allowRoot(rHandle): def allowRoot(rHandle: str | None) -> None:
if rHandle in postponed and rHandle in result and rHandle is not None: if rHandle in postponed and rHandle in result and rHandle is not None:
result[rHandle] = (True, FilterMode.ROOT) result[rHandle] = (True, FilterMode.ROOT)
postponed.remove(rHandle) postponed.remove(rHandle)
+1 -1
View File
@@ -449,7 +449,7 @@ class ProjectXMLReader:
result[xEntry.attrib["key"]] = checkString(xEntry.text, "") result[xEntry.attrib["key"]] = checkString(xEntry.text, "")
return result return result
def _parseDictTagText(self, xItem) -> dict: def _parseDictTagText(self, xItem: ET.Element) -> dict:
"""Parse a dictionary stored with key as the tag and the value """Parse a dictionary stored with key as the tag and the value
as the text property. as the text property.
""" """
+3 -3
View File
@@ -74,7 +74,7 @@ class NWSpellEnchant:
# Setters # Setters
## ##
def setLanguage(self, language: str | None): def setLanguage(self, language: str | None) -> 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. Note that enchant will allow loading an empty string as crash. Note that enchant will allow loading an empty string as
@@ -182,10 +182,10 @@ class FakeEnchant:
def check(self, word: str) -> bool: def check(self, word: str) -> bool:
return True return True
def suggest(self, word) -> list[str]: def suggest(self, word: str) -> list[str]:
return [] return []
def add_to_session(self, word: str): def add_to_session(self, word: str) -> None:
return return
# END Class FakeEnchant # END Class FakeEnchant
+1 -1
View File
@@ -49,7 +49,7 @@ ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL) RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
def stripEscape(text) -> str: def stripEscape(text: str) -> str:
"""Strip escaped Markdown characters from paragraph text.""" """Strip escaped Markdown characters from paragraph text."""
if "\\" in text: if "\\" in text:
return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text) return RX_ESC.sub(lambda x: ESCAPES[x.group(0)], text)
+1 -1
View File
@@ -548,7 +548,7 @@ class ToOdt(Tokenizer):
oVers = _mkTag("office", "version") oVers = _mkTag("office", "version")
xSett = ET.Element(oRoot, attrib={oVers: X_VERS}) xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
def putInZip(name, xObj, zipObj): def putInZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
with zipObj.open(name, mode="w") as fObj: with zipObj.open(name, mode="w") as fObj:
xml = ET.ElementTree(xObj) xml = ET.ElementTree(xObj)
xml.write(fObj, encoding="utf-8", xml_declaration=True) xml.write(fObj, encoding="utf-8", xml_declaration=True)
+1 -1
View File
@@ -795,7 +795,7 @@ class GuiPreferences(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _selectTextFont(self): def _selectTextFont(self) -> None:
"""Open the QFontDialog and set a font for the font style.""" """Open the QFontDialog and set a font for the font style."""
current = QFont() current = QFont()
current.setFamily(CONFIG.textFont) current.setFamily(CONFIG.textFont)
+1 -1
View File
@@ -54,7 +54,7 @@ def logException() -> None:
return return
def formatException(exc) -> str: def formatException(exc: BaseException) -> str:
"""Format an exception as a string the same way the default """Format an exception as a string the same way the default
exception handler does. exception handler does.
""" """
+4 -4
View File
@@ -381,7 +381,7 @@ class GuiDocEditor(QPlainTextEdit):
return return
def loadText(self, tHandle: str, tLine=None) -> bool: def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""Load text from a document into the editor. If we have an I/O """Load text from a document into the editor. If we have an I/O
error, we must handle this and clear the editor so that we don't error, we must handle this and clear the editor so that we don't
risk overwriting the file if it exists. This can for instance risk overwriting the file if it exists. This can for instance
@@ -1080,7 +1080,7 @@ class GuiDocEditor(QPlainTextEdit):
return return
@pyqtSlot() @pyqtSlot()
def _cursorMoved(self): def _cursorMoved(self) -> None:
"""Triggered when the cursor moved in the editor.""" """Triggered when the cursor moved in the editor."""
self.docFooter.updateLineCount(self.textCursor()) self.docFooter.updateLineCount(self.textCursor())
return return
@@ -2186,7 +2186,7 @@ class MetaCompleter(QMenu):
# Internal Functions # Internal Functions
## ##
def _emitComplete(self, pos: int, length: int, value: str): def _emitComplete(self, pos: int, length: int, value: str) -> None:
"""Emit the signal to indicate a selection has been made.""" """Emit the signal to indicate a selection has been made."""
self.complete.emit(pos, length, value) self.complete.emit(pos, length, value)
return return
@@ -2967,7 +2967,7 @@ class GuiDocEditHeader(QWidget):
# Events # Events
## ##
def mousePressEvent(self, event: QMouseEvent): def mousePressEvent(self, event: QMouseEvent) -> None:
"""Capture a click on the title and ensure that the item is """Capture a click on the title and ensure that the item is
selected in the project tree. selected in the project tree.
""" """
+1 -1
View File
@@ -344,7 +344,7 @@ class GuiNovelToolBar(QWidget):
# Internal Functions # Internal Functions
## ##
def _addLastColAction(self, colType, actionLabel) -> None: def _addLastColAction(self, colType: NovelTreeColumn, actionLabel: str) -> None:
"""Add a column selection entry to the last column menu.""" """Add a column selection entry to the last column menu."""
aLast = self.mLastCol.addAction(actionLabel) aLast = self.mLastCol.addAction(actionLabel)
aLast.setCheckable(True) aLast.setCheckable(True)
+3 -3
View File
@@ -190,7 +190,7 @@ class GuiOutlineView(QWidget):
return return
@pyqtSlot(str) @pyqtSlot(str)
def _rootItemChanged(self, tHandle) -> None: def _rootItemChanged(self, tHandle: str) -> None:
"""Handle root novel changed or needs to be refreshed.""" """Handle root novel changed or needs to be refreshed."""
self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True) self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
return return
@@ -428,7 +428,7 @@ class GuiOutlineTree(QTreeWidget):
## ##
@property @property
def hiddenColumns(self): def hiddenColumns(self) -> dict[nwOutline, bool]:
return self._colHidden return self._colHidden
## ##
@@ -586,7 +586,7 @@ class GuiOutlineTree(QTreeWidget):
# Internal Functions # Internal Functions
## ##
def _loadHeaderState(self): def _loadHeaderState(self) -> None:
"""Load the state of the main tree header, that is, column order """Load the state of the main tree header, that is, column order
and column width. and column width.
""" """
+1 -1
View File
@@ -453,7 +453,7 @@ class GuiProjectToolBar(QWidget):
def _buildRootMenu(self) -> None: def _buildRootMenu(self) -> None:
"""Build the rood folder menu.""" """Build the rood folder menu."""
def addClass(itemClass): def addClass(itemClass: nwItemClass) -> None:
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])) aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
+2 -2
View File
@@ -944,7 +944,7 @@ class GuiMain(QMainWindow):
# Events # Events
## ##
def closeEvent(self, event: QCloseEvent): def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the closing event of the GUI and call the close """Capture the closing event of the GUI and call the close
function to handle all the close process steps. function to handle all the close process steps.
""" """
@@ -1201,7 +1201,7 @@ class GuiMain(QMainWindow):
return return
@pyqtSlot() @pyqtSlot()
def _toggleViewerPanelVisibility(self): def _toggleViewerPanelVisibility(self) -> None:
"""Toggle the visibility of the document viewer panel.""" """Toggle the visibility of the document viewer panel."""
CONFIG.showViewerPanel = not CONFIG.showViewerPanel CONFIG.showViewerPanel = not CONFIG.showViewerPanel
self.docViewerPanel.setVisible(CONFIG.showViewerPanel) self.docViewerPanel.setVisible(CONFIG.showViewerPanel)
+2 -2
View File
@@ -179,7 +179,7 @@ class GuiDictionaries(QDialog):
## ##
@pyqtSlot() @pyqtSlot()
def _doBrowseHunspell(self): def _doBrowseHunspell(self) -> None:
"""Browse for a Free/Libre Office dictionary.""" """Browse for a Free/Libre Office dictionary."""
ffilter = formatFileFilter([ ffilter = formatFileFilter([
(self.tr("Free or Libre Office extension"), "*.sox *.oxt"), "*" (self.tr("Free or Libre Office extension"), "*.sox *.oxt"), "*"
@@ -193,7 +193,7 @@ class GuiDictionaries(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _doImportHunspell(self): def _doImportHunspell(self) -> None:
"""Import a hunspell dictionary from .sox or .oxt file.""" """Import a hunspell dictionary from .sox or .oxt file."""
procErr = self.tr("Could not process dictionary file") procErr = self.tr("Could not process dictionary file")
if self._installPath: if self._installPath:
+8 -8
View File
@@ -60,7 +60,7 @@ class GuiManuscriptBuild(QDialog):
D_KEY = QtUserRole D_KEY = QtUserRole
def __init__(self, parent: QWidget, build: BuildSettings): def __init__(self, parent: QWidget, build: BuildSettings) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
logger.debug("Create: GuiManuscriptBuild") logger.debug("Create: GuiManuscriptBuild")
@@ -260,7 +260,7 @@ class GuiManuscriptBuild(QDialog):
## ##
@pyqtSlot("QAbstractButton*") @pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton): def _dialogButtonClicked(self, button: QAbstractButton) -> None:
"""Handle button clicks from the dialog button box.""" """Handle button clicks from the dialog button box."""
role = self.dlgButtons.buttonRole(button) role = self.dlgButtons.buttonRole(button)
if role == QtRoleAction: if role == QtRoleAction:
@@ -273,7 +273,7 @@ class GuiManuscriptBuild(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _doSelectPath(self): def _doSelectPath(self) -> None:
"""Select a folder for output.""" """Select a folder for output."""
bPath = Path(self.buildPath.text()) bPath = Path(self.buildPath.text())
bPath = bPath if bPath.is_dir() else self._build.lastPath bPath = bPath if bPath.is_dir() else self._build.lastPath
@@ -285,7 +285,7 @@ class GuiManuscriptBuild(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _doResetBuildName(self): def _doResetBuildName(self) -> None:
"""Generate a default build name.""" """Generate a default build name."""
bName = f"{SHARED.project.data.name} - {self._build.name}" bName = f"{SHARED.project.data.name} - {self._build.name}"
self.buildName.setText(bName) self.buildName.setText(bName)
@@ -293,7 +293,7 @@ class GuiManuscriptBuild(QDialog):
return return
@pyqtSlot() @pyqtSlot()
def _resetProgress(self): def _resetProgress(self) -> None:
"""Set the progress bar back to 0.""" """Set the progress bar back to 0."""
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
return return
@@ -350,7 +350,7 @@ class GuiManuscriptBuild(QDialog):
return items[0].data(self.D_KEY) return items[0].data(self.D_KEY)
return None return None
def _saveSettings(self): def _saveSettings(self) -> None:
"""Save the user GUI settings.""" """Save the user GUI settings."""
winWidth = CONFIG.rpxInt(self.width()) winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
@@ -369,7 +369,7 @@ class GuiManuscriptBuild(QDialog):
return return
def _populateContentList(self): def _populateContentList(self) -> None:
"""Build the content list.""" """Build the content list."""
rootMap = {} rootMap = {}
filtered = self._build.buildItemFilter(SHARED.project) filtered = self._build.buildItemFilter(SHARED.project)
@@ -398,7 +398,7 @@ class GuiManuscriptBuild(QDialog):
return return
def _openOutputFolder(self): def _openOutputFolder(self) -> None:
"""Open the build folder in the system's file explorer.""" """Open the build folder in the system's file explorer."""
openExternalPath(Path(self.buildPath.text())) openExternalPath(Path(self.buildPath.text()))
return return
+1 -1
View File
@@ -1312,7 +1312,7 @@ class _FormatTab(NScrollableForm):
return return
@pyqtSlot() @pyqtSlot()
def _pageSizeValueChanged(self): def _pageSizeValueChanged(self) -> None:
"""The user has changed the page size spin boxes, so we flip """The user has changed the page size spin boxes, so we flip
the page size box to Custom. the page size box to Custom.
""" """
+1 -1
View File
@@ -753,7 +753,7 @@ class _NewProjectForm(QWidget):
return return
@pyqtSlot() @pyqtSlot()
def _syncSwitches(self): def _syncSwitches(self) -> None:
"""Check if the add notes option should also be switched off.""" """Check if the add notes option should also be switched off."""
addPlot = self.addPlot.isChecked() addPlot = self.addPlot.isChecked()
addChar = self.addChar.isChecked() addChar = self.addChar.isChecked()
+13 -7
View File
@@ -47,7 +47,7 @@ def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
"""Extract the novelWriter version number without having to import """Extract the novelWriter version number without having to import
anything else from the main package. anything else from the main package.
""" """
def getValue(text): def getValue(text: str) -> str:
bits = text.partition("=") bits = text.partition("=")
return bits[2].strip().strip('"') return bits[2].strip().strip('"')
@@ -176,7 +176,7 @@ def cleanBuildDirs() -> None:
print("Cleaning up build environment ...") print("Cleaning up build environment ...")
print("") print("")
def removeFolder(rmDir): def removeFolder(rmDir: str) -> None:
if os.path.isdir(rmDir): if os.path.isdir(rmDir):
try: try:
shutil.rmtree(rmDir) shutil.rmtree(rmDir)
@@ -320,7 +320,7 @@ def buildQtI18nTS(sysArgs: list[str]) -> None:
print("=============================") print("=============================")
try: try:
from PyQt6.lupdate import lupdate from PyQt6.lupdate.lupdate import lupdate
except ImportError: except ImportError:
print("ERROR: This command requires lupdate from PyQt6") print("ERROR: This command requires lupdate from PyQt6")
print("On Debian/Ubuntu, install: pyqt6-dev-tools") print("On Debian/Ubuntu, install: pyqt6-dev-tools")
@@ -911,7 +911,7 @@ def makeAppImage(sysArgs: list[str]) -> list[str]:
import argparse import argparse
try: try:
import python_appimage # noqa F401 import python_appimage # noqa: F401 # type: ignore
except ImportError: except ImportError:
print( print(
"ERROR: Package 'python-appimage' is missing on this system.\n" "ERROR: Package 'python-appimage' is missing on this system.\n"
@@ -1252,12 +1252,12 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None:
# Clean Up Files # Clean Up Files
# ============== # ==============
def unlinkIfFound(delFile): def unlinkIfFound(delFile: str) -> None:
if os.path.isfile(delFile): if os.path.isfile(delFile):
os.unlink(delFile) os.unlink(delFile)
print("Deleted: %s" % delFile) print("Deleted: %s" % delFile)
def deleteFolder(delPath): def deleteFolder(delPath: str) -> None:
if os.path.isdir(delPath): if os.path.isdir(delPath):
shutil.rmtree(delPath) shutil.rmtree(delPath)
print("Deleted: %s" % delPath) print("Deleted: %s" % delPath)
@@ -1561,6 +1561,9 @@ def xdgUninstall() -> None:
def winInstall() -> None: def winInstall() -> None:
"""Will attempt to install icons and make a launcher for Windows.""" """Will attempt to install icons and make a launcher for Windows."""
if sys.platform != "win32":
raise Exception("This method only runs on Windows")
import winreg import winreg
try: try:
import win32com.client import win32com.client
@@ -1654,7 +1657,7 @@ def winInstall() -> None:
print("") print("")
print("Creating registry keys ...") print("Creating registry keys ...")
def setKey(kPath, kName, kVal): def setKey(kPath: str, kName: str, kVal: str) -> None:
winreg.CreateKey(winreg.HKEY_CURRENT_USER, kPath) winreg.CreateKey(winreg.HKEY_CURRENT_USER, kPath)
regKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, kPath, 0, winreg.KEY_WRITE) regKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, kPath, 0, winreg.KEY_WRITE)
winreg.SetValueEx(regKey, kName, 0, winreg.REG_SZ, kVal) winreg.SetValueEx(regKey, kName, 0, winreg.REG_SZ, kVal)
@@ -1688,6 +1691,9 @@ def winInstall() -> None:
def winUninstall() -> None: def winUninstall() -> None:
"""Will attempt to uninstall icons previously installed.""" """Will attempt to uninstall icons previously installed."""
if sys.platform != "win32":
raise Exception("This method only runs on Windows")
import winreg import winreg
try: try:
import win32com.client import win32com.client
+1
View File
@@ -1,2 +1,3 @@
flake8 flake8
flake8-pep585 flake8-pep585
flake8-annotations