Add hash and dates to document meta data (#1509)
This commit is contained in:
@@ -25,7 +25,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
import unicodedata
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -488,25 +487,6 @@ def makeFileNameSafe(text: str) -> str:
|
||||
return "".join(c for c in text if c.isalnum() or c in allowed)
|
||||
|
||||
|
||||
def sha256sum(path: str | Path) -> str | None:
|
||||
"""Make a shasum of a file using a buffer.
|
||||
Based on: https://stackoverflow.com/a/44873382/5825851
|
||||
"""
|
||||
digest = hashlib.sha256()
|
||||
bData = bytearray(65536)
|
||||
mData = memoryview(bData)
|
||||
try:
|
||||
with open(path, mode="rb", buffering=0) as inFile:
|
||||
for n in iter(lambda: inFile.readinto(mData), 0):
|
||||
digest.update(mData[:n])
|
||||
except Exception:
|
||||
logger.error("Could not create sha256sum of: %s", path)
|
||||
logException()
|
||||
return None
|
||||
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Other Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
@@ -23,15 +23,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from novelwriter.core.item import NWItem
|
||||
|
||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||
from novelwriter.error import formatException
|
||||
from novelwriter.common import isHandle, sha256sum
|
||||
from novelwriter.common import formatTimeStamp, isHandle
|
||||
from novelwriter.core.item import NWItem
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -50,30 +52,38 @@ class NWDocument:
|
||||
|
||||
def __init__(self, project: NWProject, tHandle: str | None) -> None:
|
||||
|
||||
self._project = project
|
||||
self._project = project
|
||||
|
||||
# Internal Variables
|
||||
self._theItem = None # The currently open item
|
||||
self._docHandle = None # The handle of the currently open item
|
||||
self._fileLoc = None # The file location of the currently open item
|
||||
self._docMeta = {} # The meta data of the currently open item
|
||||
self._docError = "" # The latest encountered IO error
|
||||
self._prevHash = None # Previous sha256sum of the document file
|
||||
self._currHash = None # Latest sha256sum of the document file
|
||||
self._item = None # The currently open item
|
||||
self._handle = None # The handle of the currently open item
|
||||
self._fileLoc = None # The file location of the currently open item
|
||||
self._docMeta = {} # The meta data of the currently open item
|
||||
self._docError = "" # The latest encountered IO error
|
||||
self._lastHash = "" # The last known SHA hash
|
||||
self._hashError = False # Hash mismatch on last write attempt
|
||||
|
||||
if isHandle(tHandle):
|
||||
self._docHandle = tHandle
|
||||
self._handle = tHandle
|
||||
|
||||
if self._docHandle is not None:
|
||||
self._theItem = self._project.tree[tHandle]
|
||||
if self._handle is not None:
|
||||
self._item = self._project.tree[tHandle]
|
||||
|
||||
return
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<NWDocument handle={self._docHandle}>"
|
||||
return f"<NWDocument handle={self._handle}>"
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self._docHandle is not None and bool(self._theItem)
|
||||
return self._handle is not None and self._item is not None
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def hashError(self) -> bool:
|
||||
"""Check if the file hash has changed outside of novelWriter."""
|
||||
return self._hashError
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
@@ -81,7 +91,7 @@ class NWDocument:
|
||||
|
||||
def fileExists(self) -> bool:
|
||||
"""Check if the document file exists."""
|
||||
if self._docHandle is None:
|
||||
if self._handle is None:
|
||||
return False
|
||||
|
||||
contentPath = self._project.storage.contentPath
|
||||
@@ -89,7 +99,7 @@ class NWDocument:
|
||||
logger.error("No content path set")
|
||||
return False
|
||||
|
||||
return (contentPath / f"{self._docHandle}.nwd").is_file()
|
||||
return (contentPath / f"{self._handle}.nwd").is_file()
|
||||
|
||||
def readDocument(self, isOrphan: bool = False) -> str | None:
|
||||
"""Read the document specified by the handle set in the
|
||||
@@ -98,11 +108,11 @@ class NWDocument:
|
||||
empty string. If something went wrong, return None.
|
||||
"""
|
||||
self._docError = ""
|
||||
if not isinstance(self._docHandle, str):
|
||||
if not isinstance(self._handle, str):
|
||||
logger.error("No document handle set")
|
||||
return None
|
||||
|
||||
if self._theItem is None and not isOrphan:
|
||||
if self._item is None and not isOrphan:
|
||||
logger.error("Unknown novelWriter document")
|
||||
return None
|
||||
|
||||
@@ -111,31 +121,30 @@ class NWDocument:
|
||||
logger.error("No content path set")
|
||||
return None
|
||||
|
||||
docFile = f"{self._docHandle}.nwd"
|
||||
docFile = f"{self._handle}.nwd"
|
||||
logger.debug("Opening document: %s", docFile)
|
||||
|
||||
docPath = contentPath / docFile
|
||||
self._fileLoc = docPath
|
||||
|
||||
theText = ""
|
||||
text = ""
|
||||
self._docMeta = {}
|
||||
self._prevHash = None
|
||||
self._lastHash = ""
|
||||
|
||||
if docPath.exists():
|
||||
self._prevHash = sha256sum(docPath)
|
||||
try:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
# Check the first <= 10 lines for metadata
|
||||
for i in range(10):
|
||||
inLine = inFile.readline()
|
||||
if inLine.startswith(r"%%~"):
|
||||
self._parseMeta(inLine)
|
||||
line = inFile.readline()
|
||||
if line.startswith(r"%%~"):
|
||||
self._parseMeta(line)
|
||||
else:
|
||||
theText = inLine
|
||||
text = line
|
||||
break
|
||||
|
||||
# Load the rest of the file
|
||||
theText += inFile.read()
|
||||
text += inFile.read()
|
||||
|
||||
except Exception as exc:
|
||||
self._docError = formatException(exc)
|
||||
@@ -143,19 +152,20 @@ class NWDocument:
|
||||
|
||||
else:
|
||||
# The document file does not exist, so we assume it's a new
|
||||
# document and initialise an empty text string.
|
||||
# document and return an empty text string.
|
||||
logger.debug("The requested document does not exist")
|
||||
return ""
|
||||
|
||||
return theText
|
||||
self._lastHash = hashlib.sha1(text.encode()).hexdigest()
|
||||
|
||||
def writeDocument(self, docText: str, forceWrite: bool = False) -> bool:
|
||||
return text
|
||||
|
||||
def writeDocument(self, text: str, forceWrite: bool = False) -> bool:
|
||||
"""Write the document specified by the handle attribute. Handle
|
||||
any IO errors in the process Returns True if successful, False
|
||||
if not.
|
||||
"""
|
||||
self._docError = ""
|
||||
if not isinstance(self._docHandle, str):
|
||||
if not isinstance(self._handle, str):
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
@@ -164,32 +174,45 @@ class NWDocument:
|
||||
logger.error("No content path set")
|
||||
return False
|
||||
|
||||
docFile = f"{self._docHandle}.nwd"
|
||||
docFile = f"{self._handle}.nwd"
|
||||
logger.debug("Saving document: %s", docFile)
|
||||
|
||||
docPath = contentPath / docFile
|
||||
docTemp = docPath.with_suffix(".tmp")
|
||||
|
||||
if self._prevHash is not None and not forceWrite:
|
||||
self._currHash = sha256sum(docPath)
|
||||
if self._currHash is not None and self._currHash != self._prevHash:
|
||||
logger.error("File has been altered on disk since opened")
|
||||
return False
|
||||
# Re-read the document on disk to check if it has changed
|
||||
prevHash = self._lastHash
|
||||
self.readDocument()
|
||||
if prevHash and self._lastHash != prevHash and not forceWrite:
|
||||
logger.error("File has been altered on disk since opened")
|
||||
self._hashError = True
|
||||
return False
|
||||
|
||||
currTime = formatTimeStamp(time())
|
||||
writeHash = hashlib.sha1(text.encode()).hexdigest()
|
||||
createdDate = self._docMeta.get("created", "Unknown")
|
||||
updatedDate = self._docMeta.get("updated", "Unknown")
|
||||
if writeHash != self._lastHash:
|
||||
updatedDate = currTime
|
||||
if not docPath.is_file():
|
||||
createdDate = currTime
|
||||
updatedDate = currTime
|
||||
|
||||
# DocMeta Line
|
||||
if self._theItem is None:
|
||||
docMeta = ""
|
||||
else:
|
||||
docMeta = ""
|
||||
if self._item:
|
||||
docMeta = (
|
||||
f"%%~name: {self._theItem.itemName}\n"
|
||||
f"%%~path: {self._theItem.itemParent}/{self._theItem.itemHandle}\n"
|
||||
f"%%~kind: {self._theItem.itemClass.name}/{self._theItem.itemLayout.name}\n"
|
||||
f"%%~name: {self._item.itemName}\n"
|
||||
f"%%~path: {self._item.itemParent}/{self._item.itemHandle}\n"
|
||||
f"%%~kind: {self._item.itemClass.name}/{self._item.itemLayout.name}\n"
|
||||
f"%%~hash: {writeHash}\n"
|
||||
f"%%~date: {createdDate}/{updatedDate}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
with open(docTemp, mode="w", encoding="utf-8") as outFile:
|
||||
outFile.write(docMeta)
|
||||
outFile.write(docText)
|
||||
outFile.write(text)
|
||||
except Exception as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
@@ -202,8 +225,8 @@ class NWDocument:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
|
||||
self._prevHash = sha256sum(docPath)
|
||||
self._currHash = self._prevHash
|
||||
self._lastHash = writeHash
|
||||
self._hashError = False
|
||||
|
||||
return True
|
||||
|
||||
@@ -212,7 +235,7 @@ class NWDocument:
|
||||
from the project data folder.
|
||||
"""
|
||||
self._docError = ""
|
||||
if not isinstance(self._docHandle, str):
|
||||
if not isinstance(self._handle, str):
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
@@ -221,7 +244,7 @@ class NWDocument:
|
||||
logger.error("No content path set")
|
||||
return False
|
||||
|
||||
docPath = contentPath / f"{self._docHandle}.nwd"
|
||||
docPath = contentPath / f"{self._handle}.nwd"
|
||||
docTemp = docPath.with_suffix(".tmp")
|
||||
|
||||
try:
|
||||
@@ -247,7 +270,7 @@ class NWDocument:
|
||||
|
||||
def getCurrentItem(self) -> NWItem | None:
|
||||
"""Return a pointer to the currently open NWItem."""
|
||||
return self._theItem
|
||||
return self._item
|
||||
|
||||
def getMeta(self) -> tuple[str, str | None, nwItemClass | None, nwItemLayout | None]:
|
||||
"""Parse the document meta tag and return the name, parent,
|
||||
@@ -293,8 +316,18 @@ class NWDocument:
|
||||
if metaBits[1] in nwItemLayout.__members__:
|
||||
self._docMeta["layout"] = nwItemLayout[metaBits[1]]
|
||||
|
||||
elif metaLine.startswith("%%~hash:"):
|
||||
self._docMeta["hash"] = metaLine[8:].strip()
|
||||
|
||||
elif metaLine.startswith("%%~date:"):
|
||||
metaVal = metaLine[8:].strip()
|
||||
metaBits = metaVal.split("/")
|
||||
if len(metaBits) == 2:
|
||||
self._docMeta["created"] = metaBits[0].strip()
|
||||
self._docMeta["updated"] = metaBits[1].strip()
|
||||
|
||||
else:
|
||||
logger.debug("Ignoring meta data: '%s'", metaLine.strip())
|
||||
logger.debug("Unknown meta data: '%s'", metaLine.strip())
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self.saveCursorPosition()
|
||||
if not self._nwDocument.writeDocument(docText):
|
||||
saveOk = False
|
||||
if self._nwDocument._currHash != self._nwDocument._prevHash:
|
||||
if self._nwDocument.hashError:
|
||||
msgYes = SHARED.question(self.tr(
|
||||
"This document has been changed outside of novelWriter "
|
||||
"while it was open. Overwrite the file on disk?"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: John Smith
|
||||
%%~path: f7e2d9f330615/14298de4d9524
|
||||
%%~kind: CHARACTER/NOTE
|
||||
%%~hash: 259eff30f10e101cf93e27e8764e851d356d54b8
|
||||
%%~date: Unknown/2023-08-25 16:52:03
|
||||
# John Smith
|
||||
|
||||
@tag: John
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Title Page
|
||||
%%~path: 7031beac91f75/53b69b83cdafc
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: c5dc35d18ecb074a9e41a1410d1bff8021cf0a5b
|
||||
%%~date: Unknown/2023-08-25 16:51:52
|
||||
#! My Novel
|
||||
|
||||
>> **By Jane Smith** <<
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Mars
|
||||
%%~path: 15c4492bd5107/5eaea4e8cdee8
|
||||
%%~kind: WORLD/NOTE
|
||||
%%~hash: 0838371bc03e31f503fd12a8617d94ef36e5f7c7
|
||||
%%~date: Unknown/2023-08-25 16:52:07
|
||||
# Mars
|
||||
|
||||
@tag: Mars
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Making a Scene
|
||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63
|
||||
%%~date: Unknown/2023-08-25 16:56:11
|
||||
### Making a Scene
|
||||
|
||||
@pov: Jane
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Chapter One
|
||||
%%~path: 7031beac91f75/6a2d6d5f4f401
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 89687756c204742d17f1d37a0ba7fa3aadaf00f2
|
||||
%%~date: Unknown/2023-08-25 16:51:56
|
||||
## So it Begins
|
||||
|
||||
@pov: Jane
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Chapter Two
|
||||
%%~path: 7031beac91f75/88706ddc78b1b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: ca68d1134e1b7870b7bf0fcbd8c0d67384970717
|
||||
%%~date: Unknown/2023-08-25 16:52:00
|
||||
## Where has John Gone?
|
||||
|
||||
@pov: Jane
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Old File
|
||||
%%~path: ae9bf3c3ea159/8a5deb88c0e97
|
||||
%%~kind: ARCHIVE/DOCUMENT
|
||||
%%~hash: de9f8f39428e9d127504f47c554fc3e701b0b773
|
||||
%%~date: Unknown/2023-08-25 16:52:08
|
||||
### Discarded Scene
|
||||
|
||||
If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: A Note on Structure
|
||||
%%~path: 7031beac91f75/96b68994dfa3d
|
||||
%%~kind: NOVEL/NOTE
|
||||
%%~hash: f4f88fa2d824ed4e92223bd6744979cd3f9f45e7
|
||||
%%~date: Unknown/2023-08-25 16:51:59
|
||||
# A Note on Structure
|
||||
|
||||
This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Page
|
||||
%%~path: 7031beac91f75/974e400180a99
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 7547809e972205eb2a55dd988595e0aa1d01e139
|
||||
%%~date: Unknown/2023-08-25 16:51:54
|
||||
[NEW PAGE]
|
||||
[VSPACE:2]
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Chapter One
|
||||
%%~path: e5e47ebf63b1c/a520879ca0b45
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 2285256b970dbb3ccf833d9275cd5f8935b6aff1
|
||||
%%~date: Unknown/2023-08-25 16:52:02
|
||||
## Chapter One
|
||||
|
||||
@pov: Jane
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: We Found John!
|
||||
%%~path: 88706ddc78b1b/ae7339df26ded
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 1da9cf71a57d11c5d94b021a2c3e6a5b8df67f14
|
||||
%%~date: Unknown/2023-08-25 16:52:01
|
||||
### We Found John!
|
||||
|
||||
@pov: John
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Earth
|
||||
%%~path: 15c4492bd5107/b3e74dbc1f584
|
||||
%%~kind: WORLD/NOTE
|
||||
%%~hash: 87f520b4e80af8505af1b868ab25ec6d51b7ff48
|
||||
%%~date: Unknown/2023-08-25 16:52:05
|
||||
# Earth
|
||||
|
||||
@tag: Earth
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Delete Me!
|
||||
%%~path: 98acd8c76c93a/b8136a5a774a0
|
||||
%%~kind: TRASH/DOCUMENT
|
||||
%%~hash: e1eabd80260e03595c8c0a1f82d813d0ae33c273
|
||||
%%~date: Unknown/2023-08-25 16:52:09
|
||||
### Delete Me!
|
||||
|
||||
This scene is trash.
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Interlude
|
||||
%%~path: 7031beac91f75/ba8a28a246524
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: df8904011eda90c7b694bed725dc774bf631b348
|
||||
%%~date: Unknown/2023-08-25 16:51:58
|
||||
##! Interlude
|
||||
|
||||
% Notice that this document has a title with a ‘!’ in it in addition to the two hash symbols. This means it is an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Title Page
|
||||
%%~path: e5e47ebf63b1c/bacb7059e3083
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 16b55dd0f8dc6bbba266384930a87ec211d471df
|
||||
%%~date: Unknown/2023-08-25 16:52:01
|
||||
#! Sequel Novel
|
||||
|
||||
>> **By Jane Doh** <<
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Jane Smith
|
||||
%%~path: f7e2d9f330615/bb2c23b3c42cc
|
||||
%%~kind: CHARACTER/NOTE
|
||||
%%~hash: a69e4ca6ceede2536c8d364f88e6b62ef0ec36c3
|
||||
%%~date: Unknown/2023-08-25 16:52:04
|
||||
# Jane Smith
|
||||
|
||||
@tag: Jane
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Another Scene
|
||||
%%~path: 6a2d6d5f4f401/bc0cbd2a407f3
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: b3f9e4b097f7041c77489876577014c435a8be0c
|
||||
%%~date: Unknown/2023-08-25 16:51:57
|
||||
### Another Scene
|
||||
|
||||
@pov: John
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Part One
|
||||
%%~path: 7031beac91f75/edca4be2fcaf8
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 9dc53a9bcc457d18b775adf7be38ac89df3408ca
|
||||
%%~date: Unknown/2023-08-25 16:51:54
|
||||
# Part One
|
||||
|
||||
>> In the beginning … <<
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Space
|
||||
%%~path: 15c4492bd5107/f1471bef9f2ae
|
||||
%%~kind: WORLD/NOTE
|
||||
%%~hash: bc139a020239c9c0d936aa9d0bcc3644ae2f370c
|
||||
%%~date: Unknown/2023-08-25 16:52:06
|
||||
# Space
|
||||
|
||||
@tag: Space
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-08-08 22:19:23">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1517" autoCount="237" editTime="75241">
|
||||
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-08-25 16:56:30">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1518" autoCount="237" editTime="75281">
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
@@ -58,7 +58,7 @@
|
||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||
</item>
|
||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="748" />
|
||||
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="66" />
|
||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
|
||||
+1
-2
@@ -226,8 +226,7 @@ def prjLipsum():
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ipsumText():
|
||||
"""Return five paragraphs of Lorem Ipsum text.
|
||||
"""
|
||||
"""Return five paragraphs of Lorem Ipsum text."""
|
||||
thatIpsum = [(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum co"
|
||||
"mmodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, e"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Ancient Europe
|
||||
%%~path: 60bdf227455cc/04468803b92e1
|
||||
%%~kind: WORLD/NOTE
|
||||
%%~hash: b4318c2de40a1fc4055d18f8d02696b9cf171ea2
|
||||
%%~date: Unknown/Unknown
|
||||
# Ancient Europe
|
||||
|
||||
@tag: Europe
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Main
|
||||
%%~path: 6c6afb1247750/2426c6f0ca922
|
||||
%%~kind: PLOT/NOTE
|
||||
%%~hash: db3897d166e246acdb5e25e9bd98c5a40a699ed0
|
||||
%%~date: Unknown/Unknown
|
||||
# Main Plot
|
||||
|
||||
@tag: Main
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Chapter Two
|
||||
%%~path: 6bd935d2490cd/441420a886d82
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: fd6d46708faa1333f8f7ba0442fc5b35bf1e3f85
|
||||
%%~date: Unknown/Unknown
|
||||
## Chapter Two
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Scene Five
|
||||
%%~path: 6bd935d2490cd/47666c91c7ccf
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: d210c26966da6f9edea8726567abf4860b7bb9b7
|
||||
%%~date: Unknown/Unknown
|
||||
### Scene Five
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Mr. Nobody
|
||||
%%~path: 67a8707f2f249/4c4f28287af27
|
||||
%%~kind: CHARACTER/NOTE
|
||||
%%~hash: 07a86c2001669de7d7a7545286c22b963e3ccf63
|
||||
%%~date: Unknown/Unknown
|
||||
# Nobody Owens
|
||||
|
||||
@tag: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Lorem Ipsum
|
||||
%%~path: b3643d0f92e32/7a992350f3eb6
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 8efda028000b70be0d7dbe9647b6026082ef05c9
|
||||
%%~date: Unknown/Unknown
|
||||
#! Lorem Ipsum
|
||||
|
||||
>> **By lipsum.com** <<
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Interlude
|
||||
%%~path: b3643d0f92e32/846352075de7d
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: ac0e16c65142b9f1e0fa281bdc9b954e44026740
|
||||
%%~date: Unknown/Unknown
|
||||
##! Why do we use it?
|
||||
|
||||
% Exctracted from the lipsum.com website.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Scene One
|
||||
%%~path: 45e6b01ca35c1/88243afbe5ed8
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: a09245a7a772bbe02850b5db109977e336cd9cc1
|
||||
%%~date: Unknown/Unknown
|
||||
### Scene One
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Prologue
|
||||
%%~path: b3643d0f92e32/88d59a277361b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 19a2aa95b07ce10eaea753c46569929439648c63
|
||||
%%~date: Unknown/Unknown
|
||||
##! Prologue
|
||||
|
||||
% Synopsis:Explanation from the lipsum.com website.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Front Matter
|
||||
%%~path: b3643d0f92e32/8c58a65414c23
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 5c3961cb7616ef2b378010f38a89ed268ef15d92
|
||||
%%~date: Unknown/Unknown
|
||||
[NEW PAGE]
|
||||
|
||||
% Exctracted from the lipsum.com website.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Act One
|
||||
%%~path: b3643d0f92e32/db7e733775d4d
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: d93cd4c96d49e4afd93c82cca29d413a35012108
|
||||
%%~date: Unknown/Unknown
|
||||
# Act One
|
||||
|
||||
>> “Fusce maximus felis libero” <<
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Scene Three
|
||||
%%~path: 6bd935d2490cd/eb103bc70c90c
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: c4eda49e4fe81dc450d547eee0bdabe77fdaaa98
|
||||
%%~date: Unknown/Unknown
|
||||
### Scene Three
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Scene Four
|
||||
%%~path: 6bd935d2490cd/f8c0562e50f1b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 9461a279b9fb6ef005ee4d432fcda77ff5bfbd42
|
||||
%%~date: Unknown/Unknown
|
||||
### Scene Four
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Scene Two
|
||||
%%~path: 45e6b01ca35c1/f96ec11c6a3da
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: ebe3fbaa16d9d81bc1a139822e3bf39bb357866d
|
||||
%%~date: Unknown/Unknown
|
||||
### Scene Two
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Chapter One
|
||||
%%~path: 45e6b01ca35c1/fb609cd8319dc
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 5dabeaa7a58238a6ad99ce73176b1bdfad1a71b7
|
||||
%%~date: Unknown/Unknown
|
||||
## Chapter One
|
||||
|
||||
@pov: Bod
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:18:14">
|
||||
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="42" autoCount="24" editTime="1913">
|
||||
<novelWriterXML appVersion="2.1-beta1" hexVersion="0x020100b1" fileVersion="1.5" fileRevision="1" timeStamp="2023-08-25 18:03:37">
|
||||
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="43" autoCount="24" editTime="1935">
|
||||
<name>Lorem Ipsum</name>
|
||||
<title>Lorem Ipsum</title>
|
||||
<author>lipsum.com</author>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: Chapter 1
|
||||
%%~path: 0000000000008/0000000000010
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 25af8567c8a3b3b49a27dcda2999ed1f8811955a
|
||||
%%~date: 2023-08-25 18:32:30/2023-08-25 18:32:30
|
||||
## Chapter 1
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: All of Chapter 1
|
||||
%%~path: 0000000000008/0000000000014
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 9e632e7e29860572c685da8501b92bf99825ac3b
|
||||
%%~date: 2023-08-25 18:29:39/2023-08-25 18:29:39
|
||||
% Merge Novel Chapter: Chapter 1 [New]
|
||||
|
||||
## Chapter 1
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: New Scene
|
||||
%%~path: 000000000000d/000000000000f
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: fd5dc2f0c9767cb124b1bf2300d7a33b7780045e
|
||||
%%~date: 2023-08-25 18:08:01/2023-08-25 18:08:04
|
||||
# Novel
|
||||
|
||||
## Chapter
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: New Note
|
||||
%%~path: 000000000000a/0000000000010
|
||||
%%~kind: CHARACTER/NOTE
|
||||
%%~hash: 9fae6dfdd3d1c0822d3a3cf90c0142e65ad8e557
|
||||
%%~date: 2023-08-25 18:14:24/2023-08-25 18:14:24
|
||||
# Jane Doe
|
||||
|
||||
@tag: Jane
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: New Note
|
||||
%%~path: 0000000000009/0000000000011
|
||||
%%~kind: PLOT/NOTE
|
||||
%%~hash: 8ff26f8a18ad6390c2ce725c441ab0c792a125cf
|
||||
%%~date: 2023-08-25 18:15:35/2023-08-25 18:15:35
|
||||
# Main Plot
|
||||
|
||||
@tag: MainPlot
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%%~name: New Note
|
||||
%%~path: 000000000000b/0000000000012
|
||||
%%~kind: WORLD/NOTE
|
||||
%%~hash: 3f5c3c6c3ba1c27c30b8ac9e59c222fb9a1bd775
|
||||
%%~date: 2023-08-25 18:17:45/2023-08-25 18:17:45
|
||||
# Main Location
|
||||
|
||||
@tag: Home
|
||||
|
||||
@@ -21,7 +21,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import pytest
|
||||
import hashlib
|
||||
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
@@ -35,8 +34,8 @@ from novelwriter.common import (
|
||||
checkString, checkStringNone, checkUuid, formatInt, formatTime,
|
||||
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
|
||||
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
|
||||
numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified,
|
||||
transferCase, xmlIndent, yesNo
|
||||
numberToRoman, NWConfigParser, readTextFile, simplified, transferCase,
|
||||
xmlIndent, yesNo
|
||||
)
|
||||
|
||||
|
||||
@@ -622,45 +621,6 @@ def testBaseCommon_makeFileNameSafe():
|
||||
# END Test testBaseCommon_makeFileNameSafe
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_sha256sum(monkeypatch, fncPath, ipsumText):
|
||||
"""Test the sha256sum function."""
|
||||
longText = 50*(" ".join(ipsumText) + " ")
|
||||
shortText = "This is a short file"
|
||||
noneText = ""
|
||||
|
||||
assert len(longText) == 175650
|
||||
|
||||
longFile = fncPath / "long_file.txt"
|
||||
shortFile = fncPath / "short_file.txt"
|
||||
noneFile = fncPath / "none_file.txt"
|
||||
|
||||
writeFile(longFile, longText)
|
||||
writeFile(shortFile, shortText)
|
||||
writeFile(noneFile, noneText)
|
||||
|
||||
# Taken with sha256sum command on command line
|
||||
longHash = "9b22aee35660da4fae204acbe96aec7f563022746ca2b7a3831f5e44544765eb"
|
||||
shortHash = "6d7c9b2722364c471b8a8666bcb35d18500272d05b23b3427288e2e34c6618f0"
|
||||
noneHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
|
||||
assert sha256sum(longFile) == longHash
|
||||
assert sha256sum(shortFile) == shortHash
|
||||
assert sha256sum(noneFile) == noneHash
|
||||
|
||||
assert hashlib.sha256(longText.encode("utf-8")).hexdigest() == longHash
|
||||
assert hashlib.sha256(shortText.encode("utf-8")).hexdigest() == shortHash
|
||||
assert hashlib.sha256(noneText.encode("utf-8")).hexdigest() == noneHash
|
||||
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert sha256sum(longFile) is None
|
||||
assert sha256sum(shortFile) is None
|
||||
assert sha256sum(noneFile) is None
|
||||
|
||||
# END Test testBaseCommon_sha256sum
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_getGuiItem(nwGUI):
|
||||
"""Check the GUI item function."""
|
||||
|
||||
@@ -26,8 +26,8 @@ from shutil import copyfile
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE
|
||||
from mocked import causeOSError
|
||||
from tools import C, buildTestProject, cmpFiles, XML_IGNORE
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwItemClass
|
||||
@@ -46,19 +46,19 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
||||
# =====================
|
||||
|
||||
hChapter1 = theProject.newFile("Chapter 1", C.hNovelRoot)
|
||||
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1)
|
||||
hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1)
|
||||
hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1)
|
||||
hSceneOne11 = theProject.newFile("Scene 1.1", hChapter1) # type: ignore
|
||||
hSceneOne12 = theProject.newFile("Scene 1.2", hChapter1) # type: ignore
|
||||
hSceneOne13 = theProject.newFile("Scene 1.3", hChapter1) # type: ignore
|
||||
|
||||
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
|
||||
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
|
||||
docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
|
||||
docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
|
||||
|
||||
theProject.writeNewFile(hChapter1, 2, True, docText1)
|
||||
theProject.writeNewFile(hSceneOne11, 3, True, docText2)
|
||||
theProject.writeNewFile(hSceneOne12, 3, True, docText3)
|
||||
theProject.writeNewFile(hSceneOne13, 3, True, docText4)
|
||||
theProject.writeNewFile(hChapter1, 2, True, docText1) # type: ignore
|
||||
theProject.writeNewFile(hSceneOne11, 3, True, docText2) # type: ignore
|
||||
theProject.writeNewFile(hSceneOne12, 3, True, docText3) # type: ignore
|
||||
theProject.writeNewFile(hSceneOne13, 3, True, docText4) # type: ignore
|
||||
|
||||
# Basic Checks
|
||||
# ============
|
||||
@@ -81,12 +81,12 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
||||
testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000014.nwd"
|
||||
compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000014.nwd"
|
||||
|
||||
assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014"
|
||||
assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014" # type: ignore
|
||||
|
||||
assert docMerger.appendText(hChapter1, True, "Merge") is True
|
||||
assert docMerger.appendText(hSceneOne11, True, "Merge") is True
|
||||
assert docMerger.appendText(hSceneOne12, True, "Merge") is True
|
||||
assert docMerger.appendText(hSceneOne13, True, "Merge") is True
|
||||
assert docMerger.appendText(hChapter1, True, "Merge") is True # type: ignore
|
||||
assert docMerger.appendText(hSceneOne11, True, "Merge") is True # type: ignore
|
||||
assert docMerger.appendText(hSceneOne12, True, "Merge") is True # type: ignore
|
||||
assert docMerger.appendText(hSceneOne13, True, "Merge") is True # type: ignore
|
||||
|
||||
# Block writing and check error handling
|
||||
with monkeypatch.context() as mp:
|
||||
@@ -98,7 +98,7 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
||||
# Write properly, and compare
|
||||
assert docMerger.writeTargetDoc() is True
|
||||
copyfile(saveFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||
|
||||
# Merge into Existing
|
||||
# ===================
|
||||
@@ -107,15 +107,15 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
|
||||
testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000010.nwd"
|
||||
compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000010.nwd"
|
||||
|
||||
docMerger.setTargetDoc(hChapter1)
|
||||
docMerger.setTargetDoc(hChapter1) # type: ignore
|
||||
|
||||
assert docMerger.appendText(hSceneOne11, True, "Merge") is True
|
||||
assert docMerger.appendText(hSceneOne12, True, "Merge") is True
|
||||
assert docMerger.appendText(hSceneOne13, True, "Merge") is True
|
||||
assert docMerger.appendText(hSceneOne11, True, "Merge") is True # type: ignore
|
||||
assert docMerger.appendText(hSceneOne12, True, "Merge") is True # type: ignore
|
||||
assert docMerger.appendText(hSceneOne13, True, "Merge") is True # type: ignore
|
||||
|
||||
assert docMerger.writeTargetDoc() is True
|
||||
copyfile(saveFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||
|
||||
# Just for debugging
|
||||
docMerger.writeTargetDoc()
|
||||
@@ -163,11 +163,11 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText)
|
||||
docText = "\n\n".join(docData)
|
||||
docRaw = docText.splitlines()
|
||||
assert theProject.storage.getDocument(hSplitDoc).writeDocument(docText) is True
|
||||
theProject.tree[hSplitDoc].setStatus(C.sFinished)
|
||||
theProject.tree[hSplitDoc].setImport(C.iMain)
|
||||
theProject.tree[hSplitDoc].setStatus(C.sFinished) # type: ignore
|
||||
theProject.tree[hSplitDoc].setImport(C.iMain) # type: ignore
|
||||
|
||||
docSplitter = DocSplitter(theProject, hSplitDoc)
|
||||
assert docSplitter._srcItem.isFileType()
|
||||
docSplitter = DocSplitter(theProject, hSplitDoc) # type: ignore
|
||||
assert docSplitter._srcItem.isFileType() # type: ignore
|
||||
assert docSplitter.getError() == ""
|
||||
|
||||
# Run the split algorithm
|
||||
@@ -247,8 +247,8 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText)
|
||||
|
||||
# Check that status and importance has been preserved
|
||||
for rHandle in resDocHandle:
|
||||
assert theProject.tree[rHandle].itemStatus == C.sFinished
|
||||
assert theProject.tree[rHandle].itemImport == C.iMain
|
||||
assert theProject.tree[rHandle].itemStatus == C.sFinished # type: ignore
|
||||
assert theProject.tree[rHandle].itemImport == C.iMain # type: ignore
|
||||
|
||||
# Check handling of improper initialisation
|
||||
docSplitter = DocSplitter(theProject, C.hInvalid)
|
||||
@@ -416,7 +416,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
|
||||
assert projBuild.buildProject({}) is False
|
||||
|
||||
# Wrong type should also fail
|
||||
assert projBuild.buildProject("stuff") is False
|
||||
assert projBuild.buildProject("stuff") is False # type: ignore
|
||||
|
||||
# Try again with a proper path
|
||||
assert projBuild.buildProject({"projPath": fncPath}) is True
|
||||
@@ -508,7 +508,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockRnd):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths):
|
||||
def testCoreTools_NewSample(monkeypatch, mockGUI, fncPath, tstPaths):
|
||||
"""Check that we can create a new project can be created from the
|
||||
provided sample project via a zip file.
|
||||
"""
|
||||
|
||||
@@ -21,8 +21,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
|
||||
from tools import C, MOCK_TIME, buildTestProject, readFile, writeFile
|
||||
from mocked import causeOSError
|
||||
from tools import C, buildTestProject, readFile, writeFile
|
||||
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -32,6 +32,8 @@ from novelwriter.core.document import NWDocument
|
||||
@pytest.mark.core
|
||||
def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
"""Test loading and saving a document with the NWDocument class."""
|
||||
monkeypatch.setattr("novelwriter.core.document.time", lambda: MOCK_TIME)
|
||||
|
||||
theProject = NWProject()
|
||||
mockRnd.reset()
|
||||
buildTestProject(theProject, fncPath)
|
||||
@@ -48,7 +50,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
# Non-existent handle
|
||||
theDoc = NWDocument(theProject, C.hInvalid)
|
||||
assert theDoc.readDocument() is None
|
||||
assert theDoc._currHash is None
|
||||
assert theDoc._lastHash == ""
|
||||
assert theDoc.fileExists() is False
|
||||
|
||||
# No content path
|
||||
@@ -90,7 +92,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
# Set handle and save
|
||||
theText = "### Test File\n\nText ...\n\n"
|
||||
theDoc = NWDocument(theProject, xHandle)
|
||||
assert theDoc.readDocument(xHandle) == ""
|
||||
assert theDoc.readDocument(xHandle) == "" # type: ignore
|
||||
assert theDoc.writeDocument(theText) is True
|
||||
|
||||
# Save again to ensure temp file and previous file is handled
|
||||
@@ -102,6 +104,8 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
"%%~name: New File\n"
|
||||
f"%%~path: {C.hNovelRoot}/{xHandle}\n"
|
||||
"%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%%~hash: b288c3ab03181027d9a16d7fd2291262f5de9ac8\n"
|
||||
"%%~date: 2019-05-10 18:52:00/2019-05-10 18:52:00\n"
|
||||
"### Test File\n\n"
|
||||
"Text ...\n\n"
|
||||
)
|
||||
@@ -114,7 +118,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
assert theDoc.writeDocument(theText, forceWrite=True) is True
|
||||
|
||||
# Force no meta data
|
||||
theDoc._theItem = None
|
||||
theDoc._item = None
|
||||
assert theDoc.writeDocument(theText) is True
|
||||
assert readFile(docPath) == theText
|
||||
|
||||
@@ -137,7 +141,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
assert theDoc.getError() == ""
|
||||
|
||||
# Saving with no handle
|
||||
theDoc._docHandle = None
|
||||
theDoc._handle = None
|
||||
assert theDoc.writeDocument(theText) is False
|
||||
|
||||
# Delete Document
|
||||
@@ -170,9 +174,10 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
|
||||
"""Test other methods of the NWDocument class.
|
||||
"""
|
||||
def testCoreDocument_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
"""Test other methods of the NWDocument class."""
|
||||
monkeypatch.setattr("novelwriter.core.document.time", lambda: MOCK_TIME)
|
||||
|
||||
theProject = NWProject()
|
||||
mockRnd.reset()
|
||||
buildTestProject(theProject, fncPath)
|
||||
@@ -187,7 +192,7 @@ def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
|
||||
|
||||
# Check the item
|
||||
assert theDoc.getCurrentItem() is not None
|
||||
assert theDoc.getCurrentItem().itemHandle == C.hSceneDoc
|
||||
assert theDoc.getCurrentItem().itemHandle == C.hSceneDoc # type: ignore
|
||||
|
||||
# Check the meta
|
||||
theName, theParent, theClass, theLayout = theDoc.getMeta()
|
||||
@@ -202,6 +207,8 @@ def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
|
||||
"%%~name: New Scene\n"
|
||||
f"%%~path: {C.hChapterDir}/{C.hSceneDoc}\n"
|
||||
"%%~kind: NOVEL/DOCUMENT\n"
|
||||
"%%~hash: dd350c602de803554b2a7c17f191ae25dea1df63\n"
|
||||
"%%~date: 2019-05-10 18:52:00/2019-05-10 18:52:00\n"
|
||||
"%%~ stuff\n"
|
||||
"### Test File\n\n"
|
||||
"Text ...\n\n"
|
||||
|
||||
@@ -24,7 +24,7 @@ import pytest
|
||||
from shutil import copyfile
|
||||
|
||||
from tools import (
|
||||
C, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem, writeFile
|
||||
C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem, writeFile
|
||||
)
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
@@ -529,25 +529,25 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_000000000000f.nwd"
|
||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_000000000000f.nwd"
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||
|
||||
projFile = projPath / "content" / "0000000000010.nwd"
|
||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000010.nwd"
|
||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000010.nwd"
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||
|
||||
projFile = projPath / "content" / "0000000000011.nwd"
|
||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000011.nwd"
|
||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000011.nwd"
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||
|
||||
projFile = projPath / "content" / "0000000000012.nwd"
|
||||
testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000012.nwd"
|
||||
compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000012.nwd"
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=NWD_IGNORE)
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
+4
-2
@@ -23,11 +23,14 @@ from __future__ import annotations
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
XML_IGNORE = ("<novelWriterXML", "<project")
|
||||
ODT_IGNORE = ("<meta:generator", "<meta:creation-date", "<dc:date", "<meta:editing")
|
||||
NWD_IGNORE = ("%%~date:",)
|
||||
MOCK_TIME = datetime(2019, 5, 10, 18, 52, 0).timestamp()
|
||||
|
||||
|
||||
class C:
|
||||
@@ -62,8 +65,7 @@ def cmpFiles(
|
||||
ignoreLines: list | None = None,
|
||||
ignoreStart: tuple | None = None
|
||||
) -> bool:
|
||||
"""Compare two files, but optionally ignore lines given by a list.
|
||||
"""
|
||||
"""Compare two files, with optional line ignore."""
|
||||
if ignoreLines is None:
|
||||
ignoreLines = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user