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
project = NWProject(self.mainGui)
if not project.setProjectPath(projPath, newProject=True):
if not project.storage.openProjectInPlace(projPath, newProject=True):
return False
if not project.storage.openProjectInPlace(projPath):
return False
project.projPath = projPath
lblNewProject = self.tr("New Project")
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/>.
"""
import os
import json
import logging
from time import time
from pathlib import Path
from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException
@@ -141,12 +141,15 @@ class NWIndex:
def loadIndex(self):
"""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 = {}
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time()
self._indexBroken = False
if os.path.isfile(indexFile):
if indexFile.exists():
logger.debug("Loading index file")
try:
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
data folder.
"""
indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path):
return False
logger.debug("Saving index file")
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time()
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/>.
"""
import os
import json
import logging
from enum import Enum
from pathlib import Path
from novelwriter.error import logException
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
@@ -77,13 +77,12 @@ class OptionState:
def loadSettings(self):
"""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
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {}
if os.path.isfile(stateFile):
if stateFile.exists():
logger.debug("Loading GUI options file")
try:
with open(stateFile, mode="r", encoding="utf-8") as inFile:
@@ -106,12 +105,11 @@ class OptionState:
def saveSettings(self):
"""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
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file")
try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self._theState, outFile, indent=2)
+5 -48
View File
@@ -27,6 +27,7 @@ from __future__ import annotations
import os
import json
from pathlib import Path
import shutil
import logging
import novelwriter
@@ -84,7 +85,6 @@ class NWProject(QObject):
# Class Settings
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.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary
@@ -254,7 +254,6 @@ class NWProject(QObject):
# Project Settings
self.projPath = None
self.projMeta = None
self.projCache = None
self.projContent = None
self.projDict = None
@@ -276,11 +275,10 @@ class NWProject(QObject):
self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath)
self.projCache = str(self._storage.cachePath)
self.projMeta = str(self._storage.metaPath)
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
# ============
@@ -421,8 +419,6 @@ class NWProject(QObject):
return False
saveTime = time()
if not self.ensureFolderStructure():
return False
logger.info("Saving project: %s", self.projPath)
@@ -482,7 +478,6 @@ class NWProject(QObject):
if self.projPath is None or self.projPath == "":
return False
self.projMeta = os.path.join(self.projPath, "meta")
self.projCache = os.path.join(self.projPath, "cache")
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
return False
if not self._checkFolder(self.projMeta):
return False
if not self._checkFolder(self.projCache):
return False
if not self._checkFolder(self.projContent):
@@ -589,41 +582,6 @@ class NWProject(QObject):
# 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):
"""Set the project-specific language.
"""
@@ -934,12 +892,10 @@ class NWProject(QObject):
def _appendSessionStats(self, idleTime):
"""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
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = os.path.isfile(sessionFile)
nowTime = time()
iNovel, iNotes = self._data.initCounts
cNovel, cNotes = self._data.currCounts
@@ -953,6 +909,7 @@ class NWProject(QObject):
return False
try:
isFile = sessionFile.exists() # We must save the state before we open
with open(sessionFile, mode="a+", encoding="utf-8") as outFile:
if not isFile:
# 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
def openProjectInPlace(self, path):
def openProjectInPlace(self, path, newProject=False):
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
@@ -112,7 +112,7 @@ class NWStorage:
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE
if self._prepareStorage(checkLegacy=True) is False:
if not self._prepareStorage(checkLegacy=True, newProject=newProject):
self.clear()
return False
@@ -142,7 +142,7 @@ class NWStorage:
##
def getXmlReader(self):
"""
"""Return a properly configured ProjectXMLReader instance.
"""
if self._runtimePath is None:
return None
@@ -153,7 +153,7 @@ class NWStorage:
return xmlReader
def getXmlWriter(self):
"""
"""Return a properly configured ProjectXMLWriter instance.
"""
if self._runtimePath is None:
return None
@@ -165,8 +165,12 @@ class NWStorage:
def getDocument(self, tHandle):
pass
def getMetaFile(self, kind):
pass
def getMetaFile(self, fileName):
"""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):
"""Read the project lock file.
@@ -234,7 +238,7 @@ class NWStorage:
def _writeLockFile(self):
pass
def _prepareStorage(self, checkLegacy=True):
def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project.
"""
path = self._runtimePath
@@ -248,6 +252,15 @@ class NWStorage:
self.clear()
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
# project, so we make sure it does. Then we add subfolders.
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/>.
"""
import os
import logging
import novelwriter
from pathlib import Path
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
@@ -150,9 +151,11 @@ class GuiWordList(QDialog):
"""
self._saveGuiSettings()
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
tmpFile = dctFile + "~"
dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
if not isinstance(dctFile, Path):
return False
tmpFile = dctFile.with_suffix(".tmp")
try:
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
for i in range(self.listBox.count()):
@@ -160,15 +163,16 @@ class GuiWordList(QDialog):
if item is not None:
outFile.write(item.text() + "\n")
if dctFile.exists():
dctFile.unlink()
tmpFile.rename(dctFile)
except Exception:
logger.error("Could not save new word list")
logException()
self.reject()
return False
if os.path.isfile(dctFile):
os.unlink(dctFile)
os.rename(tmpFile, dctFile)
self.accept()
return True
@@ -187,10 +191,12 @@ class GuiWordList(QDialog):
def _loadWordList(self):
"""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)
if not os.path.isfile(wordList):
self.listBox.clear()
if not wordList.exists():
logger.debug("No project dictionary file found")
return False
+3 -2
View File
@@ -28,6 +28,7 @@ import json
import logging
import novelwriter
from pathlib import Path
from datetime import datetime
from PyQt5.QtGui import QPixmap, QCursor
@@ -439,8 +440,8 @@ class GuiWritingStats(QDialog):
ttTime = 0
ttIdle = 0
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
if not os.path.isfile(logFile):
logFile = self.theProject.storage.getMetaFile(nwFiles.SESS_STATS)
if not isinstance(logFile, Path) or not logFile.exists():
logger.info("This project has no writing stats logfile")
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/>.
"""
import os
import json
import pytest
from mock import causeOSError
from tools import writeFile
from novelwriter.constants import nwFiles
from novelwriter.core.options import OptionState
from novelwriter.core.project import NWProject
from novelwriter.constants import nwFiles
from novelwriter.gui.noveltree import NovelTreeColumn
@pytest.mark.core
def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
"""Test loading and saving from the OptionState class.
"""
theProject = NWProject(mockGUI)
theOpts = OptionState(theProject)
metaDir = fncPath / "meta"
metaDir.mkdir()
# Write a test file
optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE)
writeFile(optFile, json.dumps({
optFile = metaDir / nwFiles.OPTS_FILE
optFile.write_text(json.dumps({
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
@@ -52,22 +54,22 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
"MockGroup": {
"mockItem": None,
},
}))
}), encoding="utf-8")
# Load and save with no path set
theProject.projMeta = None
assert not theOpts.loadSettings()
assert not theOpts.saveSettings()
theProject.storage._runtimePath = None
assert theOpts.loadSettings() is False
assert theOpts.saveSettings() is False
# Set path
theProject.projMeta = tmpDir
assert theProject.projMeta == tmpDir
theProject.storage._runtimePath = fncPath
assert theProject.storage.getMetaFile(nwFiles.OPTS_FILE) == optFile
# Cause open() to fail
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert not theOpts.loadSettings()
assert not theOpts.saveSettings()
assert theOpts.loadSettings() is False
assert theOpts.saveSettings() is False
# Load proper
assert theOpts.loadSettings()
@@ -108,9 +110,11 @@ def testCoreOptions_SetGet(mockGUI):
theProject = NWProject(mockGUI)
theOpts = OptionState(theProject)
nwColHidden = NovelTreeColumn.HIDDEN
# Set invalid values
assert not theOpts.setValue("MockGroup", "mockItem", None)
assert not theOpts.setValue("GuiBuildNovel", "mockItem", None)
assert theOpts.setValue("MockGroup", "mockItem", None) is False
assert theOpts.setValue("GuiBuildNovel", "mockItem", None) is False
# Set valid value
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", "addNovel", True)
assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell")
assert theOpts.setValue("GuiNovelView", "lastCol", nwColHidden)
# Generic get, doesn't check type
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.getBool("GuiBuildNovel", "addNovel", None) is True
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
+24 -56
View File
@@ -23,11 +23,13 @@ import os
import shutil
import pytest
from time import time
from shutil import copyfile
from pathlib import Path
from zipfile import ZipFile
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.common import formatTimeStamp
@@ -52,11 +54,6 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
mockRnd.reset()
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.PLOT) == "0000000000011"
assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000012"
@@ -108,11 +105,6 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
mockRnd.reset()
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
assert theProject.newFolder("New Folder", "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
# Fail on lock file
theProject.setProjectPath(fncDir)
assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir) is False
@@ -208,7 +199,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert theProject.closeProject()
# Force open with lockfile
theProject.setProjectPath(fncDir)
assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir, overrideLock=True) is True
assert theProject.closeProject()
@@ -267,12 +257,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
mockRnd.reset()
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
with monkeypatch.context() as mp:
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)
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
cacheDir = os.path.join(fncDir, "cache")
writeFile(cacheDir, "stuff")
@@ -323,7 +301,7 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
# Now, do it right
assert theProject.ensureFolderStructure() is True
assert os.path.isdir(metaDir)
# assert os.path.isdir(metaDir)
assert os.path.isdir(cacheDir)
assert os.path.isdir(contentDir)
@@ -506,32 +484,12 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
@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.
"""
theProject = NWProject(mockGUI)
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
theProject.data.setName(" 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
# Session stats
theProject._data._initCounts = [50, 50]
theProject._data._currCounts = [100, 100]
theProject.data.setInitCounts(50, 50)
theProject.data.setCurrCounts(100, 100)
# No path for writing
with monkeypatch.context() as mp:
mp.setattr("os.path.isdir", lambda *a, **k: False)
assert not theProject._appendSessionStats(idleTime=0)
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
assert theProject._appendSessionStats(idleTime=0) is False
# Block open
with monkeypatch.context() as mp:
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
assert theProject.projMeta == os.path.join(fncDir, "meta")
statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS)
statsFile = theProject.storage.getMetaFile(nwFiles.SESS_STATS)
assert isinstance(statsFile, Path)
statsFile.unlink(missing_ok=True)
theProject._projOpened = 1600002000
theProject._data._currCounts = [200, 100]
theProject.data._initCounts = [50, 50]
theProject.data._currCounts = [200, 100]
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
assert theProject._appendSessionStats(idleTime=99)
assert readFile(statsFile) == (
assert statsFile.read_text(encoding="utf-8") == (
"# Offset 100\n"
"# Start Time End Time Novel Notes Idle\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._treeRoots) == 0
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.title == ""
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._treeRoots) == 4
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.title == "New Novel"
assert nwGUI.theProject.data.authors == ["Jane Doe"]
+2 -2
View File
@@ -166,8 +166,8 @@ def buildTestProject(theObject, projPath):
theProject = theObject.theProject
theProject.clearProject()
theProject.setProjectPath(projPath, newProject=True)
theProject.storage.openProjectInPlace(theProject.projPath)
theProject.projPath = projPath
theProject.storage.openProjectInPlace(projPath)
theProject.setDefaultStatusImport()
theProject.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")