Add linting of type annotations (#1809)
This commit is contained in:
@@ -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/*
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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,2 +1,3 @@
|
||||
flake8
|
||||
flake8-pep585
|
||||
flake8-annotations
|
||||
|
||||
Reference in New Issue
Block a user