diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index a1bf648d..e66f3104 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -23,11 +23,15 @@ jobs: uses: actions/checkout@v4 - name: Install Dependencies run: pip install -r requirements.txt -r requirements-dev.txt - - name: Run Ruff + - name: Ruff Check run: | ruff --version ruff check - - name: Run Pyright + - name: Pyright Check run: | pyright --version pyright + - name: Isort Check + run: | + isort --version + isort --check . diff --git a/docs/source/conf.py b/docs/source/conf.py index 8b507e58..4791c0e4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,7 +24,7 @@ initFile = os.path.join( os.path.dirname(__file__), os.pardir, os.pardir, "novelwriter", "__init__.py" ) -with open(initFile) as inFile: +with open(initFile, encoding="utf-8") as inFile: for aLine in inFile: if aLine.startswith("__version__"): release = aLine.split('"')[1].strip() diff --git a/novelWriter.py b/novelWriter.py index 285cb5dd..63d437e1 100755 --- a/novelWriter.py +++ b/novelWriter.py @@ -7,8 +7,8 @@ import os import sys try: - import PyQt6.QtCore # noqa: F401 - import PyQt6.QtGui # noqa: F401 + import PyQt6.QtCore + import PyQt6.QtGui import PyQt6.QtWidgets # noqa: F401 except Exception: print("ERROR: Failed to load dependency PyQt6") diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index efa6a177..8f5214dc 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -36,7 +36,7 @@ from novelwriter.config import Config from novelwriter.error import exceptionHandler from novelwriter.shared import SharedData -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.guimain import GuiMain # Package Meta @@ -148,7 +148,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None: inOpts, inRemain = getopt.getopt(sysArgs, shortOpt, longOpt) except getopt.GetoptError as exc: print(helpMsg) - print(f"ERROR: {str(exc)}") + print(f"ERROR: {exc!s}") sys.exit(2) if len(inRemain) > 0: diff --git a/novelwriter/common.py b/novelwriter/common.py index 435efd4c..7cb6c5e0 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -29,11 +29,10 @@ import unicodedata import uuid import xml.etree.ElementTree as ET -from collections.abc import Callable from configparser import ConfigParser from datetime import datetime from pathlib import Path -from typing import Any, Literal, TypeGuard, TypeVar +from typing import TYPE_CHECKING, Any, Literal, TypeGuard, TypeVar from urllib.parse import urljoin from urllib.request import pathname2url @@ -45,6 +44,9 @@ from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException +if TYPE_CHECKING: + from collections.abc import Callable + logger = logging.getLogger(__name__) _Type = TypeVar("_Type") @@ -347,7 +349,7 @@ def fuzzyTime(seconds: int) -> str: elif seconds < 3300: # 55 minutes return QCoreApplication.translate( "Common", "{0} minutes ago" - ).format(int(round(seconds/60))) + ).format(round(seconds/60)) elif seconds < 5400: # 90 minutes return QCoreApplication.translate( "Common", "an hour ago" @@ -355,7 +357,7 @@ def fuzzyTime(seconds: int) -> str: elif seconds < 84600: # 23.5 hours return QCoreApplication.translate( "Common", "{0} hours ago" - ).format(int(round(seconds/3600))) + ).format(round(seconds/3600)) elif seconds < 129600: # 1.5 days return QCoreApplication.translate( "Common", "a day ago" @@ -363,7 +365,7 @@ def fuzzyTime(seconds: int) -> str: elif seconds < 561600: # 6.5 days return QCoreApplication.translate( "Common", "{0} days ago" - ).format(int(round(seconds/86400))) + ).format(round(seconds/86400)) elif seconds < 907200: # 10.5 days return QCoreApplication.translate( "Common", "a week ago" @@ -371,7 +373,7 @@ def fuzzyTime(seconds: int) -> str: elif seconds < 2419200: # 28 days return QCoreApplication.translate( "Common", "{0} weeks ago" - ).format(int(round(seconds/604800))) + ).format(round(seconds/604800)) elif seconds < 3888000: # 45 days return QCoreApplication.translate( "Common", "a month ago" @@ -379,7 +381,7 @@ def fuzzyTime(seconds: int) -> str: elif seconds < 29808000: # 345 days return QCoreApplication.translate( "Common", "{0} months ago" - ).format(int(round(seconds/2592000))) + ).format(round(seconds/2592000)) elif seconds < 47336400: # 1.5 years return QCoreApplication.translate( "Common", "a year ago" @@ -387,7 +389,7 @@ def fuzzyTime(seconds: int) -> str: else: return QCoreApplication.translate( "Common", "{0} years ago" - ).format(int(round(seconds/31557600))) + ).format(round(seconds/31557600)) def numberToRoman(value: int, toLower: bool = False) -> str: @@ -423,7 +425,7 @@ def describeFont(font: QFont) -> str: info = QFontInfo(font) family = info.family() styles = [v for v in info.styleName().split() if v not in family] - return " ".join([f"{info.pointSize()} pt", family] + styles) + return " ".join([f"{info.pointSize()} pt", family, *styles]) return "Error" diff --git a/novelwriter/config.py b/novelwriter/config.py index c5d1f166..51d6e164 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -29,10 +29,9 @@ import json import logging import sys -from datetime import datetime from pathlib import Path from time import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from PyQt6.QtCore import ( PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo, @@ -48,7 +47,9 @@ from novelwriter.common import ( from novelwriter.constants import nwFiles, nwUnicode from novelwriter.error import formatException, logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from datetime import datetime + from novelwriter.core.projectdata import NWProjectData logger = logging.getLogger(__name__) @@ -62,29 +63,27 @@ DEF_TREECOL = "theme" class Config: __slots__ = ( - "_confPath", "_dataPath", "_homePath", "_backPath", "_appPath", "_appRoot", "_hasError", - "_errData", "_nwLangPath", "_qtLangPath", "_qLocale", "_dLocale", "_dShortDate", - "_dShortDateTime", "_qtTrans", "_manuals", "_recentProjects", "_recentPaths", - "_backupPath", - - "appName", "appHandle", "guiLocale", "guiTheme", "guiSyntax", "guiFont", "hideVScroll", - "hideHScroll", "lastNotes", "nativeFont", "useCharCount", "iconTheme", "iconColTree", - "iconColDocs", "mainWinSize", "welcomeWinSize", "prefsWinSize", "mainPanePos", - "viewPanePos", "outlinePanePos", "autoSaveProj", "autoSaveDoc", "emphLabels", - "backupOnClose", "askBeforeBackup", "askBeforeExit", "textFont", "textWidth", "textMargin", - "tabWidth", "cursorWidth", "focusWidth", "hideFocusFooter", "showFullPath", "autoSelect", - "doJustify", "showTabsNSpaces", "showLineEndings", "showMultiSpaces", "doReplace", - "doReplaceSQuote", "doReplaceDQuote", "doReplaceDash", "doReplaceDots", "autoScroll", - "autoScrollPos", "scrollPastEnd", "dialogStyle", "allowOpenDial", "dialogLine", - "narratorBreak", "narratorDialog", "altDialogOpen", "altDialogClose", "highlightEmph", - "stopWhenIdle", "userIdleTime", "incNotesWCount", "fmtApostrophe", "fmtSQuoteOpen", - "fmtSQuoteClose", "fmtDQuoteOpen", "fmtDQuoteClose", "fmtPadBefore", "fmtPadAfter", - "fmtPadThin", "spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime", - "viewComments", "viewSynopsis", "searchCase", "searchWord", "searchRegEx", "searchLoop", - "searchNextFile", "searchMatchCap", "searchProjCase", "searchProjWord", "searchProjRegEx", - "verQtString", "verQtValue", "verPyQtString", "verPyQtValue", "verPyString", "osType", - "osLinux", "osWindows", "osDarwin", "osUnknown", "hostName", "kernelVer", "isDebug", - "memInfo", "hasEnchant", + "_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate", + "_dShortDateTime", "_dataPath", "_errData", "_hasError", "_homePath", "_manuals", + "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects", + "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName", + "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", "autoScroll", + "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", "dialogStyle", + "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots", + "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose", "fmtDQuoteOpen", + "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose", "fmtSQuoteOpen", + "focusWidth", "guiFont", "guiLocale", "guiSyntax", "guiTheme", "hasEnchant", + "hideFocusFooter", "hideHScroll", "hideVScroll", "highlightEmph", "hostName", + "iconColDocs", "iconColTree", "iconTheme", "incNotesWCount", "isDebug", "kernelVer", + "lastNotes", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog", + "nativeFont", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos", + "prefsWinSize", "scrollPastEnd", "searchCase", "searchLoop", "searchMatchCap", + "searchNextFile", "searchProjCase", "searchProjRegEx", "searchProjWord", "searchRegEx", + "searchWord", "showEditToolBar", "showFullPath", "showLineEndings", "showMultiSpaces", + "showSessionTime", "showTabsNSpaces", "showViewerPanel", "spellLanguage", "stopWhenIdle", + "tabWidth", "textFont", "textMargin", "textWidth", "useCharCount", "userIdleTime", + "verPyQtString", "verPyQtValue", "verPyString", "verQtString", "verQtValue", + "viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize", ) LANG_NW = 1 @@ -306,6 +305,10 @@ class Config: def pdfDocs(self) -> Path | None: return self._manuals.get(f"manual_{self.locale.name()}", self._manuals.get("manual")) + @property + def nwLangPath(self) -> Path: + return self._nwLangPath + @property def locale(self) -> QLocale: return self._dLocale @@ -918,7 +921,7 @@ class RecentProjects: class RecentPaths: - KEYS = ["default", "project", "import", "outline", "stats"] + KEYS: Final[list[str]] = ["default", "project", "import", "outline", "stats"] def __init__(self, config: Config) -> None: self._conf = config diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 9f4c0e1e..9928ece4 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -23,6 +23,8 @@ along with this program. If not, see . """ from __future__ import annotations +from typing import Final + from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from novelwriter.enum import ( @@ -99,7 +101,7 @@ class nwShortcode: FOOTNOTE_B = "[footnote:" FIELD_B = "[field:" - COMMENT_STYLES = { + COMMENT_STYLES: Final[dict[nwComment, str]] = { nwComment.FOOTNOTE: "[footnote:{0}]", nwComment.COMMENT: "[comment:{0}]", } @@ -110,13 +112,13 @@ class nwShortcode: class nwStyles: H_VALID = ("H0", "H1", "H2", "H3", "H4") - H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} - H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25} + H_LEVEL: Final[dict[str, int]] = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} + H_SIZES: Final[dict[int, float]] = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25} T_NORMAL = 1.0 T_SMALL = 0.8 - T_LABEL = { + T_LABEL: Final[dict[str, str]] = { "H0": QT_TRANSLATE_NOOP("Constant", "Title"), "H1": QT_TRANSLATE_NOOP("Constant", "Heading 1 (Partition)"), "H2": QT_TRANSLATE_NOOP("Constant", "Heading 2 (Chapter)"), @@ -125,7 +127,7 @@ class nwStyles: "TT": QT_TRANSLATE_NOOP("Constant", "Text Paragraph"), "SP": QT_TRANSLATE_NOOP("Constant", "Scene Separator"), } - T_MARGIN = { + T_MARGIN: Final[dict[str, tuple[float, float]]] = { "H0": (1.50, 0.60), # Title margins (top, bottom) "H1": (1.50, 0.60), # Heading 1 margins (top, bottom) "H2": (1.50, 0.60), # Heading 2 margins (top, bottom) @@ -174,20 +176,20 @@ class nwKeyWords: MENTION_KEY = "@mention" # Note: The order here affects the order of menu entries - ALL_KEYS = [ + ALL_KEYS: Final[list[str]] = [ TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY, ] - CAN_CREATE = [ + CAN_CREATE: Final[list[str]] = [ POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, ] # Set of Valid Keys - VALID_KEYS = set(ALL_KEYS) + VALID_KEYS: Final[set[str]] = set(ALL_KEYS) # Map from Keys to Item Class - KEY_CLASS = { + KEY_CLASS: Final[dict[str, nwItemClass]] = { POV_KEY: nwItemClass.CHARACTER, FOCUS_KEY: nwItemClass.CHARACTER, CHAR_KEY: nwItemClass.CHARACTER, @@ -203,7 +205,7 @@ class nwKeyWords: class nwLists: - USER_CLASSES = [ + USER_CLASSES: Final[list[nwItemClass]] = [ nwItemClass.CHARACTER, nwItemClass.PLOT, nwItemClass.WORLD, @@ -229,7 +231,7 @@ class nwStats: WORDS_TITLE = "titleWords" # Note: The order here affects the order of menu entries - ALL_FIELDS = [ + ALL_FIELDS: Final[list[str]] = [ WORDS, WORDS_TEXT, WORDS_TITLE, CHARS, CHARS_TEXT, CHARS_TITLE, WCHARS_ALL, WCHARS_TEXT, WCHARS_TITLE, @@ -239,7 +241,7 @@ class nwStats: class nwLabels: - CLASS_NAME = { + CLASS_NAME: Final[dict[nwItemClass, str]] = { nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"), nwItemClass.NOVEL: QT_TRANSLATE_NOOP("Constant", "Novel"), nwItemClass.PLOT: QT_TRANSLATE_NOOP("Constant", "Plot"), @@ -253,7 +255,7 @@ class nwLabels: nwItemClass.TEMPLATE: QT_TRANSLATE_NOOP("Constant", "Templates"), nwItemClass.TRASH: QT_TRANSLATE_NOOP("Constant", "Trash"), } - CLASS_ICON = { + CLASS_ICON: Final[dict[nwItemClass, str]] = { nwItemClass.NO_CLASS: "cls_none", nwItemClass.NOVEL: "cls_novel", nwItemClass.PLOT: "cls_plot", @@ -267,12 +269,12 @@ class nwLabels: nwItemClass.TEMPLATE: "cls_template", nwItemClass.TRASH: "cls_trash", } - LAYOUT_NAME = { + LAYOUT_NAME: Final[dict[nwItemLayout, str]] = { nwItemLayout.NO_LAYOUT: QT_TRANSLATE_NOOP("Constant", "None"), nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"), nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Project Note"), } - ITEM_DESCRIPTION = { + ITEM_DESCRIPTION: Final[dict[str, str]] = { "none": QT_TRANSLATE_NOOP("Constant", "None"), "root": QT_TRANSLATE_NOOP("Constant", "Root Folder"), "folder": QT_TRANSLATE_NOOP("Constant", "Folder"), @@ -283,11 +285,11 @@ class nwLabels: "doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"), "note": QT_TRANSLATE_NOOP("Constant", "Project Note"), } - ACTIVE_NAME = { + ACTIVE_NAME: Final[dict[str, str]] = { "checked": QT_TRANSLATE_NOOP("Constant", "Active"), "unchecked": QT_TRANSLATE_NOOP("Constant", "Inactive"), } - KEY_NAME = { + KEY_NAME: Final[dict[str, str]] = { nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"), nwKeyWords.POV_KEY: QT_TRANSLATE_NOOP("Constant", "Point of View"), nwKeyWords.FOCUS_KEY: QT_TRANSLATE_NOOP("Constant", "Focus"), @@ -301,7 +303,7 @@ class nwLabels: nwKeyWords.STORY_KEY: QT_TRANSLATE_NOOP("Constant", "Story"), nwKeyWords.MENTION_KEY: QT_TRANSLATE_NOOP("Constant", "Mentions"), } - KEY_SHORTCUT = { + KEY_SHORTCUT: Final[dict[str, str]] = { nwKeyWords.TAG_KEY: "Ctrl+K, G", nwKeyWords.POV_KEY: "Ctrl+K, V", nwKeyWords.FOCUS_KEY: "Ctrl+K, F", @@ -315,7 +317,7 @@ class nwLabels: nwKeyWords.STORY_KEY: "Ctrl+K, N", nwKeyWords.MENTION_KEY: "Ctrl+K, M", } - OUTLINE_COLS = { + OUTLINE_COLS: Final[dict[nwOutline, str]] = { nwOutline.TITLE: QT_TRANSLATE_NOOP("Constant", "Title"), nwOutline.LEVEL: QT_TRANSLATE_NOOP("Constant", "Level"), nwOutline.LABEL: QT_TRANSLATE_NOOP("Constant", "Document"), @@ -337,7 +339,7 @@ class nwLabels: nwOutline.MENTION: KEY_NAME[nwKeyWords.MENTION_KEY], nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"), } - STATS_NAME = { + STATS_NAME: Final[dict[str, str]] = { nwStats.CHARS: QT_TRANSLATE_NOOP("Stats", "Characters"), nwStats.CHARS_TEXT: QT_TRANSLATE_NOOP("Stats", "Characters in Text"), nwStats.CHARS_TITLE: QT_TRANSLATE_NOOP("Stats", "Characters in Headings"), @@ -350,7 +352,7 @@ class nwLabels: nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Stats", "Words in Text"), nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Stats", "Words in Headings"), } - BUILD_FMT = { + BUILD_FMT: Final[dict[nwBuildFmt, str]] = { nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"), nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"), nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"), @@ -362,7 +364,7 @@ class nwLabels: nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"), nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"), } - BUILD_EXT = { + BUILD_EXT: Final[dict[nwBuildFmt, str]] = { nwBuildFmt.ODT: ".odt", nwBuildFmt.FODT: ".fodt", nwBuildFmt.DOCX: ".docx", @@ -374,7 +376,7 @@ class nwLabels: nwBuildFmt.J_HTML: ".json", nwBuildFmt.J_NWD: ".json", } - SHAPES_PLAIN = { + SHAPES_PLAIN: Final[dict[nwStatusShape, str]] = { nwStatusShape.SQUARE: QT_TRANSLATE_NOOP("Constant", "Square"), nwStatusShape.TRIANGLE: QT_TRANSLATE_NOOP("Constant", "Triangle"), nwStatusShape.NABLA: QT_TRANSLATE_NOOP("Constant", "Nabla"), @@ -384,42 +386,42 @@ class nwLabels: nwStatusShape.STAR: QT_TRANSLATE_NOOP("Constant", "Star"), nwStatusShape.PACMAN: QT_TRANSLATE_NOOP("Constant", "Pacman"), } - SHAPES_CIRCLE = { + SHAPES_CIRCLE: Final[dict[nwStatusShape, str]] = { nwStatusShape.CIRCLE_Q: QT_TRANSLATE_NOOP("Constant", "1/4 Circle"), nwStatusShape.CIRCLE_H: QT_TRANSLATE_NOOP("Constant", "Half Circle"), nwStatusShape.CIRCLE_T: QT_TRANSLATE_NOOP("Constant", "3/4 Circle"), nwStatusShape.CIRCLE: QT_TRANSLATE_NOOP("Constant", "Full Circle"), } - SHAPES_BARS = { + SHAPES_BARS: Final[dict[nwStatusShape, str]] = { nwStatusShape.BARS_1: QT_TRANSLATE_NOOP("Constant", "1 Bar"), nwStatusShape.BARS_2: QT_TRANSLATE_NOOP("Constant", "2 Bars"), nwStatusShape.BARS_3: QT_TRANSLATE_NOOP("Constant", "3 Bars"), nwStatusShape.BARS_4: QT_TRANSLATE_NOOP("Constant", "4 Bars"), } - SHAPES_BLOCKS = { + SHAPES_BLOCKS: Final[dict[nwStatusShape, str]] = { nwStatusShape.BLOCK_1: QT_TRANSLATE_NOOP("Constant", "1 Block"), nwStatusShape.BLOCK_2: QT_TRANSLATE_NOOP("Constant", "2 Blocks"), nwStatusShape.BLOCK_3: QT_TRANSLATE_NOOP("Constant", "3 Blocks"), nwStatusShape.BLOCK_4: QT_TRANSLATE_NOOP("Constant", "4 Blocks"), } - FILE_FILTERS = { + FILE_FILTERS: Final[dict[str, str]] = { "*.txt": QT_TRANSLATE_NOOP("Constant", "Text files"), "*.md": QT_TRANSLATE_NOOP("Constant", "Markdown files"), "*.nwd": QT_TRANSLATE_NOOP("Constant", "novelWriter files"), "*.csv": QT_TRANSLATE_NOOP("Constant", "CSV files"), "*": QT_TRANSLATE_NOOP("Constant", "All files"), } - UNIT_NAME = { + UNIT_NAME: Final[dict[str, str]] = { "mm": QT_TRANSLATE_NOOP("Constant", "Millimetres"), "cm": QT_TRANSLATE_NOOP("Constant", "Centimetres"), "in": QT_TRANSLATE_NOOP("Constant", "Inches"), } - UNIT_SCALE = { + UNIT_SCALE: Final[dict[str, float]] = { "mm": 1.0, "cm": 10.0, "in": 25.4, } - PAPER_NAME = { + PAPER_NAME: Final[dict[str, str]] = { "A4": QT_TRANSLATE_NOOP("Constant", "A4"), "A5": QT_TRANSLATE_NOOP("Constant", "A5"), "A6": QT_TRANSLATE_NOOP("Constant", "A6"), @@ -427,7 +429,7 @@ class nwLabels: "Letter": QT_TRANSLATE_NOOP("Constant", "US Letter"), "Custom": QT_TRANSLATE_NOOP("Constant", "Custom"), } - PAPER_SIZE = { + PAPER_SIZE: Final[dict[str, tuple[float, float]]] = { "A4": (210.0, 297.0), "A5": (148.0, 210.0), "A6": (105.0, 148.0), @@ -435,7 +437,7 @@ class nwLabels: "Letter": (215.9, 279.4), "Custom": (-1.0, -1.0), } - THEME_COLORS = { + THEME_COLORS: Final[dict[str, str]] = { "theme": QT_TRANSLATE_NOOP("Constant", "Theme Colours"), "default": QT_TRANSLATE_NOOP("Constant", "Foreground Colour"), "faded": QT_TRANSLATE_NOOP("Constant", "Faded Colour"), @@ -462,7 +464,7 @@ class nwHeadFmt: CHAR_POV = "{Char:POV}" CHAR_FOCUS = "{Char:Focus}" - PAGE_HEADERS = [ + PAGE_HEADERS: Final[list[str]] = [ TITLE, CH_NUM, CH_WORD, CH_ROMU, CH_ROML, SC_NUM, SC_ABS, CHAR_POV, CHAR_FOCUS ] @@ -478,7 +480,7 @@ class nwQuotes: """Allowed quotation marks. Source: https://en.wikipedia.org/wiki/Quotation_mark """ - SYMBOLS = { + SYMBOLS: Final[dict[str, str]] = { "\u0027": QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"), "\u0022": QT_TRANSLATE_NOOP("Constant", "Straight double quotation mark"), @@ -645,7 +647,7 @@ class nwUnicode: class nwHtmlUnicode: - U_TO_H = { + U_TO_H: Final[dict[str, str]] = { # Quotes nwUnicode.U_QUOT: nwUnicode.H_QUOT, nwUnicode.U_APOS: nwUnicode.H_APOS, diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 6c6420b6..4032eb0b 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -28,9 +28,9 @@ import json import logging import uuid -from collections.abc import Iterable from enum import Enum from pathlib import Path +from typing import TYPE_CHECKING from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication @@ -41,6 +41,9 @@ from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt from novelwriter.error import logException +if TYPE_CHECKING: + from collections.abc import Iterable + logger = logging.getLogger(__name__) T_BuildValue = str | int | float | bool @@ -227,9 +230,9 @@ class BuildSettings: @classmethod def fromDict(cls, data: dict) -> BuildSettings: """Create a build settings object from a dict.""" - cls = BuildSettings() - cls.unpack(data) - return cls + new = cls() + new.unpack(data) + return new ## # Properties @@ -514,11 +517,11 @@ class BuildSettings: @classmethod def duplicate(cls, source: BuildSettings) -> BuildSettings: """Make a copy of another build.""" - cls = BuildSettings() - cls.unpack(source.pack()) - cls._uuid = str(uuid.uuid4()) - cls._name = f"{source.name} 2" - return cls + new = cls() + new.unpack(source.pack()) + new._uuid = str(uuid.uuid4()) + new._name = f"{source.name} 2" + return new class BuildCollection: diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index d691835e..815504e2 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -30,9 +30,9 @@ import logging import re import shutil -from collections.abc import Iterable from functools import partial from pathlib import Path +from typing import TYPE_CHECKING from zipfile import ZipFile, is_zipfile from PyQt6.QtCore import QCoreApplication @@ -40,10 +40,14 @@ from PyQt6.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED from novelwriter.common import isHandle, minmax, simplified from novelwriter.constants import nwConst, nwFiles, nwItemClass, nwStats -from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject from novelwriter.core.storage import NWStorageCreate +if TYPE_CHECKING: + from collections.abc import Iterable + + from novelwriter.core.item import NWItem + logger = logging.getLogger(__name__) @@ -284,7 +288,7 @@ class DocDuplicator: class DocSearch: def __init__(self) -> None: - self._regEx = re.compile("") + self._regEx = re.compile(r"") self._opts = re.UNICODE | re.IGNORECASE self._words = False self._escape = True @@ -381,7 +385,7 @@ class ProjectBuilder: path = data.get("path", None) or None if isinstance(path, str | Path): self._path = Path(path).resolve() - if data.get("sample", False): + if data.get("sample"): return self._extractSampleProject(self._path) elif data.get("template"): return self._copyProject(self._path, data) diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 0b10bfe4..ce3f4930 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -25,26 +25,30 @@ from __future__ import annotations import logging -from collections.abc import Iterable -from pathlib import Path +from typing import TYPE_CHECKING from PyQt6.QtGui import QFont from novelwriter import CONFIG from novelwriter.constants import nwLabels -from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.item import NWItem -from novelwriter.core.project import NWProject from novelwriter.enum import nwBuildFmt from novelwriter.error import formatException, logException from novelwriter.formats.todocx import ToDocX from novelwriter.formats.tohtml import ToHtml -from novelwriter.formats.tokenizer import Tokenizer from novelwriter.formats.tomarkdown import ToMarkdown from novelwriter.formats.toodt import ToOdt from novelwriter.formats.toqdoc import ToQTextDocument from novelwriter.formats.toraw import ToRaw +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + + from novelwriter.core.buildsettings import BuildSettings + from novelwriter.core.project import NWProject + from novelwriter.formats.tokenizer import Tokenizer + logger = logging.getLogger(__name__) @@ -56,8 +60,8 @@ class NWBuildDocument: """ __slots__ = ( - "_project", "_build", "_queue", "_error", "_cache", "_count", - "_outline", + "_build", "_cache", "_count", "_error", "_outline", "_project", + "_queue", ) def __init__(self, project: NWProject, build: BuildSettings) -> None: diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py index 9a512753..e0f469e9 100644 --- a/novelwriter/core/document.py +++ b/novelwriter/core/document.py @@ -31,11 +31,11 @@ from time import time from typing import TYPE_CHECKING from novelwriter.common import formatTimeStamp, isHandle -from novelwriter.core.item import NWItem from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.error import formatException, logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 1097294c..4263c0cf 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -29,7 +29,6 @@ import json import logging import random -from collections.abc import ItemsView, Iterable from pathlib import Path from time import time from typing import TYPE_CHECKING @@ -44,7 +43,9 @@ from novelwriter.error import logException from novelwriter.text.comments import processComment from novelwriter.text.counting import standardCounter -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from collections.abc import ItemsView, Iterable + from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject @@ -906,7 +907,7 @@ class ItemIndex: IndexHeading object for each heading of the text. """ - __slots__ = ("_project", "_tags", "_items") + __slots__ = ("_items", "_project", "_tags") def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None: self._project = project diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py index 9aee57c3..914263a0 100644 --- a/novelwriter/core/indexdata.py +++ b/novelwriter/core/indexdata.py @@ -28,14 +28,15 @@ from __future__ import annotations import logging -from collections.abc import ItemsView, Sequence from typing import TYPE_CHECKING, Literal from novelwriter import CONFIG from novelwriter.common import checkInt, isListInstance, isTitleTag from novelwriter.constants import nwKeyWords, nwStyles -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from collections.abc import ItemsView, Sequence + from novelwriter.core.index import TagsIndex from novelwriter.core.item import NWItem @@ -57,7 +58,7 @@ class IndexNode: must be reset each time the item is re-indexed. """ - __slots__ = ("_tags", "_handle", "_item", "_headings", "_notes", "_count") + __slots__ = ("_count", "_handle", "_headings", "_item", "_notes", "_tags") def __init__(self, tagsIndex: TagsIndex, tHandle: str, nwItem: NWItem) -> None: self._tags = tagsIndex @@ -205,8 +206,8 @@ class IndexHeading: """ __slots__ = ( - "_tags", "_key", "_line", "_level", "_title", - "_counts", "_tag", "_refs", "_comments", + "_comments", "_counts", "_key", "_level", "_line", "_refs", "_tag", + "_tags", "_title", ) def __init__( diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 3bbb8cec..ca3ce498 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -27,8 +27,6 @@ import logging from typing import TYPE_CHECKING, Any -from PyQt6.QtGui import QFont, QIcon - from novelwriter import CONFIG, SHARED from novelwriter.common import ( checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, @@ -37,7 +35,9 @@ from novelwriter.common import ( from novelwriter.constants import nwLabels, nwStyles, trConst from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from PyQt6.QtGui import QFont, QIcon + from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) @@ -53,10 +53,10 @@ class NWItem: """ __slots__ = ( - "_project", "_name", "_handle", "_parent", "_root", "_order", - "_type", "_class", "_layout", "_status", "_import", "_active", - "_expanded", "_heading", "_charCount", "_wordCount", - "_paraCount", "_cursorPos", "_initCount", + "_active", "_charCount", "_class", "_cursorPos", "_expanded", + "_handle", "_heading", "_import", "_initCount", "_layout", "_name", + "_order", "_paraCount", "_parent", "_project", "_root", "_status", + "_type", "_wordCount", ) def __init__(self, project: NWProject, handle: str) -> None: @@ -264,25 +264,25 @@ class NWItem: @classmethod def duplicate(cls, source: NWItem, handle: str) -> NWItem: """Make a copy of an item.""" - cls = NWItem(source._project, handle) - cls._name = source._name - cls._parent = source._parent - cls._root = source._root - cls._order = source._order - cls._type = source._type - cls._class = source._class - cls._layout = source._layout - cls._status = source._status - cls._import = source._import - cls._active = source._active - cls._expanded = source._expanded - cls._heading = source._heading - cls._charCount = source._charCount - cls._wordCount = source._wordCount - cls._paraCount = source._paraCount - cls._cursorPos = source._cursorPos - cls._initCount = source._initCount - return cls + new = cls(source._project, handle) + new._name = source._name + new._parent = source._parent + new._root = source._root + new._order = source._order + new._type = source._type + new._class = source._class + new._layout = source._layout + new._status = source._status + new._import = source._import + new._active = source._active + new._expanded = source._expanded + new._heading = source._heading + new._charCount = source._charCount + new._wordCount = source._wordCount + new._paraCount = source._paraCount + new._cursorPos = source._cursorPos + new._initCount = source._initCount + return new ## # Action Methods diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index 505ee542..6fb2580a 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -37,7 +37,7 @@ from novelwriter.core.item import NWItem from novelwriter.enum import nwItemClass from novelwriter.types import QtAlignRight -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.core.tree import NWTree logger = logging.getLogger(__name__) @@ -89,7 +89,7 @@ class ProjectNode: C_ACTIVE = 2 C_STATUS = 3 - __slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_flags", "_count") + __slots__ = ("_cache", "_children", "_count", "_flags", "_item", "_parent", "_row") def __init__(self, item: NWItem) -> None: self._item = item @@ -162,7 +162,7 @@ class ProjectNode: def updateCount(self, propagate: bool = True) -> None: """Update counts, and propagate upwards in the tree.""" - self._count = self._item.wordCount + sum(c._count for c in self._children) + self._count = self._item.wordCount + sum(c._count for c in self._children) # noqa: SLF001 self._cache[C_COUNT_TEXT] = f"{self._count:n}" if propagate and (parent := self._parent): parent.updateCount() @@ -257,13 +257,13 @@ class ProjectNode: """Recursively add all nodes to a list.""" for node in self._children: children.append(node) - node._recursiveAppendChildren(children) + node._recursiveAppendChildren(children) # noqa: SLF001 return def _refreshChildrenPos(self) -> None: """Update the row value on all children.""" for n, child in enumerate(self._children): - child._row = n + child._row = n # noqa: SLF001 child.item.setOrder(n) return @@ -290,12 +290,12 @@ class ProjectModel(QAbstractItemModel): methods needed primarily by the project tree GUI component. """ - __slots__ = ("_tree", "_root") + __slots__ = ("_root", "_tree") def __init__(self, tree: NWTree) -> None: super().__init__() self._tree = tree - self._root = ProjectNode(NWItem(tree._project, INV_ROOT)) + self._root = ProjectNode(NWItem(tree.project, INV_ROOT)) self._root.item.setName("Invisible Root") logger.debug("Ready: ProjectModel") return @@ -391,10 +391,10 @@ class ProjectModel(QAbstractItemModel): ) -> bool: """Process mime data drop.""" if self.canDropMimeData(data, action, row, column, parent): - items = [] - for handle in decodeMimeHandles(data): - if (index := self.indexFromHandle(handle)).isValid(): - items.append(index) + items = [ + index for handle in decodeMimeHandles(data) + if (index := self.indexFromHandle(handle)).isValid() + ] self.multiMove(items, parent, row) return True return False @@ -490,7 +490,7 @@ class ProjectModel(QAbstractItemModel): if temp := self.removeChild(index.parent(), index.row()): self.insertChild(temp, target, pos) for child in reversed(node.allChildren()): - node._updateRelationships(child) + node._updateRelationships(child) # noqa: SLF001 child.item.notifyToRefresh() node.item.notifyToRefresh() return @@ -501,16 +501,15 @@ class ProjectModel(QAbstractItemModel): def clear(self) -> None: """Clear the project model.""" - self._root._children.clear() + self._root.children.clear() return def allExpanded(self) -> list[QModelIndex]: """Return a list of all expanded items.""" - expanded = [] - for node in self._root.allChildren(): - if node._item.isExpanded: - expanded.append(self.createIndex(node.row(), 0, node)) - return expanded + return [ + self.createIndex(node.row(), 0, node) for node in self._root.allChildren() + if node.item.isExpanded + ] def trashSelection(self, indices: list[QModelIndex]) -> bool: """Check if a selection of indices are all in trash or not.""" diff --git a/novelwriter/core/novelmodel.py b/novelwriter/core/novelmodel.py index b55fa615..e5b49b15 100644 --- a/novelwriter/core/novelmodel.py +++ b/novelwriter/core/novelmodel.py @@ -25,15 +25,19 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING + from PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt from PyQt6.QtGui import QIcon, QPixmap from novelwriter import SHARED from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst -from novelwriter.core.indexdata import IndexHeading, IndexNode from novelwriter.enum import nwNovelExtra from novelwriter.types import QtAlignRight +if TYPE_CHECKING: + from novelwriter.core.indexdata import IndexHeading, IndexNode + logger = logging.getLogger(__name__) C_FACTOR = 0x0100 @@ -50,7 +54,7 @@ T_NodeData = str | QIcon | QPixmap | Qt.AlignmentFlag | None class NovelModel(QAbstractTableModel): - __slots__ = ("_rows", "_more", "_columns", "_extraKey", "_extraLabel") + __slots__ = ("_columns", "_extraKey", "_extraLabel", "_more", "_rows") def __init__(self) -> None: super().__init__() diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 40f8567d..2753c086 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -35,7 +35,7 @@ from novelwriter.common import checkBool, checkFloat, checkInt, checkString, jso from novelwriter.constants import nwFiles from novelwriter.error import logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 040f12f1..9ad46682 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -30,6 +30,7 @@ from enum import Enum from functools import partial from pathlib import Path from time import time +from typing import TYPE_CHECKING from PyQt6.QtCore import QCoreApplication @@ -44,12 +45,14 @@ from novelwriter.core.options import OptionState from novelwriter.core.projectdata import NWProjectData from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.core.sessions import NWSessionLog -from novelwriter.core.status import T_StatusKind, T_UpdateEntry from novelwriter.core.storage import NWStorage, NWStorageOpen from novelwriter.core.tree import NWTree from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException +if TYPE_CHECKING: + from novelwriter.core.status import T_StatusKind, T_UpdateEntry + logger = logging.getLogger(__name__) @@ -64,8 +67,8 @@ class NWProjectState(Enum): class NWProject: __slots__ = ( - "_options", "_storage", "_data", "_tree", "_index", "_session", - "_langData", "_changed", "_valid", "_state", "tr", + "_changed", "_data", "_index", "_langData", "_options", "_session", + "_state", "_storage", "_tree", "_valid", "tr", ) def __init__(self) -> None: @@ -557,13 +560,13 @@ class NWProject: def _loadProjectLocalisation(self) -> bool: """Load the language data for the current project language.""" - if self._data.language is None or CONFIG._nwLangPath is None: + if self._data.language is None or CONFIG.nwLangPath is None: self._langData = {} return False - langFile = Path(CONFIG._nwLangPath) / f"project_{self._data.language}.json" + langFile = Path(CONFIG.nwLangPath) / f"project_{self._data.language}.json" if not langFile.is_file(): - langFile = Path(CONFIG._nwLangPath) / "project_en_GB.json" + langFile = Path(CONFIG.nwLangPath) / "project_en_GB.json" try: with open(langFile, mode="r", encoding="utf-8") as inFile: diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index ff608874..1c2c1f42 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -34,7 +34,7 @@ from novelwriter.common import ( ) from novelwriter.core.status import NWStatus -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 033bd463..461d9a40 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -39,7 +39,7 @@ from novelwriter.common import ( hexToInt, simplified, xmlIndent, yesNo ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.core.projectdata import NWProjectData from novelwriter.core.status import NWStatus diff --git a/novelwriter/core/sessions.py b/novelwriter/core/sessions.py index 4be8689d..6f7fcef4 100644 --- a/novelwriter/core/sessions.py +++ b/novelwriter/core/sessions.py @@ -26,7 +26,6 @@ from __future__ import annotations import json import logging -from collections.abc import Iterable from pathlib import Path from time import time from typing import TYPE_CHECKING @@ -35,7 +34,9 @@ from novelwriter.common import formatTimeStamp from novelwriter.constants import nwFiles from novelwriter.error import logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from collections.abc import Iterable + from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index 87b701ba..a9e8b443 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -27,7 +27,6 @@ from __future__ import annotations import json import logging -from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING @@ -36,7 +35,9 @@ from PyQt6.QtCore import QLocale from novelwriter.constants import nwFiles from novelwriter.error import logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from collections.abc import Iterator + from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 92c78d50..f7d9b584 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -28,8 +28,7 @@ import dataclasses import logging import random -from collections.abc import Iterable -from typing import Literal, TypeGuard +from typing import TYPE_CHECKING, Literal, TypeGuard from PyQt6.QtCore import QPointF, Qt from PyQt6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygonF @@ -39,6 +38,9 @@ from novelwriter.common import simplified from novelwriter.enum import nwStatusShape from novelwriter.types import QtPaintAntiAlias, QtTransparent +if TYPE_CHECKING: + from collections.abc import Iterable + logger = logging.getLogger(__name__) @@ -54,10 +56,10 @@ class StatusEntry: @classmethod def duplicate(cls, source: StatusEntry) -> StatusEntry: """Create a deep copy of the source object.""" - cls = dataclasses.replace(source) - cls.color = QColor(source.color) - cls.icon = QIcon(source.icon) - return cls + status = dataclasses.replace(source) + status.color = QColor(source.color) + status.icon = QIcon(source.icon) + return status NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0) @@ -71,7 +73,7 @@ class NWStatus: STATUS = "s" IMPORT = "i" - __slots__ = ("_store", "_default", "_prefix", "_height") + __slots__ = ("_default", "_height", "_prefix", "_store") def __init__(self, prefix: T_StatusKind) -> None: self._store: dict[str, StatusEntry] = {} diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py index 23e37909..b8c21948 100644 --- a/novelwriter/core/storage.py +++ b/novelwriter/core/storage.py @@ -40,7 +40,7 @@ from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter from novelwriter.core.spellcheck import UserDictionary from novelwriter.error import logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index be582526..2ff74a53 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -27,7 +27,6 @@ from __future__ import annotations import logging import random -from collections.abc import Iterable, Iterator from pathlib import Path from typing import TYPE_CHECKING, Literal, overload @@ -40,7 +39,9 @@ from novelwriter.core.itemmodel import ProjectModel, ProjectNode from novelwriter.enum import nwChange, nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from novelwriter.core.project import NWProject logger = logging.getLogger(__name__) @@ -60,7 +61,7 @@ class NWTree: also used for file names. """ - __slots__ = ("_project", "_model", "_items", "_nodes", "_trash", "_ready") + __slots__ = ("_items", "_model", "_nodes", "_project", "_ready", "_trash") def __init__(self, project: NWProject) -> None: self._project = project @@ -107,6 +108,11 @@ class NWTree: # Properties ## + @property + def project(self) -> NWProject: + """Return the parent project.""" + return self._project + @property def trash(self) -> ProjectNode | None: """Return trash node, if it exists.""" @@ -396,7 +402,7 @@ class NWTree: def checkType(self, tHandle: str, itemType: nwItemType) -> bool: """Check if item exists and is of the specified item type.""" - if tItem := self.__getitem__(tHandle): + if tItem := self[tHandle]: return tItem.itemType == itemType return False @@ -414,8 +420,7 @@ class NWTree: node = parent else: return path - else: - logger.error("Max project tree depth reached") + logger.error("Max project tree depth reached") return path def subTree(self, tHandle: str) -> list[str]: diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index e122fd19..3c7070df 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -25,7 +25,8 @@ from __future__ import annotations import logging -from PyQt6.QtGui import QCloseEvent +from typing import TYPE_CHECKING + from PyQt6.QtWidgets import ( QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget ) @@ -37,6 +38,9 @@ from novelwriter.extensions.modified import NDialog from novelwriter.extensions.versioninfo import VersionInfoWidget from novelwriter.types import QtAlignRightTop, QtDialogClose, QtHexArgb +if TYPE_CHECKING: + from PyQt6.QtGui import QCloseEvent + logger = logging.getLogger(__name__) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 14e6553a..97717dc6 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -131,11 +131,11 @@ class GuiDocMerge(NDialog): @classmethod def getData(cls, parent: QWidget, handle: str, items: list[str]) -> tuple[dict, bool]: """Pop the dialog and return the result.""" - cls = GuiDocMerge(parent, handle, items) - cls.exec() - data = cls.data() - accepted = cls.result() == QtAccepted - cls.softDelete() + dialog = cls(parent, handle, items) + dialog.exec() + data = dialog.data() + accepted = dialog.result() == QtAccepted + dialog.softDelete() return data, accepted ## diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 447d1696..3f1f34ad 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -178,11 +178,11 @@ class GuiDocSplit(NDialog): @classmethod def getData(cls, parent: QWidget, handle: str) -> tuple[dict, list[str], bool]: """Pop the dialog and return the result.""" - cls = GuiDocSplit(parent, handle) - cls.exec() - data, text = cls.data() - accepted = cls.result() == QtAccepted - cls.softDelete() + dialog = cls(parent, handle) + dialog.exec() + data, text = dialog.data() + accepted = dialog.result() == QtAccepted + dialog.softDelete() return data, text, accepted ## diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index caf97335..5386acb4 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -82,9 +82,9 @@ class GuiEditLabel(NDialog): @classmethod def getLabel(cls, parent: QWidget, text: str) -> tuple[str, bool]: """Pop the dialog and return the result.""" - cls = GuiEditLabel(parent, text=text) - cls.exec() - label = cls.itemLabel - accepted = cls.result() == QtAccepted - cls.softDelete() + dialog = cls(parent, text=text) + dialog.exec() + label = dialog.itemLabel + accepted = dialog.result() == QtAccepted + dialog.softDelete() return label, accepted diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index a1166c04..aa4f9cd1 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -432,7 +432,7 @@ class GuiPreferences(NDialog): self.textWidth.setSingleStep(10) self.textWidth.setValue(CONFIG.textWidth) self.mainForm.addRow( - self.tr("Maximum text width in \"Normal Mode\""), self.textWidth, + self.tr('Maximum text width in "Normal Mode"'), self.textWidth, self.tr("Set to 0 to disable this feature."), unit=self.tr("px") ) @@ -443,7 +443,7 @@ class GuiPreferences(NDialog): self.focusWidth.setSingleStep(10) self.focusWidth.setValue(CONFIG.focusWidth) self.mainForm.addRow( - self.tr("Maximum text width in \"Focus Mode\""), self.focusWidth, + self.tr('Maximum text width in "Focus Mode"'), self.focusWidth, self.tr("The maximum width cannot be disabled."), unit=self.tr("px") ) @@ -451,7 +451,7 @@ class GuiPreferences(NDialog): self.hideFocusFooter = NSwitch(self) self.hideFocusFooter.setChecked(CONFIG.hideFocusFooter) self.mainForm.addRow( - self.tr("Hide document footer in \"Focus Mode\""), self.hideFocusFooter, + self.tr('Hide document footer in "Focus Mode"'), self.hideFocusFooter, self.tr("Hide the information bar in the document editor.") ) diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 2f7501f6..931558a2 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -122,11 +122,11 @@ class GuiQuoteSelect(NDialog): @classmethod def getQuote(cls, parent: QWidget, current: str = "") -> tuple[str, bool]: """Pop the dialog and return the result.""" - cls = GuiQuoteSelect(parent, current=current) - cls.exec() - quote = cls._selected - accepted = cls.result() == QtAccepted - cls.softDelete() + dialog = cls(parent, current=current) + dialog.exec() + quote = dialog._selected + accepted = dialog.result() == QtAccepted + dialog.softDelete() return quote, accepted ## diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 65d19193..a667738a 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -26,9 +26,9 @@ from __future__ import annotations import logging from pathlib import Path +from typing import TYPE_CHECKING from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit, QListWidget, QVBoxLayout, QWidget @@ -41,6 +41,9 @@ from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog, NIconToolButton from novelwriter.types import QtDialogClose, QtDialogSave +if TYPE_CHECKING: + from PyQt6.QtGui import QCloseEvent + logger = logging.getLogger(__name__) @@ -242,8 +245,7 @@ class GuiWordList(NDialog): def _listWords(self) -> list[str]: """List all words in the list box.""" - result = [] - for i in range(self.listBox.count()): - if (item := self.listBox.item(i)) and (word := item.text().strip()): - result.append(word) - return result + return [ + word for i in range(self.listBox.count()) + if (item := self.listBox.item(i)) and (word := item.text().strip()) + ] diff --git a/novelwriter/error.py b/novelwriter/error.py index 53ac8846..03f5d07a 100644 --- a/novelwriter/error.py +++ b/novelwriter/error.py @@ -36,7 +36,7 @@ from PyQt6.QtWidgets import ( QPlainTextEdit, QStyle, QWidget ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from types import TracebackType logger = logging.getLogger(__name__) @@ -46,7 +46,7 @@ def logException() -> None: """Log the content of an exception message.""" exType, exValue, _ = sys.exc_info() if exType is not None: - logger.error(f"{exType.__name__}: {str(exValue)}", stacklevel=2) + logger.error(f"{exType.__name__}: {exValue!s}", stacklevel=2) return @@ -54,7 +54,7 @@ def formatException(exc: BaseException) -> str: """Format an exception as a string the same way the default exception handler does. """ - return f"{type(exc).__name__}: {str(exc)}" + return f"{type(exc).__name__}: {exc!s}" class NWErrorMessage(QDialog): @@ -151,7 +151,7 @@ class NWErrorMessage(QDialog): f"Python: {sys.version.split()[0]} ({sys.hexversion:#x})\n" f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n" f"enchant: {enchantVersion}\n\n" - f"{exType.__name__}:\n{str(exValue)}\n\n" + f"{exType.__name__}:\n{exValue!s}\n\n" f"Traceback:\n{txtTrace}\n" ) except Exception: diff --git a/novelwriter/extensions/eventfilters.py b/novelwriter/extensions/eventfilters.py index 7e0939b6..09366590 100644 --- a/novelwriter/extensions/eventfilters.py +++ b/novelwriter/extensions/eventfilters.py @@ -24,9 +24,13 @@ along with this program. If not, see . """ from __future__ import annotations +from typing import TYPE_CHECKING + from PyQt6.QtCore import QEvent, QObject from PyQt6.QtGui import QStatusTipEvent, QWheelEvent -from PyQt6.QtWidgets import QWidget + +if TYPE_CHECKING: + from PyQt6.QtWidgets import QWidget class WheelEventFilter(QObject): @@ -40,7 +44,7 @@ class WheelEventFilter(QObject): Reference: https://stackoverflow.com/a/17739995/5825851 """ - __slots__ = ("_parent", "_locked") + __slots__ = ("_locked", "_parent") def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 6a095d8e..9bea4c78 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -27,11 +27,9 @@ along with this program. If not, see . """ from __future__ import annotations -from enum import Enum from typing import TYPE_CHECKING from PyQt6.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot -from PyQt6.QtGui import QMouseEvent, QWheelEvent from PyQt6.QtWidgets import ( QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox, QToolButton, QTreeView, QWidget @@ -40,7 +38,11 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.types import QtMouseLeft, QtMouseMiddle -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: + from enum import Enum + + from PyQt6.QtGui import QMouseEvent, QWheelEvent + from novelwriter.guimain import GuiMain diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index b58de63a..b1745816 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -113,7 +113,7 @@ class NPagedSideBar(QToolBar): class _PagedToolButton(QToolButton): - __slots__ = ("_bH", "_tM", "_aH") + __slots__ = ("_aH", "_bH", "_tM") def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) diff --git a/novelwriter/extensions/progressbars.py b/novelwriter/extensions/progressbars.py index 456d1550..0c92564e 100644 --- a/novelwriter/extensions/progressbars.py +++ b/novelwriter/extensions/progressbars.py @@ -44,8 +44,8 @@ class NProgressCircle(QProgressBar): """ __slots__ = ( - "_text", "_point", "_dRect", "_cRect", "_dPen", "_dBrush", - "_cPen", "_bPen", "_tColor" + "_bPen", "_cPen", "_cRect", "_dBrush", "_dPen", "_dRect", "_point", + "_tColor", "_text", ) def __init__(self, parent: QWidget, size: int, point: int) -> None: diff --git a/novelwriter/extensions/statusled.py b/novelwriter/extensions/statusled.py index 53f0c65e..5547a664 100644 --- a/novelwriter/extensions/statusled.py +++ b/novelwriter/extensions/statusled.py @@ -35,7 +35,7 @@ logger = logging.getLogger(__name__) class StatusLED(QAbstractButton): - __slots__ = ("_neutral", "_postitve", "_negative", "_color", "_state") + __slots__ = ("_color", "_negative", "_neutral", "_postitve", "_state") def __init__(self, sW: int, sH: int, parent: QWidget | None = None) -> None: super().__init__(parent=parent) diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 5a97edde..421fb712 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -33,7 +33,7 @@ from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAntiAlias, QtSizeFixe class NSwitch(QAbstractButton): - __slots__ = ("_xW", "_xH", "_xR", "_rH", "_rR", "_offset") + __slots__ = ("_offset", "_rH", "_rR", "_xH", "_xR", "_xW") def __init__(self, parent: QWidget, height: int = 0) -> None: super().__init__(parent=parent) diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py index 9acc0cc9..c43def8a 100644 --- a/novelwriter/extensions/switchbox.py +++ b/novelwriter/extensions/switchbox.py @@ -23,8 +23,9 @@ along with this program. If not, see . """ from __future__ import annotations +from typing import TYPE_CHECKING + from PyQt6.QtCore import pyqtSignal -from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QGridLayout, QLabel, QScrollArea, QWidget from novelwriter.extensions.switch import NSwitch @@ -33,6 +34,9 @@ from novelwriter.types import ( QtSizeMinimumExpanding ) +if TYPE_CHECKING: + from PyQt6.QtGui import QIcon + class NSwitchBox(QScrollArea): """Extension: Switch Box Widget diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 76577826..c173918d 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -29,21 +29,25 @@ import re import xml.etree.ElementTree as ET from datetime import datetime -from pathlib import Path -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from zipfile import ZIP_DEFLATED, ZipFile from PyQt6.QtCore import QMargins, QSize -from PyQt6.QtGui import QColor from novelwriter import __version__ from novelwriter.common import firstFloat, xmlElement, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles -from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt from novelwriter.formats.tokenizer import Tokenizer from novelwriter.types import QtHexRgb +if TYPE_CHECKING: + from pathlib import Path + + from PyQt6.QtGui import QColor + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) # RegEx @@ -1052,9 +1056,9 @@ class ToDocX(Tokenizer): class DocXParagraph: __slots__ = ( - "_content", "_style", "_textAlign", - "_topMargin", "_bottomMargin", "_leftMargin", "_rightMargin", - "_indentFirst", "_breakBefore", "_breakAfter", "_footnoteRef", + "_bottomMargin", "_breakAfter", "_breakBefore", "_content", + "_footnoteRef", "_indentFirst", "_leftMargin", "_rightMargin", + "_style", "_textAlign", "_topMargin", ) def __init__(self) -> None: diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index fb2c1ee5..c7bf7b17 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -26,16 +26,20 @@ from __future__ import annotations import json import logging -from pathlib import Path from time import time +from typing import TYPE_CHECKING from novelwriter.common import formatTimeStamp from novelwriter.constants import nwHtmlUnicode, nwStyles -from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape from novelwriter.formats.tokenizer import Tokenizer from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb +if TYPE_CHECKING: + from pathlib import Path + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) # Each opener tag, with the id of its corresponding closer and tag format @@ -311,10 +315,8 @@ class ToHtml(Tokenizer): def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None: """Replace tabs with spaces in the html.""" - pages = [] tabSpace = spaceChar*nSpaces - for aLine in self._pages: - pages.append(aLine.replace("\t", tabSpace)) + pages = [aLine.replace("\t", tabSpace) for aLine in self._pages] self._pages = pages return diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index f44139a3..7d1aeec0 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -28,8 +28,7 @@ import logging import re from abc import ABC, abstractmethod -from pathlib import Path -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from PyQt6.QtCore import QLocale from PyQt6.QtGui import QColor, QFont @@ -40,7 +39,6 @@ from novelwriter.constants import ( nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwStats, nwStyles, nwUnicode, trConst ) -from novelwriter.core.project import NWProject from novelwriter.enum import nwComment, nwItemLayout from novelwriter.formats.shared import ( BlockFmt, BlockTyp, T_Block, T_Formats, T_Note, TextDocumentTheme, TextFmt @@ -48,6 +46,11 @@ from novelwriter.formats.shared import ( from novelwriter.text.comments import processComment from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser +if TYPE_CHECKING: + from pathlib import Path + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) @@ -72,7 +75,7 @@ HEADINGS = [ BlockTyp.TITLE, BlockTyp.PART, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4, ] -SKIP_INDENT = HEADINGS + [BlockTyp.SEP, BlockTyp.SKIP] +SKIP_INDENT = [*HEADINGS, BlockTyp.SEP, BlockTyp.SKIP] B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE) @@ -651,7 +654,7 @@ class Tokenizer(ABC): tText = aLine[2:].strip() tType = BlockTyp.HEAD1 if isPlain else BlockTyp.TITLE sHide = self._hidePart if isPlain else False - if not (isPlain or isNovel and sHide): + if not (isPlain or (isNovel and sHide)): tStyle |= self._titleStyle if isNovel: tType = BlockTyp.PART if isPlain else BlockTyp.TITLE @@ -1124,12 +1127,10 @@ class Tokenizer(ABC): temp.append((res.end(0), 0, TextFmt.HRF_E, "")) # Match Shortcodes - for res in REGEX_PATTERNS.shortcodePlain.finditer(text): - temp.append(( - res.start(1), res.end(1), - self._shortCodeFmt.get(res.group(1).lower(), 0), - "", - )) + temp.extend( + (res.start(1), res.end(1), self._shortCodeFmt.get(res.group(1).lower(), 0), "") + for res in REGEX_PATTERNS.shortcodePlain.finditer(text) + ) # Match Shortcode w/Values tHandle = self._handle or "" diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index 2f87813b..b87d0325 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -25,13 +25,17 @@ from __future__ import annotations import logging -from pathlib import Path +from typing import TYPE_CHECKING from novelwriter.constants import nwUnicode -from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt from novelwriter.formats.tokenizer import Tokenizer +if TYPE_CHECKING: + from pathlib import Path + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index fce5eec1..15622845 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -29,10 +29,9 @@ from __future__ import annotations import logging import xml.etree.ElementTree as ET -from collections.abc import Sequence from datetime import datetime from hashlib import sha256 -from pathlib import Path +from typing import TYPE_CHECKING, Final from zipfile import ZIP_DEFLATED, ZipFile from PyQt6.QtGui import QColor, QFont @@ -40,11 +39,16 @@ from PyQt6.QtGui import QColor, QFont from novelwriter import __version__ from novelwriter.common import xmlElement, xmlIndent, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles -from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape from novelwriter.formats.tokenizer import Tokenizer from novelwriter.types import FONT_STYLE, QtHexRgb +if TYPE_CHECKING: + from collections.abc import Sequence + from pathlib import Path + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) # Main XML NameSpaces @@ -1000,11 +1004,11 @@ class ODTParagraphStyle: exporter. Only the used settings are exposed here to keep the class minimal and fast. """ - VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"] - VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"] - VALID_LEVEL = ["1", "2", "3", "4"] - VALID_CLASS = ["text", "chapter", "extra"] - VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM + VALID_ALIGN: Final[list[str]] = ["start", "center", "end", "justify", "left", "right"] + VALID_BREAK: Final[list[str]] = ["auto", "page", "even-page", "odd-page", "inherit"] + VALID_LEVEL: Final[list[str]] = ["1", "2", "3", "4"] + VALID_CLASS: Final[list[str]] = ["text", "chapter", "extra"] + VALID_WEIGHT: Final[list[str]] = ["normal", "bold", *FONT_WEIGHT_NUM] def __init__(self, name: str) -> None: @@ -1207,9 +1211,9 @@ class ODTParagraphStyle: def getID(self) -> str: """Generate a unique ID from the settings.""" return sha256(( - f"Paragraph:Main:{str(self._mAttr)}:" - f"Paragraph:Para:{str(self._pAttr)}:" - f"Paragraph:Text:{str(self._tAttr)}:" + f"Paragraph:Main:{self._mAttr!s}:" + f"Paragraph:Para:{self._pAttr!s}:" + f"Paragraph:Text:{self._tAttr!s}:" ).encode()).hexdigest() def packXML(self, xParent: ET.Element) -> None: @@ -1237,13 +1241,13 @@ class ODTTextStyle: Only the used settings are exposed here to keep the class minimal and fast. """ - VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM - VALID_STYLE = ["normal", "italic", "oblique"] - VALID_POS = ["super", "sub"] - VALID_LSTYLE = ["none", "solid"] - VALID_LTYPE = ["single", "double"] - VALID_LWIDTH = ["auto"] - VALID_LCOL = ["font-color"] + VALID_WEIGHT: Final[list[str]] = ["normal", "bold", *FONT_WEIGHT_NUM] + VALID_STYLE: Final[list[str]] = ["normal", "italic", "oblique"] + VALID_POS: Final[list[str]] = ["super", "sub"] + VALID_LSTYLE: Final[list[str]] = ["none", "solid"] + VALID_LTYPE: Final[list[str]] = ["single", "double"] + VALID_LWIDTH: Final[list[str]] = ["auto"] + VALID_LCOL: Final[list[str]] = ["font-color"] def __init__(self, name: str) -> None: self._name = name diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 0cccfd09..f4bc897b 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -25,7 +25,7 @@ from __future__ import annotations import logging -from pathlib import Path +from typing import TYPE_CHECKING from PyQt6.QtCore import QMarginsF, QSizeF from PyQt6.QtGui import ( @@ -36,7 +36,6 @@ from PyQt6.QtPrintSupport import QPrinter from novelwriter import __version__ from novelwriter.constants import nwStyles, nwUnicode -from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt from novelwriter.formats.tokenizer import HEADINGS, Tokenizer from novelwriter.types import ( @@ -45,6 +44,11 @@ from novelwriter.types import ( QtPropLineHeight, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper ) +if TYPE_CHECKING: + from pathlib import Path + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat] diff --git a/novelwriter/formats/toraw.py b/novelwriter/formats/toraw.py index 241d7c77..c4ed979b 100644 --- a/novelwriter/formats/toraw.py +++ b/novelwriter/formats/toraw.py @@ -26,13 +26,17 @@ from __future__ import annotations import json import logging -from pathlib import Path from time import time +from typing import TYPE_CHECKING from novelwriter.common import formatTimeStamp -from novelwriter.core.project import NWProject from novelwriter.formats.tokenizer import Tokenizer +if TYPE_CHECKING: + from pathlib import Path + + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index f4d1bff5..208db194 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -97,10 +97,10 @@ class GuiDocEditor(QPlainTextEdit): """Gui Widget: Main Document Editor""" __slots__ = ( - "_nwDocument", "_nwItem", "_docChanged", "_docHandle", "_vpMargin", - "_lastEdit", "_lastActive", "_lastFind", "_doReplace", "_autoReplace", - "_completer", "_qDocument", "_keyContext", "_followTag1", "_followTag2", - "_timerDoc", "_wCounterDoc", "_timerSel", "_wCounterSel", + "_autoReplace", "_completer", "_doReplace", "_docChanged", "_docHandle", "_followTag1", + "_followTag2", "_keyContext", "_lastActive", "_lastEdit", "_lastFind", "_nwDocument", + "_nwItem", "_qDocument", "_timerDoc", "_timerSel", "_vpMargin", "_wCounterDoc", + "_wCounterSel", ) MOVE_KEYS = ( @@ -760,7 +760,7 @@ class GuiDocEditor(QPlainTextEdit): elif action == nwDocAction.REPL_SNG: self._replaceQuotes("'", CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose) elif action == nwDocAction.REPL_DBL: - self._replaceQuotes("\"", CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose) + self._replaceQuotes('"', CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose) elif action == nwDocAction.RM_BREAKS: self._removeInParLineBreaks() elif action == nwDocAction.ALIGN_L: @@ -2003,7 +2003,7 @@ class GuiDocEditor(QPlainTextEdit): sPos = cPos - i - 1 cOne = str(self._qDocument.characterAt(sPos)) cTwo = str(self._qDocument.characterAt(sPos - 1)) - if not (cOne.isalnum() or cOne in apos and cTwo.isalnum()): + if not (cOne.isalnum() or (cOne in apos and cTwo.isalnum())): sPos += 1 break @@ -2013,7 +2013,7 @@ class GuiDocEditor(QPlainTextEdit): ePos = cPos + i cOne = str(self._qDocument.characterAt(ePos)) cTwo = str(self._qDocument.characterAt(ePos + 1)) - if not (cOne.isalnum() or cOne in apos and cTwo.isalnum()): + if not (cOne.isalnum() or (cOne in apos and cTwo.isalnum())): break if ePos - sPos <= 0: @@ -2193,9 +2193,9 @@ class BackgroundWordCounterSignals(QObject): class TextAutoReplace: __slots__ = ( - "_quoteSO", "_quoteSC", "_quoteDO", "_quoteDC", - "_replaceSQuote", "_replaceDQuote", "_replaceDash", "_replaceDots", - "_padChar", "_padBefore", "_padAfter", "_doPadBefore", "_doPadAfter", + "_doPadAfter", "_doPadBefore", "_padAfter", "_padBefore", "_padChar", + "_quoteDC", "_quoteDO", "_quoteSC", "_quoteSO", "_replaceDQuote", + "_replaceDash", "_replaceDots", "_replaceSQuote", ) def __init__(self) -> None: diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 40d6c657..61082fa2 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -58,8 +58,8 @@ BLOCK_TITLE = 4 class GuiDocHighlighter(QSyntaxHighlighter): __slots__ = ( - "_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr", - "_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogParser", + "_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel", + "_minRules", "_spellCheck", "_spellErr", "_tHandle", "_txtRules", ) def __init__(self, document: QTextDocument) -> None: @@ -466,7 +466,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): class TextBlockData(QTextBlockUserData): - __slots__ = ("_text", "_offset", "_metaData", "_spellErrors") + __slots__ = ("_metaData", "_offset", "_spellErrors", "_text") def __init__(self) -> None: super().__init__() diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index d395a4ec..7d996992 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -26,6 +26,7 @@ from __future__ import annotations import logging from enum import Enum +from typing import TYPE_CHECKING from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import ( @@ -36,12 +37,14 @@ from PyQt6.QtWidgets import ( from novelwriter import SHARED from novelwriter.common import checkInt, qtAddAction from novelwriter.constants import nwLabels, nwLists, nwStyles, trConst -from novelwriter.core.indexdata import IndexHeading, IndexNode from novelwriter.enum import nwChange, nwDocMode, nwItemClass from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON from novelwriter.types import QtDecoration, QtHeaderFixed, QtHeaderToContents, QtUserRole +if TYPE_CHECKING: + from novelwriter.core.indexdata import IndexHeading, IndexNode + logger = logging.getLogger(__name__) diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py index e2753038..d42ffaf1 100644 --- a/novelwriter/gui/editordocument.py +++ b/novelwriter/gui/editordocument.py @@ -25,8 +25,8 @@ from __future__ import annotations import logging -from collections.abc import Iterable from time import time +from typing import TYPE_CHECKING from PyQt6.QtCore import QObject, pyqtSlot from PyQt6.QtGui import QTextBlock, QTextCursor, QTextDocument @@ -35,6 +35,9 @@ from PyQt6.QtWidgets import QApplication, QPlainTextDocumentLayout from novelwriter import SHARED from novelwriter.gui.dochighlight import GuiDocHighlighter, TextBlockData +if TYPE_CHECKING: + from collections.abc import Iterable + logger = logging.getLogger(__name__) diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index d873b8a1..9bece183 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -228,7 +228,7 @@ class GuiItemDetails(QWidget): return self._handle = tHandle - iPx = int(round(0.9*SHARED.theme.baseIconHeight)) + iPx = round(0.9*SHARED.theme.baseIconHeight) # Label # ===== diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 7bc2d196..33f97dde 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -41,7 +41,7 @@ from novelwriter.constants import ( from novelwriter.enum import nwDocAction, nwDocInsert, nwFocus, nwView from novelwriter.extensions.eventfilters import StatusTipFilter -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.guimain import GuiMain logger = logging.getLogger(__name__) @@ -471,7 +471,7 @@ class GuiMainMenu(QMenuBar): # Insert > Double Prime self.aInsDPrime = qtAddAction(self.mInsPunct, self.tr("Double Prime")) - self.aInsDPrime.setShortcut("Ctrl+K, Ctrl+\"") + self.aInsDPrime.setShortcut('Ctrl+K, Ctrl+"') self.aInsDPrime.triggered.connect( lambda: self.requestDocInsertText.emit(nwUnicode.U_DPRIME) ) @@ -680,7 +680,7 @@ class GuiMainMenu(QMenuBar): # Format > Double Quotes self.aFmtDQuote = qtAddAction(self.fmtMenu, self.tr("Wrap Double Quotes")) - self.aFmtDQuote.setShortcut("Ctrl+\"") + self.aFmtDQuote.setShortcut('Ctrl+"') self.aFmtDQuote.triggered.connect( lambda: self.requestDocAction.emit(nwDocAction.D_QUOTE) ) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index a6551466..10a396b4 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -32,6 +32,7 @@ import logging from enum import Enum from time import time +from typing import Final from PyQt6.QtCore import QT_TRANSLATE_NOOP, Qt, pyqtSignal, pyqtSlot from PyQt6.QtGui import QAction, QIcon @@ -319,7 +320,7 @@ class GuiOutlineToolBar(QToolBar): class GuiOutlineTree(QTreeWidget): - DEF_WIDTH = { + DEF_WIDTH: Final[dict[nwOutline, int]] = { nwOutline.TITLE: 200, nwOutline.LEVEL: 40, nwOutline.LABEL: 150, @@ -342,7 +343,7 @@ class GuiOutlineTree(QTreeWidget): nwOutline.SYNOP: 200, } - DEF_HIDDEN = { + DEF_HIDDEN: Final[dict[nwOutline, bool]] = { nwOutline.TITLE: False, nwOutline.LEVEL: True, nwOutline.LABEL: False, @@ -498,7 +499,7 @@ class GuiOutlineTree(QTreeWidget): tree. """ # If it's the first time, we always build - if self._firstView or self._firstView and overRide: + if self._firstView or (self._firstView and overRide): self._loadHeaderState() self._populateTree(rootHandle) self._firstView = False @@ -558,7 +559,7 @@ class GuiOutlineTree(QTreeWidget): logger.info("Writing CSV file: %s", path) cols = [col for col in self._treeOrder if not self._colHidden[col]] order = [self._colIdx[col] for col in cols] - with open(path, mode="w", newline="") as csvFile: + with open(path, mode="w", newline="", encoding="utf-8") as csvFile: writer = csv.writer(csvFile, dialect="excel", quoting=csv.QUOTE_ALL) writer.writerow([trConst(nwLabels.OUTLINE_COLS[col]) for col in cols]) for i in range(self.topLevelItemCount()): @@ -795,7 +796,7 @@ class GuiOutlineHeaderMenu(QMenu): class GuiOutlineDetails(QScrollArea): - LVL_MAP = { + LVL_MAP: Final[dict[str, str]] = { "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"), "H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"), "H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"), diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 721324f7..f8b13e9f 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1099,7 +1099,7 @@ class _UpdatableMenu(QMenu): class _TreeContextMenu(QMenu): - __slots__ = ("_tree", "_view", "_node", "_item", "_model", "_handle", "_indices", "_children") + __slots__ = ("_children", "_handle", "_indices", "_item", "_model", "_node", "_tree", "_view") def __init__( self, projTree: GuiProjectTree, model: ProjectModel, @@ -1327,7 +1327,7 @@ class _TreeContextMenu(QMenu): """Add move to Trash action.""" if ( self._model.trashSelection(self._indices) - or len(self._indices) == 1 and self._item.isRootType() + or (len(self._indices) == 1 and self._item.isRootType()) ): text = self.tr("Delete Permanently") else: diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py index bf403012..323b2200 100644 --- a/novelwriter/gui/search.py +++ b/novelwriter/gui/search.py @@ -26,6 +26,7 @@ from __future__ import annotations import logging from time import time +from typing import TYPE_CHECKING from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt6.QtGui import QAction, QCursor, QKeyEvent, QPalette @@ -37,12 +38,14 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import checkInt, qtAddAction from novelwriter.core.coretools import DocSearch -from novelwriter.core.item import NWItem from novelwriter.types import ( QtAlignMiddle, QtAlignRight, QtHeaderStretch, QtHeaderToContents, QtHexArgb, QtUserRole ) +if TYPE_CHECKING: + from novelwriter.core.item import NWItem + logger = logging.getLogger(__name__) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index e69b1dad..f784623f 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -37,7 +37,7 @@ from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.guimain import GuiMain logger = logging.getLogger(__name__) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 729db7ee..207bc6fc 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -27,7 +27,7 @@ from __future__ import annotations import logging from math import ceil -from pathlib import Path +from typing import TYPE_CHECKING, Final from PyQt6.QtCore import QSize, Qt from PyQt6.QtGui import ( @@ -44,6 +44,9 @@ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent +if TYPE_CHECKING: + from pathlib import Path + logger = logging.getLogger(__name__) STYLES_FLAT_TABS = "flatTabWidget" @@ -93,18 +96,13 @@ class GuiTheme: """ __slots__ = ( - # Attributes - "iconCache", "themeMeta", "isDarkTheme", "helpText", "fadedText", "errorText", - "syntaxMeta", "syntaxTheme", "guiFont", "guiFontB", "guiFontBU", "guiFontSmall", - "fontPointSize", "fontPixelSize", "baseIconHeight", "baseButtonHeight", "textNHeight", - "textNWidth", "baseIconSize", "buttonIconSize", "guiFontFixed", - - # Functions - "getIcon", "getPixmap", "getItemIcon", "getIconColor", "getToggleIcon", "getDecoration", - "getHeaderDecoration", "getHeaderDecorationNarrow", - - # Internal - "_guiPalette", "_themeList", "_syntaxList", "_availThemes", "_availSyntax", "_styleSheets", + "_availSyntax", "_availThemes", "_guiPalette", "_styleSheets", "_syntaxList", "_themeList", + "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", + "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", + "getHeaderDecorationNarrow", "getIcon", "getIconColor", "getItemIcon", "getPixmap", + "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", + "helpText", "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", + "textNWidth", "themeMeta", ) def __init__(self) -> None: @@ -164,9 +162,9 @@ class GuiTheme: fHeight = qMetric.height() fAscent = qMetric.ascent() self.fontPointSize = self.guiFont.pointSizeF() - self.fontPixelSize = int(round(fHeight)) - self.baseIconHeight = int(round(fAscent)) - self.baseButtonHeight = int(round(1.35*fAscent)) + self.fontPixelSize = round(fHeight) + self.baseIconHeight = round(fAscent) + self.baseButtonHeight = round(1.35*fAscent) self.textNHeight = qMetric.boundingRect("N").height() self.textNWidth = qMetric.boundingRect("N").width() @@ -202,7 +200,7 @@ class GuiTheme: qMetrics = QFontMetrics(font) else: qMetrics = QFontMetrics(self.guiFont) - return int(ceil(qMetrics.boundingRect(text).width())) + return ceil(qMetrics.boundingRect(text).width()) ## # Theme Methods @@ -567,16 +565,16 @@ class GuiIcons: """ __slots__ = ( - "mainTheme", "themeMeta", "_svgData", "_svgColors", "_qColors", - "_qIcons", "_headerDec", "_headerDecNarrow", "_availThemes", - "_themeList", "_noIcon", + "_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon", + "_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList", + "mainTheme", "themeMeta", ) - TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = { + TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = { "bullet": ("bullet-on", "bullet-off"), "unfold": ("unfold-show", "unfold-hide"), } - IMAGE_MAP: dict[str, tuple[str, str]] = { + IMAGE_MAP: Final[dict[str, tuple[str, str]]] = { "welcome": ("welcome-light.jpg", "welcome-dark.jpg"), "nw-text": ("novelwriter-text-light.svg", "novelwriter-text-dark.svg"), } diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ffff4560..fac283f3 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1009,7 +1009,7 @@ class GuiMain(QMainWindow): fN = self.novelView.treeHasFocus() self._changeView(nwView.EDITOR) - if (vM and (vP and fP or vN and not fN)) or (not vM and vN): + if (vM and ((vP and fP) or (vN and not fN))) or (not vM and vN): self._changeView(nwView.NOVEL) self.novelView.setTreeFocus() else: diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 9fbd5581..04add94c 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -40,7 +40,7 @@ from novelwriter.constants import nwFiles from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.enum import nwChange, nwItemClass -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.core.project import NWProject from novelwriter.core.status import T_StatusKind from novelwriter.gui.theme import GuiTheme @@ -54,8 +54,8 @@ NWWidget = TypeVar("NWWidget", bound=QWidget) class SharedData(QObject): __slots__ = ( - "_gui", "_theme", "_project", "_spelling", "_lockedBy", "_lastAlert", - "_idleTime", "_idleRefTime", + "_gui", "_idleRefTime", "_idleTime", "_lastAlert", "_lockedBy", + "_project", "_spelling", "_theme", ) focusModeChanged = pyqtSignal(bool) @@ -480,7 +480,7 @@ class _GuiAlert(QMessageBox): def setException(self, exception: Exception) -> None: """Add exception details.""" info = self.informativeText() - text = f"{type(exception).__name__}: {str(exception)}" + text = f"{type(exception).__name__}: {exception!s}" self.setInformativeText(f"{info}
{text}" if info else text) return diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py index c4e0bf07..d592b70c 100644 --- a/novelwriter/text/patterns.py +++ b/novelwriter/text/patterns.py @@ -128,8 +128,8 @@ REGEX_PATTERNS = RegExPatterns() class DialogParser: __slots__ = ( - "_quotes", "_dialog", "_alternate", "_enabled", - "_narrator", "_breakD", "_breakQ", "_mode", + "_alternate", "_breakD", "_breakQ", "_dialog", "_enabled", "_mode", + "_narrator", "_quotes", ) def __init__(self) -> None: diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 48aefa2d..b2028dca 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -124,10 +124,10 @@ class GuiLipsum(NDialog): @classmethod def getLipsum(cls, parent: QWidget) -> str: """Pop the dialog and return the lipsum text.""" - cls = GuiLipsum(parent) - cls.exec() - text = cls.lipsumText - cls.softDelete() + dialog = cls(parent) + dialog.exec() + text = dialog.lipsumText + dialog.softDelete() return text ## diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index f45d7a1f..22628f43 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -26,9 +26,9 @@ from __future__ import annotations import logging from pathlib import Path +from typing import TYPE_CHECKING from PyQt6.QtCore import QTimer, pyqtSlot -from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import ( QAbstractButton, QAbstractItemView, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, @@ -38,7 +38,6 @@ from PyQt6.QtWidgets import ( from novelwriter import SHARED from novelwriter.common import makeFileNameSafe, openExternalPath from novelwriter.constants import nwLabels -from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.item import NWItem from novelwriter.enum import nwBuildFmt @@ -46,6 +45,11 @@ from novelwriter.extensions.modified import NDialog, NIconToolButton from novelwriter.extensions.progressbars import NProgressSimple from novelwriter.types import QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole +if TYPE_CHECKING: + from PyQt6.QtGui import QCloseEvent + + from novelwriter.core.buildsettings import BuildSettings + logger = logging.getLogger(__name__) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index f06505d9..b6918b18 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -59,7 +59,7 @@ from novelwriter.types import ( QtUserRole ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.guimain import GuiMain logger = logging.getLogger(__name__) @@ -427,10 +427,10 @@ class GuiManuscript(NToolDialog): def _saveSettings(self) -> None: """Save the user GUI settings.""" - buildOrder = [] - for i in range(self.buildList.count()): - if item := self.buildList.item(i): - buildOrder.append(item.data(self.D_KEY)) + buildOrder = [ + item.data(self.D_KEY) for i in range(self.buildList.count()) + if (item := self.buildList.item(i)) + ] current = self.buildList.currentItem() lastBuild = current.data(self.D_KEY) if isinstance(current, QListWidgetItem) else "" @@ -744,7 +744,7 @@ class _PreviewWidget(QTextBrowser): document.setDocumentMargin(CONFIG.textMargin) self.setPlaceholderText(self.tr( - "Press the \"Preview\" button to generate ..." + 'Press the "Preview" button to generate ...' )) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index f0f6d1c0..cfac8f11 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -55,7 +55,7 @@ from novelwriter.types import ( QtUserRole ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.guimain import GuiMain logger = logging.getLogger(__name__) diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 9f4bfb26..ec42ccf1 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -26,8 +26,9 @@ from __future__ import annotations import logging import math +from typing import TYPE_CHECKING + from PyQt6.QtCore import pyqtSlot -from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import ( QAbstractItemView, QDialogButtonBox, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget, QTreeWidgetItem, @@ -44,6 +45,9 @@ from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch from novelwriter.types import QtAlignRight, QtDecoration, QtDialogClose +if TYPE_CHECKING: + from PyQt6.QtGui import QCloseEvent + logger = logging.getLogger(__name__) diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 4d2a0679..3638c18d 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -385,7 +385,7 @@ class _OpenProjectPage(QWidget): class _ProjectListItem(QStyledItemDelegate): - __slots__ = ("_pPx", "_hPx", "_tFont", "_dFont", "_dPen", "_icon") + __slots__ = ("_dFont", "_dPen", "_hPx", "_icon", "_pPx", "_tFont") def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 3a8aab06..f2c14be2 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -47,7 +47,7 @@ from novelwriter.types import ( QtDialogClose, QtRoleAction ) -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: from novelwriter.guimain import GuiMain logger = logging.getLogger(__name__) @@ -124,7 +124,7 @@ class GuiWritingStats(NToolDialog): self.listBox.setSortingEnabled(True) # Word Bar - self.barHeight = int(round(0.5*SHARED.theme.fontPixelSize)) + self.barHeight = round(0.5*SHARED.theme.fontPixelSize) self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage.fill(self.palette().highlight().color()) diff --git a/pyproject.toml b/pyproject.toml index 76bff95a..0804a98c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,30 +60,48 @@ line-length = 99 [tool.ruff.lint] preview = true + +# Rules: https://docs.astral.sh/ruff/rules select = [ - "A", # flake8-builtins (A) - "B", # flake8-bugbear (B) - "E", # pycodestyle (E) - "F", # Pyflakes (F) - "W", # pycodestyle (W) - "UP", # pyupgrade (UP) - "ANN", # flake8-annotations (ANN) - "PLE", # Pylint Error (PLE) - "FA", # flake8-future-annotations (FA) + "A", # flake8-builtins (A) + "ANN", # flake8-annotations (ANN) + "B", # flake8-bugbear (B) + "E", # pycodestyle (E) + "F", # Pyflakes (F) + "FA", # flake8-future-annotations (FA) + "PERF", # Perflint (PERF) + "PLC", # Pylint Convention (PLC) + "PLE", # Pylint Error (PLE) + "PLW", # Pylint Warning (PLW) + "Q", # flake8-quotes (Q) + "RUF", # Ruff-specific rules (RUF) + "SLF", # flake8-self (SLF) + "SLOT", # flake8-slots (SLOT) + "TC", # flake8-type-checking (TC) + "UP", # pyupgrade (UP) + "W", # pycodestyle (W) ] ignore = [ - "E221", # multiple-spaces-before-operator - "E226", # missing-whitespace-around-arithmetic-operator - "E228", # missing-whitespace-around-modulo-operator - "E241", # multiple-spaces-after-comma - "E272", # multiple-spaces-before-keyword - "ANN401", # any-type - "UP015", # redundant-open-modes - "UP030", # format-literals + "ANN401", # any-type + "E221", # multiple-spaces-before-operator + "E226", # missing-whitespace-around-arithmetic-operator + "E228", # missing-whitespace-around-modulo-operator + "E241", # multiple-spaces-after-comma + "E272", # multiple-spaces-before-keyword + "PLC0415", # import-outside-top-level + "PLC1901", # compare-to-empty-string + "PLW0108", # unnecessary-lambda + "PLW2901", # redefined-loop-name + "RUF001", # ambiguous-unicode-character-string + "RUF002", # ambiguous-unicode-character-docstring + "RUF015", # unnecessary-iterable-allocation-for-first-element + "UP015", # redundant-open-modes + "UP030", # format-literals ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["ANN"] +"tests/*" = ["ANN", "SLF", "TC", "PLC2701"] +"utils/*" = ["ANN", "SLF", "TC"] [tool.ruff.format] quote-style = "double" @@ -111,3 +129,6 @@ branch = false [tool.coverage.report] precision = 2 +exclude_also = [ + "if TYPE_CHECKING:" +] diff --git a/requirements-dev.txt b/requirements-dev.txt index f4e05a41..18c97a30 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,3 @@ -ruff isort pyright +ruff diff --git a/tests/conftest.py b/tests/conftest.py index 3dffc8c6..33baa870 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,10 +33,10 @@ from PyQt6.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) -from novelwriter import CONFIG, SHARED # noqa: E402 +from novelwriter import CONFIG, SHARED -from tests.mocked import MockGuiMain, MockTheme # noqa: E402 -from tests.tools import cleanProject # noqa: E402 +from tests.mocked import MockGuiMain, MockTheme +from tests.tools import cleanProject _TST_ROOT = Path(__file__).parent _TMP_ROOT = _TST_ROOT / "temp" @@ -89,7 +89,7 @@ def functionFixture(qtbot): shutil.rmtree(_TMP_CONF) _TMP_CONF.mkdir() - CONFIG.__init__() + CONFIG.__init__() # noqa: PLC2801 CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF) resetConfigVars() logging.getLogger("novelwriter").setLevel(logging.INFO) diff --git a/tests/requirements.txt b/tests/requirements.txt index 7314a844..39f96e3c 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,5 @@ -pytest>=6.0.0 -pytest-timeout +coverage>=7.2.0 pytest-cov pytest-qt +pytest-timeout +pytest>=6.0.0 diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 7e03b895..bfd2b55c 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -556,7 +556,7 @@ def testBaseCommon_jsonEncode(): # Correct types assert jsonEncode([1, 2]) == "[\n 1,\n 2\n]" assert jsonEncode((1, 2)) == "[\n 1,\n 2\n]" - assert jsonEncode({1: 2}) == "{\n \"1\": 2\n}" + assert jsonEncode({1: 2}) == '{\n "1": 2\n}' tstDict = { "null": None, diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index b58c1fd2..74468629 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -141,8 +141,8 @@ def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths): assert newConf.guiSyntax == "bar" # Test Correcting Quote Settings - tstConf.fmtDQuoteOpen = "\"" - tstConf.fmtDQuoteClose = "\"" + tstConf.fmtDQuoteOpen = '"' + tstConf.fmtDQuoteClose = '"' tstConf.fmtSQuoteOpen = "'" tstConf.fmtSQuoteClose = "'" tstConf.doReplaceDQuote = True diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 5b9f6c57..0bd4b24c 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -768,7 +768,7 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths): assert builder.buildProject(data) is False # Create and open a defective zip file - with open(dstSample, mode="w+") as outFile: + with open(dstSample, mode="w+", encoding="utf-8") as outFile: outFile.write("foo") assert builder.buildProject(data) is False diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index 39fa9297..acd5d19c 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -42,6 +42,8 @@ from tests.tools import cmpFiles, writeFile class MockProject: """Fake project object.""" + data: NWProjectData + def setProjectChanged(self, *a): """Fake project method.""" pass @@ -219,7 +221,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, mockGUI, tstPaths, fncPath): packedContent = [] mockProject = MockProject() - mockProject.__setattr__("data", data) + mockProject.data = data for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore item.unpack(entry) @@ -348,7 +350,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockGUI, mockRnd): packedContent = [] mockProject = MockProject() - mockProject.__setattr__("data", data) + mockProject.data = data status = {} for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore @@ -493,7 +495,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockGUI, mockRnd): packedContent = [] mockProject = MockProject() - mockProject.__setattr__("data", data) + mockProject.data = data status = {} for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore @@ -638,7 +640,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockGUI, mockRnd): packedContent = [] mockProject = MockProject() - mockProject.__setattr__("data", data) + mockProject.data = data status = {} for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore @@ -786,7 +788,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockGUI, mockRnd): packedContent = [] mockProject = MockProject() - mockProject.__setattr__("data", data) + mockProject.data = data status = {} for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore @@ -933,7 +935,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockGUI, mockRnd): packedContent = [] mockProject = MockProject() - mockProject.__setattr__("data", data) + mockProject.data = data status = {} for entry in content: item = NWItem(mockProject, "0000000000000") # type: ignore diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index 4f72f9df..05fde0e5 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -982,7 +982,7 @@ def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths, ipsumText): def prettifyXml(inFile, outFile): with open(outFile, mode="wb") as fStream: xml = ET.parse(inFile) - xmlIndent(xml) + xmlIndent(xml.getroot()) xml.write(fStream, encoding="utf-8", xml_declaration=True) prettifyXml(maniOut, maniFile) @@ -1082,10 +1082,6 @@ def testFmtToOdt_ODTParagraphStyle(): assert parStyle._pAttr["text-align"] == ["fo", "end"] parStyle.setTextAlign("justify") assert parStyle._pAttr["text-align"] == ["fo", "justify"] - parStyle.setTextAlign("inside") - assert parStyle._pAttr["text-align"] == ["fo", "inside"] - parStyle.setTextAlign("outside") - assert parStyle._pAttr["text-align"] == ["fo", "outside"] parStyle.setTextAlign("left") assert parStyle._pAttr["text-align"] == ["fo", "left"] parStyle.setTextAlign("right") @@ -1099,8 +1095,6 @@ def testFmtToOdt_ODTParagraphStyle(): assert parStyle._pAttr["break-before"] == ["fo", None] parStyle.setBreakBefore("auto") assert parStyle._pAttr["break-before"] == ["fo", "auto"] - parStyle.setBreakBefore("column") - assert parStyle._pAttr["break-before"] == ["fo", "column"] parStyle.setBreakBefore("page") assert parStyle._pAttr["break-before"] == ["fo", "page"] parStyle.setBreakBefore("even-page") @@ -1118,8 +1112,6 @@ def testFmtToOdt_ODTParagraphStyle(): assert parStyle._pAttr["break-after"] == ["fo", None] parStyle.setBreakAfter("auto") assert parStyle._pAttr["break-after"] == ["fo", "auto"] - parStyle.setBreakAfter("column") - assert parStyle._pAttr["break-after"] == ["fo", "column"] parStyle.setBreakAfter("page") assert parStyle._pAttr["break-after"] == ["fo", "page"] parStyle.setBreakAfter("even-page") @@ -1394,9 +1386,9 @@ def testFmtToOdt_XMLParagraph(): # Plain Text xmlPar.appendText("Hello World") assert xmlToText(xRoot) == ( - '' - 'Hello World' - '' + "" + "Hello World" + "" ) # Text Span @@ -1431,9 +1423,9 @@ def testFmtToOdt_XMLParagraph(): # Plain Text w/Line Break xmlPar.appendText("Hello\nWorld\n!!") assert xmlToText(xRoot) == ( - '' - 'HelloWorld!!' - '' + "" + "HelloWorld!!" + "" ) # Text Span w/Line Break @@ -1467,9 +1459,9 @@ def testFmtToOdt_XMLParagraph(): # Plain Text w/Line Break xmlPar.appendText("Hello\tWorld\t!!") assert xmlToText(xRoot) == ( - '' - 'HelloWorld!!' - '' + "" + "HelloWorld!!" + "" ) # Text Span w/Line Break diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index be76f526..312c88bb 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -763,7 +763,7 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") # Replace Double Quotes - repText = text.replace("consectetur", "\"consectetur\"") + repText = text.replace("consectetur", '"consectetur"') docEditor.replaceText(repText) assert docEditor.docAction(nwDocAction.SEL_ALL) is True assert docEditor.docAction(nwDocAction.REPL_DBL) is True diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index da2d3ba0..24694ffe 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -116,7 +116,8 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath): # Check that project open dialog launches nwGUI.postLaunchTasks(None) qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiWelcome) is not None, timeout=1000) - assert isinstance(welcome := SHARED.findTopLevelWidget(GuiWelcome), GuiWelcome) + welcome = SHARED.findTopLevelWidget(GuiWelcome) + assert isinstance(welcome, GuiWelcome) welcome.show() welcome.close() @@ -454,7 +455,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): # Dialogue # ======== - for c in "\"Full line double quoted text.\"": + for c in '"Full line double quoted text."': 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) @@ -469,7 +470,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): CONFIG.fmtPadAfter = "\u201c" docEditor.initEditor() - for c in "Some \"double quoted text with spaces padded\".": + for c in 'Some "double quoted text with spaces padded".': 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) @@ -533,17 +534,17 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): # ================ nwGUI._switchFocus(nwView.EDITOR) - for c in "\t\"Tab-indented text\"": + for c in '\t"Tab-indented text"': 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) - for c in ">\"Paragraph-indented text\"": + for c in '>"Paragraph-indented text"': 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) - for c in ">>\"Right-aligned text\"": + for c in '>>"Right-aligned text"': 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) diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index ec29a233..f63f588a 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -279,7 +279,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): docEditor.setPlainText( "### New Text\n\n" "Text with 'single' quotes and 'tricky stuff's'.\n\n" - "Also text with \"double\" quotes which are \"less tricky\".\n\n" + 'Also text with "double" quotes which are "less tricky".\n\n' ) mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) @@ -287,7 +287,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): assert docEditor.getText() == ( "### New Text\n\n" "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n" - "Also text with \"double\" quotes which are \"less tricky\".\n\n" + 'Also text with "double" quotes which are "less tricky".\n\n' ) mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) @@ -347,7 +347,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "### New Text\n\n" "@tag: Bod\n\n" "Text with 'single' quotes and 'tricky stuff's'.\n\n" - "Also text with \"double\" quotes which are \"less tricky\".\n\n" + 'Also text with "double" quotes which are "less tricky".\n\n' ) # Cannot Format Tag @@ -363,7 +363,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "### New Text\n\n" "@tag: Bod\n\n" "Text with 'single' quotes and 'tricky stuff's'.\n\n" - "Also text with \"double\" quotes which are \"less tricky\".\n\n" + 'Also text with "double" quotes which are "less tricky".\n\n' ) # qtbot.stop() diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index ca459292..ee938e10 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -29,15 +29,12 @@ from novelwriter.constants import nwUnicode from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser -def allMatches(regEx: re.Pattern, text: str) -> list[list[str]]: +def allMatches(regEx: re.Pattern, text: str) -> list[list[tuple[str, int, int]]]: """Get all matches for a regex.""" - result = [] - for res in regEx.finditer(text): - result.append([ - (res.group(n), res.start(n), res.end(n)) - for n in range((res.lastindex or 0) + 1) - ]) - return result + return [ + [(res.group(n), res.start(n), res.end(n)) for n in range((res.lastindex or 0) + 1)] + for res in regEx.finditer(text) + ] @pytest.mark.core @@ -310,7 +307,7 @@ def testTextPatterns_DialogueStyle(): assert allMatches(regEx, "one 'two' three") == [] # Straight double quotes are ignored - assert allMatches(regEx, "one \"two\" three") == [] + assert allMatches(regEx, 'one "two" three') == [] # Check with no whitespace, single quote assert allMatches(regEx, "one\u2018two\u2019three") == [ @@ -370,19 +367,19 @@ def testTextPatterns_DialoguePlain(): # ====== # One double quoted string - assert allMatches(regEx, "one \"two\" three") == [ - [("\"two\"", 4, 9)] + assert allMatches(regEx, 'one "two" three') == [ + [('"two"', 4, 9)] ] # Two double quoted strings - assert allMatches(regEx, "one \"two\" three \"four\" five") == [ - [("\"two\"", 4, 9)], [("\"four\"", 16, 22)], + assert allMatches(regEx, 'one "two" three "four" five') == [ + [('"two"', 4, 9)], [('"four"', 16, 22)], ] # No space - assert allMatches(regEx, "one\"two\" three") == [] - assert allMatches(regEx, "one \"two\"three") == [] - assert allMatches(regEx, "one\"two\"three") == [] + assert allMatches(regEx, 'one"two" three') == [] + assert allMatches(regEx, 'one "two"three') == [] + assert allMatches(regEx, 'one"two"three') == [] # Single # ====== @@ -595,6 +592,6 @@ def testTextPatterns_DialogParserPolish(): ] assert parser( - "And so on and so forth. However, \"text in quotation marks\" should not be " + 'And so on and so forth. However, "text in quotation marks" should not be ' "highlighted at all, and if so, it should be highlighted differently." ) == [] diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 9a86eff6..50d04391 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -83,8 +83,7 @@ def testToolManuscriptBuild_Main( if item.data(manus.D_KEY) == fmt: manus.listFormats.setCurrentItem(item) return - else: - raise ValueError("No such key in format list") + raise ValueError("No such key in format list") # Build documents lastFmt = None diff --git a/utils/assets.py b/utils/assets.py index dc53b8b3..b9291c74 100644 --- a/utils/assets.py +++ b/utils/assets.py @@ -180,9 +180,9 @@ def updateTranslationSources(args: argparse.Namespace) -> None: else: # Create an empty new language file langCode = item.name[3:-3] writeFile(item, ( - "\n" + '\n' "\n" - f"\n" + f'\n' )) translations.append(item) print(f"Created: {item}") diff --git a/utils/build_debian.py b/utils/build_debian.py index b962e620..ea08e705 100644 --- a/utils/build_debian.py +++ b/utils/build_debian.py @@ -134,10 +134,10 @@ def makeDebianPackage( signArgs = [f"-k{signKey}"] if sourceBuild: - subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir) + subprocess.call(["debuild", "-S", *signArgs], cwd=outDir) toUpload(bldDir / f"{bldPkg}.tar.xz") else: - subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir) + subprocess.call(["dpkg-buildpackage", *signArgs], cwd=outDir) shutil.copyfile(bldDir / f"{bldPkg}.tar.xz", bldDir / f"{bldPkg}.debian.tar.xz") toUpload(bldDir / f"{bldPkg}.debian.tar.xz") toUpload(bldDir / f"{bldPkg}_all.deb") @@ -195,7 +195,7 @@ def launchpad(args: argparse.Namespace) -> None: signKey = SIGN_KEY if args.sign else None - print(f"Sign Key: {str(signKey)}") + print(f"Sign Key: {signKey!s}") print("") dputCmd = [] diff --git a/utils/build_windows.py b/utils/build_windows.py index 89766be0..a4f0e866 100644 --- a/utils/build_windows.py +++ b/utils/build_windows.py @@ -218,9 +218,9 @@ def main(args: argparse.Namespace) -> None: "import sys\n" "\n" "os.curdir = os.path.abspath(os.path.dirname(__file__))\n" - "sys.path.insert(0, os.path.join(os.curdir, \"lib\"))\n" + 'sys.path.insert(0, os.path.join(os.curdir, "lib"))\n' "\n" - "if __name__ == \"__main__\":\n" + 'if __name__ == "__main__":\n' " import novelwriter\n" " novelwriter.main(sys.argv[1:])\n" )) diff --git a/utils/common.py b/utils/common.py index c5d88469..6eda3841 100644 --- a/utils/common.py +++ b/utils/common.py @@ -137,7 +137,7 @@ def makeCheckSum(sumFile: str, cwd: Path | None = None) -> str: shaFile = f"{sumFile}.sha256" else: shaFile = cwd / f"{sumFile}.sha256" - with open(shaFile, mode="w") as fOut: + with open(shaFile, mode="w", encoding="utf-8") as fOut: subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd) print(f"SHA256 Sum: {shaFile}") except Exception as exc: