Update the project storage class
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user