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"
OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
BUILD_CACHE = "prevBuild.json"
BUILDS_FILE = "builds.json"
# END Class nwFiles
+1 -1
View File
@@ -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
+53 -64
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
+1 -1
View File
@@ -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__)
+1 -1
View File
@@ -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
+10 -13
View File
@@ -22,8 +22,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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)