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)