Add hash and dates to document meta data (#1509)

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