Remove project meta attribute from project class

This commit is contained in:
Veronica Berglyd Olsen
2022-11-05 23:29:25 +01:00
parent 82b34d9d2c
commit 21ab4e58f9
11 changed files with 117 additions and 159 deletions
+2 -3
View File
@@ -305,11 +305,10 @@ class ProjectBuilder:
return False return False
project = NWProject(self.mainGui) project = NWProject(self.mainGui)
if not project.setProjectPath(projPath, newProject=True): if not project.storage.openProjectInPlace(projPath, newProject=True):
return False return False
if not project.storage.openProjectInPlace(projPath): project.projPath = projPath
return False
lblNewProject = self.tr("New Project") lblNewProject = self.tr("New Project")
lblNewChapter = self.tr("New Chapter") lblNewChapter = self.tr("New Chapter")
+10 -4
View File
@@ -26,11 +26,11 @@ 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/>.
""" """
import os
import json import json
import logging import logging
from time import time from time import time
from pathlib import Path
from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
@@ -141,12 +141,15 @@ class NWIndex:
def loadIndex(self): def loadIndex(self):
"""Load index from last session from the project meta folder. """Load index from last session from the project meta folder.
""" """
indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path):
return False
theData = {} theData = {}
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time() tStart = time()
self._indexBroken = False self._indexBroken = False
if os.path.isfile(indexFile): if indexFile.exists():
logger.debug("Loading index file") logger.debug("Loading index file")
try: try:
with open(indexFile, mode="r", encoding="utf-8") as inFile: with open(indexFile, mode="r", encoding="utf-8") as inFile:
@@ -184,8 +187,11 @@ class NWIndex:
"""Save the current index as a json file in the project meta """Save the current index as a json file in the project meta
data folder. data folder.
""" """
indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path):
return False
logger.debug("Saving index file") logger.debug("Saving index file")
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time() tStart = time()
try: try:
+6 -8
View File
@@ -24,11 +24,11 @@ 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/>.
""" """
import os
import json import json
import logging import logging
from enum import Enum from enum import Enum
from pathlib import Path
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkBool, checkFloat, checkInt, checkString from novelwriter.common import checkBool, checkFloat, checkInt, checkString
@@ -77,13 +77,12 @@ class OptionState:
def loadSettings(self): def loadSettings(self):
"""Load the options dictionary from the project settings file. """Load the options dictionary from the project settings file.
""" """
if self.theProject.projMeta is None: stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
if not isinstance(stateFile, Path):
return False return False
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {} theState = {}
if stateFile.exists():
if os.path.isfile(stateFile):
logger.debug("Loading GUI options file") logger.debug("Loading GUI options file")
try: try:
with open(stateFile, mode="r", encoding="utf-8") as inFile: with open(stateFile, mode="r", encoding="utf-8") as inFile:
@@ -106,12 +105,11 @@ class OptionState:
def saveSettings(self): def saveSettings(self):
"""Save the options dictionary to the project settings file. """Save the options dictionary to the project settings file.
""" """
if self.theProject.projMeta is None: stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
if not isinstance(stateFile, Path):
return False return False
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file") logger.debug("Saving GUI options file")
try: try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile: with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self._theState, outFile, indent=2) json.dump(self._theState, outFile, indent=2)
+5 -48
View File
@@ -27,6 +27,7 @@ from __future__ import annotations
import os import os
import json import json
from pathlib import Path
import shutil import shutil
import logging import logging
import novelwriter import novelwriter
@@ -84,7 +85,6 @@ class NWProject(QObject):
# Class Settings # Class Settings
self.projPath = None # The full path to where the currently open project is saved self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder
self.projCache = None # The full path to the project's cache folder self.projCache = None # The full path to the project's cache folder
self.projContent = None # The full path to the project's content folder self.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary self.projDict = None # The spell check dictionary
@@ -254,7 +254,6 @@ class NWProject(QObject):
# Project Settings # Project Settings
self.projPath = None self.projPath = None
self.projMeta = None
self.projCache = None self.projCache = None
self.projContent = None self.projContent = None
self.projDict = None self.projDict = None
@@ -276,11 +275,10 @@ class NWProject(QObject):
self.projPath = str(self._storage.runtimePath) self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath) self.projContent = str(self._storage.contentPath)
self.projCache = str(self._storage.cachePath) self.projCache = str(self._storage.cachePath)
self.projMeta = str(self._storage.metaPath)
logger.info("Opening project: %s", self.projPath) logger.info("Opening project: %s", self.projPath)
self.projDict = os.path.join(self.projMeta, nwFiles.PROJ_DICT) self.projDict = str(self._storage.getMetaFile(nwFiles.PROJ_DICT))
# Project Lock # Project Lock
# ============ # ============
@@ -421,8 +419,6 @@ class NWProject(QObject):
return False return False
saveTime = time() saveTime = time()
if not self.ensureFolderStructure():
return False
logger.info("Saving project: %s", self.projPath) logger.info("Saving project: %s", self.projPath)
@@ -482,7 +478,6 @@ class NWProject(QObject):
if self.projPath is None or self.projPath == "": if self.projPath is None or self.projPath == "":
return False return False
self.projMeta = os.path.join(self.projPath, "meta")
self.projCache = os.path.join(self.projPath, "cache") self.projCache = os.path.join(self.projPath, "cache")
self.projContent = os.path.join(self.projPath, "content") self.projContent = os.path.join(self.projPath, "content")
@@ -490,8 +485,6 @@ class NWProject(QObject):
# Don't make a mess in the user's home folder # Don't make a mess in the user's home folder
return False return False
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projCache): if not self._checkFolder(self.projCache):
return False return False
if not self._checkFolder(self.projContent): if not self._checkFolder(self.projContent):
@@ -589,41 +582,6 @@ class NWProject(QObject):
# Setters # Setters
## ##
def setProjectPath(self, projPath, newProject=False):
"""Set the project storage path, and also expand ~ to the user
directory using the path library.
"""
if projPath is None or projPath == "":
self.projPath = None
else:
if projPath.startswith("~"):
projPath = os.path.expanduser(projPath)
self.projPath = os.path.abspath(projPath)
if newProject:
if not os.path.isdir(projPath):
try:
os.mkdir(projPath)
logger.debug("Created folder: %s", projPath)
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Could not create new project folder."
), nwAlert.ERROR, exception=exc)
return False
if os.path.isdir(projPath):
if os.listdir(self.projPath):
self.mainGui.makeAlert(self.tr(
"New project folder is not empty. "
"Each project requires a dedicated project folder."
), nwAlert.ERROR)
return False
self.ensureFolderStructure()
self.setProjectChanged(True)
return True
def setProjectLang(self, theLang): def setProjectLang(self, theLang):
"""Set the project-specific language. """Set the project-specific language.
""" """
@@ -934,12 +892,10 @@ class NWProject(QObject):
def _appendSessionStats(self, idleTime): def _appendSessionStats(self, idleTime):
"""Append session statistics to the sessions log file. """Append session statistics to the sessions log file.
""" """
if not self.ensureFolderStructure(): sessionFile = self._storage.getMetaFile(nwFiles.SESS_STATS)
if not isinstance(sessionFile, Path):
return False return False
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = os.path.isfile(sessionFile)
nowTime = time() nowTime = time()
iNovel, iNotes = self._data.initCounts iNovel, iNotes = self._data.initCounts
cNovel, cNotes = self._data.currCounts cNovel, cNotes = self._data.currCounts
@@ -953,6 +909,7 @@ class NWProject(QObject):
return False return False
try: try:
isFile = sessionFile.exists() # We must save the state before we open
with open(sessionFile, mode="a+", encoding="utf-8") as outFile: with open(sessionFile, mode="a+", encoding="utf-8") as outFile:
if not isFile: if not isFile:
# It's a new file, so add a header # It's a new file, so add a header
+20 -7
View File
@@ -97,7 +97,7 @@ class NWStorage:
""" """
return self._runtimePath is not None return self._runtimePath is not None
def openProjectInPlace(self, path): def openProjectInPlace(self, path, newProject=False):
"""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.
""" """
@@ -112,7 +112,7 @@ class NWStorage:
self._lockFilePath = inPath / nwFiles.PROJ_LOCK self._lockFilePath = inPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE self._openMode = self.MODE_INPLACE
if self._prepareStorage(checkLegacy=True) is False: if not self._prepareStorage(checkLegacy=True, newProject=newProject):
self.clear() self.clear()
return False return False
@@ -142,7 +142,7 @@ class NWStorage:
## ##
def getXmlReader(self): def getXmlReader(self):
""" """Return a properly configured ProjectXMLReader instance.
""" """
if self._runtimePath is None: if self._runtimePath is None:
return None return None
@@ -153,7 +153,7 @@ class NWStorage:
return xmlReader return xmlReader
def getXmlWriter(self): def getXmlWriter(self):
""" """Return a properly configured ProjectXMLWriter instance.
""" """
if self._runtimePath is None: if self._runtimePath is None:
return None return None
@@ -165,8 +165,12 @@ class NWStorage:
def getDocument(self, tHandle): def getDocument(self, tHandle):
pass pass
def getMetaFile(self, kind): def getMetaFile(self, fileName):
pass """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 readLockFile(self): def readLockFile(self):
"""Read the project lock file. """Read the project lock file.
@@ -234,7 +238,7 @@ class NWStorage:
def _writeLockFile(self): def _writeLockFile(self):
pass pass
def _prepareStorage(self, checkLegacy=True): def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project. """Prepare the storage area for the project.
""" """
path = self._runtimePath path = self._runtimePath
@@ -248,6 +252,15 @@ class NWStorage:
self.clear() self.clear()
return False return False
if newProject:
# If it's a new project, we check that there is no existing
# project in the selected path.
projFile = path / nwFiles.PROJ_FILE
if projFile.exists():
logger.error("A project already exists in this path")
self.clear()
return False
# The folder is not required to exist, as it could be a new # The folder is not required to exist, as it could be a new
# project, so we make sure it does. Then we add subfolders. # project, so we make sure it does. Then we add subfolders.
try: try:
+15 -9
View File
@@ -23,10 +23,11 @@ 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/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget, QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
@@ -150,9 +151,11 @@ class GuiWordList(QDialog):
""" """
self._saveGuiSettings() self._saveGuiSettings()
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
tmpFile = dctFile + "~" if not isinstance(dctFile, Path):
return False
tmpFile = dctFile.with_suffix(".tmp")
try: try:
with open(tmpFile, mode="w", encoding="utf-8") as outFile: with open(tmpFile, mode="w", encoding="utf-8") as outFile:
for i in range(self.listBox.count()): for i in range(self.listBox.count()):
@@ -160,15 +163,16 @@ class GuiWordList(QDialog):
if item is not None: if item is not None:
outFile.write(item.text() + "\n") outFile.write(item.text() + "\n")
if dctFile.exists():
dctFile.unlink()
tmpFile.rename(dctFile)
except Exception: except Exception:
logger.error("Could not save new word list") logger.error("Could not save new word list")
logException() logException()
self.reject() self.reject()
return False return False
if os.path.isfile(dctFile):
os.unlink(dctFile)
os.rename(tmpFile, dctFile)
self.accept() self.accept()
return True return True
@@ -187,10 +191,12 @@ class GuiWordList(QDialog):
def _loadWordList(self): def _loadWordList(self):
"""Load the project's word list, if it exists. """Load the project's word list, if it exists.
""" """
self.listBox.clear() wordList = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
if not isinstance(wordList, Path):
return False
wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) self.listBox.clear()
if not os.path.isfile(wordList): if not wordList.exists():
logger.debug("No project dictionary file found") logger.debug("No project dictionary file found")
return False return False
+3 -2
View File
@@ -28,6 +28,7 @@ import json
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtGui import QPixmap, QCursor from PyQt5.QtGui import QPixmap, QCursor
@@ -439,8 +440,8 @@ class GuiWritingStats(QDialog):
ttTime = 0 ttTime = 0
ttIdle = 0 ttIdle = 0
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS) logFile = self.theProject.storage.getMetaFile(nwFiles.SESS_STATS)
if not os.path.isfile(logFile): if not isinstance(logFile, Path) or not logFile.exists():
logger.info("This project has no writing stats logfile") logger.info("This project has no writing stats logfile")
return False return False
+30 -16
View File
@@ -19,28 +19,30 @@ 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/>.
""" """
import os
import json import json
import pytest import pytest
from mock import causeOSError from mock import causeOSError
from tools import writeFile
from novelwriter.constants import nwFiles
from novelwriter.core.options import OptionState from novelwriter.core.options import OptionState
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.constants import nwFiles from novelwriter.gui.noveltree import NovelTreeColumn
@pytest.mark.core @pytest.mark.core
def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir): def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
"""Test loading and saving from the OptionState class. """Test loading and saving from the OptionState class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
theOpts = OptionState(theProject) theOpts = OptionState(theProject)
metaDir = fncPath / "meta"
metaDir.mkdir()
# Write a test file # Write a test file
optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE) optFile = metaDir / nwFiles.OPTS_FILE
writeFile(optFile, json.dumps({ optFile.write_text(json.dumps({
"GuiBuildNovel": { "GuiBuildNovel": {
"winWidth": 1000, "winWidth": 1000,
"winHeight": 700, "winHeight": 700,
@@ -52,22 +54,22 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
"MockGroup": { "MockGroup": {
"mockItem": None, "mockItem": None,
}, },
})) }), encoding="utf-8")
# Load and save with no path set # Load and save with no path set
theProject.projMeta = None theProject.storage._runtimePath = None
assert not theOpts.loadSettings() assert theOpts.loadSettings() is False
assert not theOpts.saveSettings() assert theOpts.saveSettings() is False
# Set path # Set path
theProject.projMeta = tmpDir theProject.storage._runtimePath = fncPath
assert theProject.projMeta == tmpDir assert theProject.storage.getMetaFile(nwFiles.OPTS_FILE) == optFile
# Cause open() to fail # Cause open() to fail
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert not theOpts.loadSettings() assert theOpts.loadSettings() is False
assert not theOpts.saveSettings() assert theOpts.saveSettings() is False
# Load proper # Load proper
assert theOpts.loadSettings() assert theOpts.loadSettings()
@@ -108,9 +110,11 @@ def testCoreOptions_SetGet(mockGUI):
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
theOpts = OptionState(theProject) theOpts = OptionState(theProject)
nwColHidden = NovelTreeColumn.HIDDEN
# Set invalid values # Set invalid values
assert not theOpts.setValue("MockGroup", "mockItem", None) assert theOpts.setValue("MockGroup", "mockItem", None) is False
assert not theOpts.setValue("GuiBuildNovel", "mockItem", None) assert theOpts.setValue("GuiBuildNovel", "mockItem", None) is False
# Set valid value # Set valid value
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100) assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
@@ -120,6 +124,7 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34) assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34)
assert theOpts.setValue("GuiBuildNovel", "addNovel", True) assert theOpts.setValue("GuiBuildNovel", "addNovel", True)
assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell") assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell")
assert theOpts.setValue("GuiNovelView", "lastCol", nwColHidden)
# Generic get, doesn't check type # Generic get, doesn't check type
assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100 assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100
@@ -139,5 +144,14 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.getFloat("GuiBuildNovel", "mockItem", None) is None assert theOpts.getFloat("GuiBuildNovel", "mockItem", None) is None
assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True
assert theOpts.getBool("GuiBuildNovel", "mockItem", None) is None assert theOpts.getBool("GuiBuildNovel", "mockItem", None) is None
assert theOpts.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, None) == nwColHidden
# Get from non-existent groups
assert theOpts.getValue("SomeGroup", "mockItem", None) is None
assert theOpts.getString("SomeGroup", "mockItem", None) is None
assert theOpts.getInt("SomeGroup", "mockItem", None) is None
assert theOpts.getFloat("SomeGroup", "mockItem", None) is None
assert theOpts.getBool("SomeGroup", "mockItem", None) is None
assert theOpts.getEnum("SomeGroup", "mockItem", NovelTreeColumn, None) is None
# END Test testCoreOptions_SetGet # END Test testCoreOptions_SetGet
+24 -56
View File
@@ -23,11 +23,13 @@ import os
import shutil import shutil
import pytest import pytest
from time import time
from shutil import copyfile from shutil import copyfile
from pathlib import Path
from zipfile import ZipFile from zipfile import ZipFile
from mock import causeOSError from mock import causeOSError
from tools import C, cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE from tools import C, cmpFiles, writeFile, buildTestProject, XML_IGNORE
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.common import formatTimeStamp from novelwriter.common import formatTimeStamp
@@ -52,11 +54,6 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncDir)
assert theProject.setProjectPath(fncDir) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000010" assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000010"
assert theProject.newRoot(nwItemClass.PLOT) == "0000000000011" assert theProject.newRoot(nwItemClass.PLOT) == "0000000000011"
assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000012" assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000012"
@@ -108,11 +105,6 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncDir)
assert theProject.setProjectPath(fncDir) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
# Invalid call # Invalid call
assert theProject.newFolder("New Folder", "1234567890abc") is None assert theProject.newFolder("New Folder", "1234567890abc") is None
assert theProject.newFile("New File", "1234567890abc") is None assert theProject.newFile("New File", "1234567890abc") is None
@@ -195,7 +187,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert theProject.openProject(fncDir) is False assert theProject.openProject(fncDir) is False
# Fail on lock file # Fail on lock file
theProject.setProjectPath(fncDir)
assert theProject._storage.writeLockFile() assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir) is False assert theProject.openProject(fncDir) is False
@@ -208,7 +199,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert theProject.closeProject() assert theProject.closeProject()
# Force open with lockfile # Force open with lockfile
theProject.setProjectPath(fncDir)
assert theProject._storage.writeLockFile() assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir, overrideLock=True) is True assert theProject.openProject(fncDir, overrideLock=True) is True
assert theProject.closeProject() assert theProject.closeProject()
@@ -267,12 +257,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncDir)
# Fail on folder structure check
with monkeypatch.context() as mp:
mp.setattr("os.mkdir", causeOSError)
shutil.rmtree(os.path.join(fncDir, "meta"))
assert theProject.saveProject() is False
# Fail writing # Fail writing
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLWriter, "write", lambda *a: False) mp.setattr(ProjectXMLWriter, "write", lambda *a: False)
@@ -303,12 +287,6 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
mp.setattr("os.path.expanduser", lambda *a, **k: fncDir) mp.setattr("os.path.expanduser", lambda *a, **k: fncDir)
assert theProject.ensureFolderStructure() is False assert theProject.ensureFolderStructure() is False
# Create a file to block meta folder
metaDir = os.path.join(fncDir, "meta")
writeFile(metaDir, "stuff")
assert theProject.ensureFolderStructure() is False
os.unlink(metaDir)
# Create a file to block cache folder # Create a file to block cache folder
cacheDir = os.path.join(fncDir, "cache") cacheDir = os.path.join(fncDir, "cache")
writeFile(cacheDir, "stuff") writeFile(cacheDir, "stuff")
@@ -323,7 +301,7 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
# Now, do it right # Now, do it right
assert theProject.ensureFolderStructure() is True assert theProject.ensureFolderStructure() is True
assert os.path.isdir(metaDir) # assert os.path.isdir(metaDir)
assert os.path.isdir(cacheDir) assert os.path.isdir(cacheDir)
assert os.path.isdir(contentDir) assert os.path.isdir(contentDir)
@@ -506,32 +484,12 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
"""Test other project class methods and functions. """Test other project class methods and functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncDir)
# Setting project path
assert theProject.setProjectPath(None)
assert theProject.projPath is None
assert theProject.setProjectPath("")
assert theProject.projPath is None
assert theProject.setProjectPath("~")
assert theProject.projPath == os.path.expanduser("~")
# Create a new folder and populate it
projPath = os.path.join(fncDir, "mock1")
assert theProject.setProjectPath(projPath, newProject=True)
# Make os.mkdir fail
monkeypatch.setattr("os.mkdir", causeOSError)
projPath = os.path.join(fncDir, "mock2")
assert not theProject.setProjectPath(projPath, newProject=True)
# Set back
assert theProject.setProjectPath(fncDir)
# Project Name # Project Name
theProject.data.setName(" A Name ") theProject.data.setName(" A Name ")
assert theProject.data.name == "A Name" assert theProject.data.name == "A Name"
@@ -639,29 +597,39 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
assert theProject.tree.handles() == oldOrder assert theProject.tree.handles() == oldOrder
# Session stats # Session stats
theProject._data._initCounts = [50, 50] theProject.data.setInitCounts(50, 50)
theProject._data._currCounts = [100, 100] theProject.data.setCurrCounts(100, 100)
# No path for writing
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("os.path.isdir", lambda *a, **k: False) mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
assert not theProject._appendSessionStats(idleTime=0) assert theProject._appendSessionStats(idleTime=0) is False
# Block open # Block open
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert not theProject._appendSessionStats(idleTime=0) assert theProject._appendSessionStats(idleTime=0) is False
# Session too short
theProject._projOpened = time()
theProject.data.setInitCounts(50, 50)
theProject.data.setCurrCounts(50, 50)
assert theProject._appendSessionStats(idleTime=0) is False
# Write entry # Write entry
assert theProject.projMeta == os.path.join(fncDir, "meta") statsFile = theProject.storage.getMetaFile(nwFiles.SESS_STATS)
statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS) assert isinstance(statsFile, Path)
statsFile.unlink(missing_ok=True)
theProject._projOpened = 1600002000 theProject._projOpened = 1600002000
theProject._data._currCounts = [200, 100] theProject.data._initCounts = [50, 50]
theProject.data._currCounts = [200, 100]
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.project.time", lambda: 1600005600) mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
assert theProject._appendSessionStats(idleTime=99) assert theProject._appendSessionStats(idleTime=99)
assert readFile(statsFile) == ( assert statsFile.read_text(encoding="utf-8") == (
"# Offset 100\n" "# Offset 100\n"
"# Start Time End Time Novel Notes Idle\n" "# Start Time End Time Novel Notes Idle\n"
"%s %s 200 100 99\n" "%s %s 200 100 99\n"
-4
View File
@@ -170,8 +170,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert len(nwGUI.theProject.tree._treeOrder) == 0 assert len(nwGUI.theProject.tree._treeOrder) == 0
assert len(nwGUI.theProject.tree._treeRoots) == 0 assert len(nwGUI.theProject.tree._treeRoots) == 0
assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.tree.trashRoot() is None
assert nwGUI.theProject.projPath is None
assert nwGUI.theProject.projMeta is None
assert nwGUI.theProject.data.name == "" assert nwGUI.theProject.data.name == ""
assert nwGUI.theProject.data.title == "" assert nwGUI.theProject.data.title == ""
assert nwGUI.theProject.data.authors == [] assert nwGUI.theProject.data.authors == []
@@ -192,8 +190,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert len(nwGUI.theProject.tree._treeOrder) == 8 assert len(nwGUI.theProject.tree._treeOrder) == 8
assert len(nwGUI.theProject.tree._treeRoots) == 4 assert len(nwGUI.theProject.tree._treeRoots) == 4
assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.tree.trashRoot() is None
assert nwGUI.theProject.projPath == fncProj
assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta")
assert nwGUI.theProject.data.name == "New Project" assert nwGUI.theProject.data.name == "New Project"
assert nwGUI.theProject.data.title == "New Novel" assert nwGUI.theProject.data.title == "New Novel"
assert nwGUI.theProject.data.authors == ["Jane Doe"] assert nwGUI.theProject.data.authors == ["Jane Doe"]
+2 -2
View File
@@ -166,8 +166,8 @@ def buildTestProject(theObject, projPath):
theProject = theObject.theProject theProject = theObject.theProject
theProject.clearProject() theProject.clearProject()
theProject.setProjectPath(projPath, newProject=True) theProject.projPath = projPath
theProject.storage.openProjectInPlace(theProject.projPath) theProject.storage.openProjectInPlace(projPath)
theProject.setDefaultStatusImport() theProject.setDefaultStatusImport()
theProject.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") theProject.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")