From 135ba3f290962512eb5c473c9f775c864b86d1c5 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 3 Jun 2023 19:05:18 +0200
Subject: [PATCH] Update the project storage class
---
novelwriter/constants.py | 1 -
novelwriter/core/projectxml.py | 2 +-
novelwriter/core/storage.py | 117 ++++++++++++---------------
novelwriter/tools/manuscript.py | 2 +-
novelwriter/tools/manussettings.py | 2 +-
tests/test_core/test_core_storage.py | 23 +++---
6 files changed, 66 insertions(+), 81 deletions(-)
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index 46e1c1d6..ddcc1004 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -84,7 +84,6 @@ class nwFiles:
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
- BUILD_CACHE = "prevBuild.json"
BUILDS_FILE = "builds.json"
# END Class nwFiles
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 416f0934..acec2db4 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -40,7 +40,7 @@ from novelwriter.common import (
)
from novelwriter.constants import nwFiles
-if TYPE_CHECKING:
+if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.status import NWStatus
from novelwriter.core.projectdata import NWProjectData
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index a203cf9b..93794b4c 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -1,7 +1,6 @@
"""
novelWriter – Project Storage Class
===================================
-The main class handling the project storage
File History:
Created: 2022-11-01 [2.0rc2] NWStorage
@@ -22,10 +21,12 @@ 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 logging
from time import time
+from typing import TYPE_CHECKING
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
@@ -36,29 +37,32 @@ from novelwriter.constants import nwFiles
from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
+if TYPE_CHECKING: # pragma: no cover
+ from novelwriter.core.project import NWProject
+
logger = logging.getLogger(__name__)
class NWStorage:
+ """Core: Project Storage Class
+
+ The class that handles all paths related to the project storage.
+ """
MODE_INACTIVE = 0
MODE_INPLACE = 1
MODE_ARCHIVE = 2
- def __init__(self, theProject):
-
- self.theProject = theProject
-
+ def __init__(self, project: NWProject):
+ self._project = project
self._storagePath = None
self._runtimePath = None
self._lockFilePath = None
self._openMode = self.MODE_INACTIVE
-
return
def clear(self):
- """Reset internal variables.
- """
+ """Reset internal variables."""
self._storagePath = None
self._runtimePath = None
self._openMode = self.MODE_INACTIVE
@@ -69,15 +73,17 @@ class NWStorage:
##
@property
- def storagePath(self):
+ def storagePath(self) -> Path | None:
+ """Get the path where the project is saved."""
return self._storagePath
@property
- def runtimePath(self):
+ def runtimePath(self) -> Path | None:
+ """Get the path where the project is saved at runtime."""
return self._runtimePath
@property
- def contentPath(self):
+ def contentPath(self) -> Path | None:
"""Return the path used for project content. The folder must
already exist, otherwise this property is None.
"""
@@ -94,12 +100,11 @@ class NWStorage:
# Core Methods
##
- def isOpen(self):
- """Check if the storage location is open.
- """
+ def isOpen(self) -> bool:
+ """Check if the storage location is open."""
return self._runtimePath is not None
- def openProjectInPlace(self, path, newProject=False):
+ def openProjectInPlace(self, path: str | Path, newProject: bool = False) -> bool:
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
@@ -124,13 +129,15 @@ class NWStorage:
return True
- def openProjectArchive(self, path): # pragma: no cover
- """Placeholder for later implementation. See #977.
+ def openProjectArchive(self, path: str | Path) -> bool: # pragma: no cover
+ """Open the project from a single file.
+ Placeholder for later implementation. See #977.
"""
return False
- def runPostSaveTasks(self, autoSave=False): # pragma: no cover
+ def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
"""Run tasks after the project has been saved.
+ Placeholder for later implementation. See #977.
"""
if self._openMode == self.MODE_INPLACE:
# Nothing to do, so we just return
@@ -139,8 +146,7 @@ class NWStorage:
return True
def closeSession(self):
- """Run tasks related to closing the session.
- """
+ """Run tasks related to closing the session."""
# Clear lockfile
self.clear()
return
@@ -149,51 +155,35 @@ class NWStorage:
# Content Access Methods
##
- def getXmlReader(self):
- """Return a properly configured ProjectXMLReader instance.
- """
+ def getXmlReader(self) -> ProjectXMLReader | None:
+ """Return a properly configured ProjectXMLReader instance."""
if self._runtimePath is None:
return None
-
projFile = self._runtimePath / nwFiles.PROJ_FILE
xmlReader = ProjectXMLReader(projFile)
-
return xmlReader
- def getXmlWriter(self):
- """Return a properly configured ProjectXMLWriter instance.
- """
+ def getXmlWriter(self) -> ProjectXMLWriter | None:
+ """Return a properly configured ProjectXMLWriter instance."""
if self._runtimePath is None:
return None
-
xmlWriter = ProjectXMLWriter(self._runtimePath)
-
return xmlWriter
- def getDocument(self, tHandle):
- """Return a document wrapper object.
- """
+ def getDocument(self, tHandle: str) -> NWDocument:
+ """Return a document wrapper object."""
if self._runtimePath is not None:
- return NWDocument(self.theProject, tHandle)
- return NWDocument(self.theProject, None)
+ return NWDocument(self._project, tHandle)
+ return NWDocument(self._project, None)
- def getMetaFile(self, fileName):
- """Return the path to a file in the project meta folder.
- """
+ def getMetaFile(self, fileName: str) -> Path | None:
+ """Return the path to a file in the project meta folder."""
if self._runtimePath is not None:
return self._runtimePath / "meta" / fileName
return None
- def getCacheFile(self, fileName):
- """Return the path to a file in the project cache folder.
- """
- if self._runtimePath is not None:
- return self._runtimePath / "cache" / fileName
- return None
-
- def readLockFile(self):
- """Read the project lock file.
- """
+ def readLockFile(self) -> list:
+ """Read the project lock file."""
if self._lockFilePath is None:
return ["ERROR"]
@@ -212,9 +202,8 @@ class NWStorage:
return lines
- def writeLockFile(self):
- """Write the project lock file.
- """
+ def writeLockFile(self) -> bool:
+ """Write the project lock file."""
if self._lockFilePath is None:
return False
@@ -231,9 +220,8 @@ class NWStorage:
return True
- def clearLockFile(self):
- """Remove the lock file, if it exists.
- """
+ def clearLockFile(self) -> bool:
+ """Remove the lock file, if it exists."""
if self._lockFilePath is None:
return False
@@ -247,7 +235,7 @@ class NWStorage:
return True
- def zipIt(self, target, compression=None):
+ def zipIt(self, target: str | Path, compression: int | None = None) -> bool:
"""Zip the content of the project at its runtime location into a
zip file. This process will only grab files that are supposed to
be in the project. All non-project files will be left out.
@@ -291,9 +279,8 @@ class NWStorage:
# Internal Functions
##
- def _prepareStorage(self, checkLegacy=True, newProject=False):
- """Prepare the storage area for the project.
- """
+ def _prepareStorage(self, checkLegacy: bool = True, newProject: bool = False) -> bool:
+ """Prepare the storage area for the project."""
path = self._runtimePath
if not isinstance(path, Path):
logger.error("No path set")
@@ -318,7 +305,6 @@ class NWStorage:
try:
path.mkdir(exist_ok=True)
(path / "content").mkdir(exist_ok=True)
- (path / "cache").mkdir(exist_ok=True)
(path / "meta").mkdir(exist_ok=True)
except Exception as exc:
logger.error("Failed to create required project folders", exc_info=exc)
@@ -385,8 +371,7 @@ class NWStorage:
return
def _deleteDeprecatedFiles(self, path: Path):
- """Delete files that are no longer used by novelWriter.
- """
+ """Delete files that are no longer used by novelWriter."""
remove = [
path / "meta" / "mainOptions.json", # Replaced in 0.5
path / "meta" / "exportOptions.json", # Replaced in 0.5
@@ -394,16 +379,20 @@ class NWStorage:
path / "meta" / "timelineOptions.json", # Replaced in 0.5
path / "meta" / "docMergeOptions.json", # Replaced in 0.5
path / "meta" / "sessionLogOptions.json", # Replaced in 0.5
+ path / "cache" / "prevBuild.json", # Dropped in 2.1 Beta 1
+ path / "cache", # Dropped in 2.1 Beta 1
path / "ToC.json", # Dropped in 1.0 RC 1
]
for item in remove:
- if item.is_file():
+ if item.exists():
try:
- item.unlink()
+ if item.is_dir():
+ item.rmdir()
+ else:
+ item.unlink()
logger.info("Deleted: %s", item)
except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc)
-
return
# END Class NWStorage
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 2260650a..3a7796cb 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -46,7 +46,7 @@ from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.tools.manussettings import GuiBuildSettings
-if TYPE_CHECKING:
+if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__)
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index f97df0c5..43766068 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -47,7 +47,7 @@ from novelwriter.extensions.switchbox import NSwitchBox
from novelwriter.extensions.configlayout import NConfigLayout, NSimpleLayout
from novelwriter.extensions.pagedsidebar import NPagedSideBar
-if TYPE_CHECKING:
+if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
from novelwriter.gui.theme import GuiTheme
diff --git a/tests/test_core/test_core_storage.py b/tests/test_core/test_core_storage.py
index 5baa8cf0..c0410964 100644
--- a/tests/test_core/test_core_storage.py
+++ b/tests/test_core/test_core_storage.py
@@ -22,8 +22,8 @@ along with this program. If not, see .
from zipfile import ZipFile
import pytest
-from mocked import causeOSError
from tools import C, buildTestProject, writeFile
+from mocked import causeOSError
from novelwriter import CONFIG
from novelwriter.constants import nwFiles
@@ -33,13 +33,14 @@ from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
class MockProject:
+ """Test class for projects."""
+
pass
@pytest.mark.core
def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
- """Test opening a project in a folder.
- """
+ """Test opening a project in a folder."""
theProject = NWProject(mockGUI)
mockRnd.reset()
buildTestProject(theProject, fncPath)
@@ -60,7 +61,6 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
assert storage.getXmlWriter() is None
assert bool(storage.getDocument(C.hSceneDoc)) is False
assert storage.getMetaFile("file") is None
- assert storage.getCacheFile("file") is None
# Open project as a new project should fail
assert storage.openProjectInPlace(fncPath, newProject=True) is False
@@ -93,7 +93,6 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
# Get paths
assert storage.getMetaFile("stuff") == fncPath / "meta" / "stuff"
- assert storage.getCacheFile("stuff") == fncPath / "cache" / "stuff"
# Clean up
assert theProject.closeProject() is True
@@ -104,15 +103,13 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
assert storage.getXmlWriter() is None
assert bool(storage.getDocument(C.hSceneDoc)) is False
assert storage.getMetaFile("file") is None
- assert storage.getCacheFile("file") is None
# END Test testCoreStorage_ProjectInPlace
@pytest.mark.core
def testCoreStorage_LockFile(monkeypatch, fncPath):
- """Test the project lock file.
- """
+ """Test the project lock file."""
monkeypatch.setattr("novelwriter.core.storage.time", lambda: 1000.0)
storage = NWStorage(MockProject())
@@ -174,8 +171,7 @@ def testCoreStorage_LockFile(monkeypatch, fncPath):
@pytest.mark.core
def testCoreStorage_PrepareStorage(monkeypatch, fncPath):
- """Test the project path preparation functions.
- """
+ """Test the project path preparation functions."""
storage = NWStorage(MockProject())
assert storage.isOpen() is False
@@ -198,8 +194,8 @@ def testCoreStorage_PrepareStorage(monkeypatch, fncPath):
storage._runtimePath = fncPath
assert storage._prepareStorage(checkLegacy=False) is True
assert (fncPath / "content").exists()
- assert (fncPath / "cache").exists()
assert (fncPath / "meta").exists()
+ assert not (fncPath / "cache").exists() # Removed in 2.1b1
# Add a legacy folder
storage._runtimePath = fncPath
@@ -280,8 +276,10 @@ def testCoreStorage_PrepareStorage(monkeypatch, fncPath):
fncPath / "meta" / "timelineOptions.json",
fncPath / "meta" / "docMergeOptions.json",
fncPath / "meta" / "sessionLogOptions.json",
+ fncPath / "cache" / "prevBuild.json",
fncPath / "ToC.json",
]
+ (fncPath / "cache").mkdir()
for depFile in remove:
depFile.write_text("foo")
assert depFile.exists()
@@ -301,8 +299,7 @@ def testCoreStorage_PrepareStorage(monkeypatch, fncPath):
@pytest.mark.core
def testCoreStorage_ZipIt(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd):
- """Test making a zip archive of a project.
- """
+ """Test making a zip archive of a project."""
zipFile = tstPaths.tmpDir / "project.zip"
theProject = NWProject(mockGUI)