From 5f6335d8f9165eec4c8dc8efaab22cbfdaa4941a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 4 Jun 2023 00:40:04 +0200 Subject: [PATCH] Fix some typing issues --- novelwriter/common.py | 108 ++++++++++++++---------------- novelwriter/core/buildsettings.py | 8 +-- novelwriter/core/options.py | 46 +++++++------ novelwriter/core/projectdata.py | 15 +++-- novelwriter/core/projectxml.py | 4 +- novelwriter/core/storage.py | 2 +- setup.cfg | 2 +- 7 files changed, 95 insertions(+), 90 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 386681e4..76779604 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -29,13 +29,13 @@ import hashlib import logging import xml.etree.ElementTree as ET -from typing import Any +from typing import Any, Literal from pathlib import Path from datetime import datetime from configparser import ConfigParser from PyQt5.QtCore import QCoreApplication -from PyQt5.QtWidgets import qApp +from PyQt5.QtWidgets import QWidget, qApp from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.error import logException @@ -113,14 +113,14 @@ def checkHandle(value, default, allowNone=False): def checkUuid(value: Any, default: str) -> str: - """Try to process a value as an uuid, or return a default.""" + """Try to process a value as an UUID, or return a default.""" try: return str(uuid.UUID(value)) except Exception: return default -def checkPath(value, default): +def checkPath(value: Any, default: Path) -> Path: """Check if a value is a valid path. Non-empty strings are accepted. """ if isinstance(value, Path): @@ -135,7 +135,7 @@ def checkPath(value, default): # Validator Functions # =============================================================================================== # -def isHandle(value): +def isHandle(value: Any) -> bool: """Check if a string is a valid novelWriter handle. Note: This is case sensitive. Must be lower case! """ @@ -149,9 +149,8 @@ def isHandle(value): return True -def isTitleTag(value): - """Check if a string is a valid title tag string. - """ +def isTitleTag(value: Any) -> bool: + """Check if a string is a valid title tag string.""" if not isinstance(value, str): return False if len(value) != 5: @@ -164,27 +163,23 @@ def isTitleTag(value): return True -def isItemClass(value): - """Check if a string is a valid nwItemClass identifier. - """ +def isItemClass(value: str) -> bool: + """Check if a string is a valid nwItemClass identifier.""" return value in nwItemClass.__members__ -def isItemType(value): - """Check if a string is a valid nwItemType identifier. - """ +def isItemType(value: str) -> bool: + """Check if a string is a valid nwItemType identifier.""" return value in nwItemType.__members__ -def isItemLayout(value): - """Check if a string is a valid nwItemLayout identifier. - """ +def isItemLayout(value: str) -> bool: + """Check if a string is a valid nwItemLayout identifier.""" return value in nwItemLayout.__members__ -def hexToInt(value, default=0): - """Convert a hex string to an integer. - """ +def hexToInt(value: Any, default: int = 0) -> int: + """Convert a hex string to an integer.""" if isinstance(value, str): try: return int(value, 16) @@ -193,13 +188,13 @@ def hexToInt(value, default=0): return default -def minmax(value, minVal, maxVal): +def minmax(value: int, minVal: int, maxVal: int) -> int: """Make sure an integer is between min and max value (inclusive). """ return min(maxVal, max(minVal, value)) -def checkIntTuple(value, valid, default): +def checkIntTuple(value: int, valid: tuple | list | set, default: int) -> int: """Check that an int is an element of a tuple. If it isn't, return the default value. """ @@ -213,7 +208,7 @@ def checkIntTuple(value, valid, default): # Formatting Functions # =============================================================================================== # -def formatInt(value): +def formatInt(value: int) -> str: """Formats an integer with k, M, G etc. """ if not isinstance(value, int): @@ -260,20 +255,19 @@ def formatTime(t: int) -> str: # String Functions # =============================================================================================== # -def simplified(string): - """Take a string an strip leading and trailing whitespaces, and +def simplified(text: str) -> str: + """Take a string and strip leading and trailing whitespaces, and replace all occurences of (multiple) whitespaces with a 0x20 space. """ - return " ".join(str(string).strip().split()) + return " ".join(str(text).strip().split()) -def yesNo(value): - """Convert a boolean evaluated variable to a yes or no. - """ +def yesNo(value: int | bool | None) -> Literal["yes", "no"]: + """Convert a boolean evaluated variable to a yes or no.""" return "yes" if value else "no" -def transferCase(source, target): +def transferCase(source: str, target: str) -> str: """Transfers the case of the source word to the target word. This will consider all upper or lower, and first char capitalisation. """ @@ -295,7 +289,7 @@ def transferCase(source, target): return theResult -def fuzzyTime(seconds): +def fuzzyTime(seconds: int) -> str: """Converts a time difference in seconds into a fuzzy time string. """ if seconds < 0: @@ -356,7 +350,7 @@ def fuzzyTime(seconds): ).format(int(round(seconds/31557600))) -def numberToRoman(value, toLower=False): +def numberToRoman(value: int, toLower: bool = False) -> str: """Convert an integer to a Roman number. """ if not isinstance(value, int): @@ -384,7 +378,7 @@ def numberToRoman(value, toLower=False): # Encoder Functions # =============================================================================================== # -def jsonEncode(data, n=0, nmax=0): +def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: """Encode a dictionary, list or tuple as a json object or array, and indent from level n up to a max level nmax if nmax is larger than 0. """ @@ -474,7 +468,7 @@ def xmlIndent(tree: ET.Element | ET.ElementTree): # File and File System Functions # =============================================================================================== # -def readTextFile(path): +def readTextFile(path: str | Path) -> str: """Read the content of a text file in a robust manner. """ path = Path(path) @@ -488,7 +482,7 @@ def readTextFile(path): return "" -def makeFileNameSafe(value): +def makeFileNameSafe(value: str) -> str: """Returns a filename safe string of the value. """ clean = "" @@ -498,7 +492,7 @@ def makeFileNameSafe(value): return clean -def sha256sum(path): +def sha256sum(path: str | Path) -> str | None: """Make a shasum of a file using a buffer. Based on: https://stackoverflow.com/a/44873382/5825851 """ @@ -521,7 +515,7 @@ def sha256sum(path): # Other Functions # =============================================================================================== # -def getGuiItem(objName): +def getGuiItem(objName: str) -> QWidget | None: """Returns a QtWidget based on its objectName. """ for qWidget in qApp.topLevelWidgets(): @@ -535,50 +529,49 @@ def getGuiItem(objName): # =============================================================================================== # class NWConfigParser(ConfigParser): + """Common: Adapted Config Parser + + This is a subclass of the standard config parser that adds type safe + helper functions, and support for lists. + """ def __init__(self): super().__init__() - def rdStr(self, section, option, default): - """Read string value. - """ + def rdStr(self, section: str, option: str, default: str) -> str: + """Read string value.""" return self.get(section, option, fallback=default) - def rdInt(self, section, option, default): - """Read integer value. - """ + def rdInt(self, section: str, option: str, default: int) -> int: + """Read integer value.""" try: return self.getint(section, option, fallback=default) except ValueError: logger.error("Could not read '%s':'%s' from config", section, option) return default - def rdFlt(self, section, option, default): - """Read float value. - """ + def rdFlt(self, section: str, option: str, default: float) -> float: + """Read float value.""" try: return self.getfloat(section, option, fallback=default) except ValueError: logger.error("Could not read '%s':'%s' from config", section, option) return default - def rdBool(self, section, option, default): - """Read boolean value. - """ + def rdBool(self, section: str, option: str, default: bool) -> bool: + """Read boolean value.""" try: return self.getboolean(section, option, fallback=default) except ValueError: logger.error("Could not read '%s':'%s' from config", section, option) return default - def rdPath(self, section, option, default): - """Read a path value. - """ + def rdPath(self, section: str, option: str, default: Path) -> Path: + """Read a Path value.""" return checkPath(self.get(section, option, fallback=default), default) - def rdStrList(self, section, option, default): - """Read string list. - """ + def rdStrList(self, section: str, option: str, default: list[str]) -> list[str]: + """Read string list.""" result = default.copy() if isinstance(default, list) else [] if self.has_option(section, option): data = self.get(section, option, fallback="").split(",") @@ -586,9 +579,8 @@ class NWConfigParser(ConfigParser): result[i] = data[i].strip() return result - def rdIntList(self, section, option, default): - """Read integer list. - """ + def rdIntList(self, section: str, option: str, default: list[int]) -> list[int]: + """Read integer list.""" result = default.copy() if isinstance(default, list) else [] if self.has_option(section, option): data = self.get(section, option, fallback="").split(",") diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 5a04f00b..34f1e1a3 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -150,17 +150,17 @@ class BuildSettings: @property def name(self) -> str: - """Return the build name.""" + """The build name.""" return self._name @property def buildID(self) -> str: - """Return the build ID.""" + """The build ID as an UUID.""" return self._uuid @property def changed(self) -> bool: - """Return the changed status of the build.""" + """The changed status of the build.""" return self._changed ## @@ -190,7 +190,7 @@ class BuildSettings: return 0 def getFloat(self, key: str) -> float: - """Type safe value access for float.""" + """Type safe value access for floats.""" value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1])) if isinstance(value, float): return value diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index bad425b1..cf52a3f2 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -1,7 +1,6 @@ """ novelWriter – Project Options Cache =================================== -Data class for user-defined GUI project options File History: Created: 2019-10-21 [0.3.1] @@ -23,17 +22,22 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import logging from enum import Enum +from typing import TYPE_CHECKING, Any from pathlib import Path from novelwriter.error import logException from novelwriter.common import checkBool, checkFloat, checkInt, checkString from novelwriter.constants import nwFiles +if TYPE_CHECKING: # pragma: no cover + from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) VALID_MAP = { @@ -63,9 +67,15 @@ VALID_MAP = { class OptionState: + """Core: GUI Options Storage - def __init__(self, theProject): - self.theProject = theProject + A class for storing the state of the GUI. The data is stored per + project. Settings that should be project-independent are stored in + the Config instead. + """ + + def __init__(self, project: NWProject): + self._project = project self._theState = {} return @@ -73,10 +83,10 @@ class OptionState: # Load and Save Cache ## - def loadSettings(self): + def loadSettings(self) -> bool: """Load the options dictionary from the project settings file. """ - stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE) + stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE) if not isinstance(stateFile, Path): return False @@ -101,10 +111,9 @@ class OptionState: return True - def saveSettings(self): - """Save the options dictionary to the project settings file. - """ - stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE) + def saveSettings(self) -> bool: + """Save the options dictionary to the project settings file.""" + stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE) if not isinstance(stateFile, Path): return False @@ -123,9 +132,8 @@ class OptionState: # Setters ## - def setValue(self, group, name, value): - """Save a value, with a given group and name. - """ + def setValue(self, group: str, name: str, value: Any) -> bool: + """Save a value, with a given group and name.""" if group not in VALID_MAP: logger.error("Unknown option group '%s'", group) return False @@ -148,7 +156,7 @@ class OptionState: # Getters ## - def getValue(self, group, name, default): + def getValue(self, group: str, name: str, default: Any) -> Any: """Return an arbitrary type value, if it exists. Otherwise, return the default value. """ @@ -156,7 +164,7 @@ class OptionState: return self._theState[group].get(name, default) return default - def getString(self, group, name, default): + def getString(self, group: str, name: str, default: str) -> str: """Return the value as a string, if it exists. Otherwise, return the default value. """ @@ -164,7 +172,7 @@ class OptionState: return checkString(self._theState[group].get(name, default), default) return default - def getInt(self, group, name, default): + def getInt(self, group: str, name: str, default: int) -> int: """Return the value as an int, if it exists. Otherwise, return the default value. """ @@ -172,7 +180,7 @@ class OptionState: return checkInt(self._theState[group].get(name, default), default) return default - def getFloat(self, group, name, default): + def getFloat(self, group: str, name: str, default: float) -> float: """Return the value as a float, if it exists. Otherwise, return the default value. """ @@ -180,7 +188,7 @@ class OptionState: return checkFloat(self._theState[group].get(name, default), default) return default - def getBool(self, group, name, default): + def getBool(self, group: str, name: str, default: bool) -> bool: """Return the value as a bool, if it exists. Otherwise, return the default value. """ @@ -188,9 +196,9 @@ class OptionState: return checkBool(self._theState[group].get(name, default), default) return default - def getEnum(self, group, name, lookup, default): + def getEnum(self, group: str, name: str, lookup: type, default: Enum) -> Enum: """Return the value mapped to an enum. Otherwise return the - default value + default value. """ if issubclass(lookup, Enum): if group in self._theState: diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index ea7415b6..2a405798 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -285,15 +285,20 @@ class NWProjectData: self.theProject.setProjectChanged(True) return - def setLastHandle(self, value: dict, component: str | None = None): - """Set a last used handle into the handle registry. If component - is None, the value is assumed to be the whole dictionary of - values. + def setLastHandle(self, value: str | None, component: str): + """Set a last used handle into the handle registry for a given + component. """ if isinstance(component, str): self._lastHandle[component] = checkStringNone(value, None) self.theProject.setProjectChanged(True) - elif isinstance(value, dict): + return + + def setLastHandles(self, value: dict): + """Set the full last handles dictionary to a new set of values. + This is intended to be used at project load. + """ + if isinstance(value, dict): for key, entry in value.items(): if key in self._lastHandle: self._lastHandle[key] = str(entry) if isHandle(entry) else None diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index acec2db4..628dd390 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -30,8 +30,8 @@ import xml.etree.ElementTree as ET from enum import Enum from time import time -from pathlib import Path from typing import TYPE_CHECKING +from pathlib import Path from novelwriter import __version__, __hexversion__ from novelwriter.common import ( @@ -284,7 +284,7 @@ class ProjectXMLReader: elif xItem.tag == "importance": self._parseStatusImport(xItem, data.itemImport) elif xItem.tag == "lastHandle": - data.setLastHandle(self._parseDictKeyText(xItem)) + data.setLastHandles(self._parseDictKeyText(xItem)) elif xItem.tag == "autoReplace": if self._version >= 0x0102: data.setAutoReplace(self._parseDictKeyText(xItem)) diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py index 93794b4c..8574b3df 100644 --- a/novelwriter/core/storage.py +++ b/novelwriter/core/storage.py @@ -170,7 +170,7 @@ class NWStorage: xmlWriter = ProjectXMLWriter(self._runtimePath) return xmlWriter - def getDocument(self, tHandle: str) -> NWDocument: + def getDocument(self, tHandle: str | None) -> NWDocument: """Return a document wrapper object.""" if self._runtimePath is not None: return NWDocument(self._project, tHandle) diff --git a/setup.cfg b/setup.cfg index 53e27d09..b9572076 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,6 @@ gui_scripts = universal = 0 [flake8] -ignore = D107,D205,D400,E133,E221,E226,E228,E241,W503 +ignore = E133,E221,E226,E228,E241,W503 max-line-length = 99 exclude = docs/*