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