Update the project storage class

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