Merge branch 'main' into parse_story_structure

This commit is contained in:
Veronica Berglyd Olsen
2025-04-08 12:27:41 +02:00
87 changed files with 556 additions and 452 deletions
+12 -9
View File
@@ -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:
+8 -4
View File
@@ -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)
+11 -7
View File
@@ -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:
+2 -2
View File
@@ -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__)
+5 -4
View File
@@ -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
@@ -900,7 +901,7 @@ class IndexCache:
which provides lookup capabilities and caching for shared data.
"""
__slots__ = ("tags", "story")
__slots__ = ("story", "tags")
def __init__(self, tagsIndex: TagsIndex) -> None:
self.tags: TagsIndex = tagsIndex
@@ -921,7 +922,7 @@ class ItemIndex:
IndexHeading object for each heading of the text.
"""
__slots__ = ("_project", "_cache", "_items")
__slots__ = ("_cache", "_items", "_project")
def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None:
self._project = project
+7 -6
View File
@@ -28,17 +28,18 @@ 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, compact, isListInstance, isTitleTag
from novelwriter.constants import nwKeyWords, nwStyles
from novelwriter.enum import nwComment
if TYPE_CHECKING: # pragma: no cover
if TYPE_CHECKING:
from collections.abc import ItemsView, Sequence
from novelwriter.core.index import IndexCache
from novelwriter.core.item import NWItem
from novelwriter.enum import nwComment
logger = logging.getLogger(__name__)
@@ -58,7 +59,7 @@ class IndexNode:
must be reset each time the item is re-indexed.
"""
__slots__ = ("_cache", "_handle", "_item", "_headings", "_notes", "_count")
__slots__ = ("_cache", "_count", "_handle", "_headings", "_item", "_notes")
def __init__(self, cache: IndexCache, tHandle: str, nwItem: NWItem) -> None:
self._cache = cache
@@ -206,8 +207,8 @@ class IndexHeading:
"""
__slots__ = (
"_cache", "_key", "_line", "_level", "_title",
"_counts", "_tag", "_refs", "_comments",
"_cache", "_comments", "_counts", "_key", "_level", "_line", "_refs",
"_tag", "_title",
)
def __init__(
+26 -26
View File
@@ -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
+17 -18
View File
@@ -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."""
+6 -2
View File
@@ -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__()
+1 -1
View File
@@ -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__)
+9 -6
View File
@@ -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:
+1 -1
View File
@@ -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__)
+1 -1
View File
@@ -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
+3 -2
View File
@@ -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__)
+3 -2
View File
@@ -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__)
+9 -7
View File
@@ -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] = {}
+1 -1
View File
@@ -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__)
+11 -6
View File
@@ -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]: