Fix some typing issues

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