Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- In a dfs file opened for editing, a dynamic item's changed `Name` or newly
assigned `Quantity` is written to the file on `Close`, and in Edit mode
also on `Flush`. As in the
C# library, a new name is truncated or padded with spaces to the length of
the old one, and changing `Quantity.Item` or `Quantity.Unit` in place is not
written. A name that cp1252 cannot encode raises `UnicodeEncodeError`, and
a name containing a NUL character or a unit that is not a unit of the item
raises `ValueError`, before any item is written
([#38](https://github.com/DHI/mikecore-python/issues/38)).
- `DfsuFile.Flush()` flushes the file instead of raising `TypeError`, and
`DfsFileFactory.DfsuFileOpenAppend` no longer requires `parameters`, like the
other `Open` methods.
- Items read from a dfs file have the reference coordinates and orientation
stored in the file. They had the `-1e-35` defaults, whatever the file held
([#31](https://github.com/DHI/mikecore-python/issues/31)).
Expand Down
8 changes: 8 additions & 0 deletions mikecore/DfsDLL.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ def Init(libfilepath: str | None = None):
ctypes.c_void_p,
ctypes.c_void_p,
]
lib.dfsUpdateItemInfo.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_int32,
ctypes.c_char_p,
ctypes.c_int32,
]
lib.dfsUpdateItemInfo.restype = ctypes.c_int32

lib.dfsAddCustomBlock.argtypes = [
ctypes.c_void_p,
Expand Down
93 changes: 88 additions & 5 deletions mikecore/DfsFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,9 @@ def __init__(self):
self.fpTimeStepIndex = 0
self.headPointer = ctypes.c_void_p(0)
self.filePointer = ctypes.c_void_p(0)
# The dynamic items of an editable file, each with the Quantity it was
# read with. Flush and Close write their changes.
self.__itemsRead = []

def __del__(self):
self.Close()
Expand Down Expand Up @@ -855,6 +858,8 @@ def Open(self, filename, mode=DfsFileMode.Read, parameters=None):
self.ItemInfo = []
for i in range(noOfItems):
self.ItemInfo.append(self.__DynamicItemInfoReadAndCreate(i + 1, noOfItems))
if mode != DfsFileMode.Read:
self.__itemsRead = [(item, item.Quantity) for item in self.ItemInfo]

if mode == DfsFileMode.Read:
# file pointer is after header part
Expand Down Expand Up @@ -894,13 +899,68 @@ def Close(self):
"""
Close the file and release all ressources associated with it. The header information
is still valid (for reading) even though the file has been closed.

In a file opened for editing, a changed item Name or Quantity is
written first, see Flush. If a value is rejected, no item is written,
and the file is still closed.
"""
if self.filePointer.value is not None:
DfsDLL.Wrapper.dfsFileClose(
self.headPointer, ctypes.byref(self.filePointer)
try:
self.__WriteItemInfo()
finally:
self.__itemsRead = []
if self.filePointer.value is not None:
DfsDLL.Wrapper.dfsFileClose(
self.headPointer, ctypes.byref(self.filePointer)
)
if self.headPointer.value is not None:
DfsDLL.Wrapper.dfsHeaderDestroy(ctypes.byref(self.headPointer))

def __WriteItemInfo(self):
"""
Write the Name and Quantity of the dynamic items that changed since
the file was opened. As in the C# library, assigning a new Quantity
writes it, changing its Item or Unit in place does not, and a new name
is truncated or padded with spaces to the length of the name in the
file. Every change is checked before any is written.
"""
changes = []
for item, quantityRead in self.__itemsRead:
nameBytes = DfsDLLUtil.GetItemNameBytes(item.ItemPointer)
nameChanged = item.Name != nameBytes.decode("cp1252", "replace")
if not nameChanged and item.Quantity is quantityRead:
continue
if nameChanged:
# The native library reads the name up to the first NUL
if "\0" in item.Name:
raise ValueError(
f"Name of item {item.ItemNumber} contains a NUL character: "
f"{item.Name!r}"
)
try:
newNameBytes = item.Name.encode("cp1252")
except UnicodeEncodeError as error:
error.add_note(f"Name of item {item.ItemNumber}: {item.Name!r}")
raise
length = len(nameBytes)
nameBytes = newNameBytes[:length].ljust(length, b" ")
quantity = item.Quantity
if not eumWrapper.eumItemUnitEqv(quantity.Item, quantity.Unit):
raise ValueError(
f"Item {item.ItemNumber} has a Quantity whose unit "
f"{quantity.Unit!r} is not a unit of {quantity.Item!r}"
)
changes.append((item, nameBytes, quantity))
for item, nameBytes, quantity in changes:
rok = DfsDLL.Wrapper.dfsUpdateItemInfo(
self.headPointer,
item.ItemPointer,
ctypes.c_int32(quantity.Item),
nameBytes,
ctypes.c_int32(quantity.Unit),
)
if self.headPointer.value is not None:
DfsDLL.Wrapper.dfsHeaderDestroy(ctypes.byref(self.headPointer))
DfsDLL.CheckReturnCode(rok)
item.Name = nameBytes.decode("cp1252", "replace")
self.__itemsRead = [(item, item.Quantity) for item, _ in self.__itemsRead]

def GetNextItemNumber(self):
return self.fpItemNumber
Expand Down Expand Up @@ -1296,8 +1356,14 @@ def Flush(self):
Flush cached data to the file. This will especially update the header information,
such that if another process is reading the file while it is being read, the new
header information can be retrieved.

In a file opened for editing, a dynamic item's changed Name or newly
assigned Quantity is written to the header first. If a value is
rejected, this raises without writing any item. In Append mode, the
native library writes the header to the file only on Close.
"""
self.__CheckIfOpen()
self.__WriteItemInfo()
DfsDLL.Wrapper.dfsFileFlush(self.headPointer, self.filePointer)

def FlushTimeStep(self):
Expand Down Expand Up @@ -1572,6 +1638,23 @@ class DfsDLLUtil:
"""Utilities class, marshalling values to and from the DFS native library,
and creating various Dfs classes based on pointers to DFS native data"""

@staticmethod
def GetItemNameBytes(itemPointer) -> bytes:
"""The name of an item as stored in the header, without decoding."""
itemNameP = ctypes.c_char_p()
DfsDLL.Wrapper.dfsGetItemInfo(
itemPointer,
ctypes.byref(ctypes.c_int32()),
ctypes.byref(ctypes.c_char_p()),
ctypes.byref(itemNameP),
ctypes.byref(ctypes.c_int32()),
ctypes.byref(ctypes.c_char_p()),
ctypes.byref(ctypes.c_int()),
)
if itemNameP.value is None:
raise ValueError("Native library returned a null string")
return itemNameP.value

@staticmethod
def EncodeFileName(filename: str | os.PathLike[str]) -> bytes:
# The native library opens char* paths, which Windows interprets in the
Expand Down
2 changes: 1 addition & 1 deletion mikecore/DfsFileFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def DfsuFileOpenEdit(filename, parameters=None):
return DfsuFile(dfsFile)

@staticmethod
def DfsuFileOpenAppend(filename, parameters):
def DfsuFileOpenAppend(filename, parameters=None):
dfsFile = DfsFileFactory.DfsGenericOpenAppend(filename, parameters)
return DfsuFile(dfsFile)

Expand Down
2 changes: 1 addition & 1 deletion mikecore/DfsuFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def Reset(self):
self.dfsFile.Reset()

def Flush(self):
self.dfsFile.Flush(self)
self.dfsFile.Flush()

def FlushTimeStep(self):
self.dfsFile.FlushTimeStep()
Expand Down
57 changes: 24 additions & 33 deletions tests/test_dfs2.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,39 +243,30 @@ def test_CreateOresundBathy900Test(self):
#
# dfs2File.Close();

# def ModifyLanduseItemInfoTest():
#
# originalFilename = "testdata/Landuse.dfs2";
# filename = "testdata/testtmp/test_create_Landuse.dfs2";
#
# File.Copy(originalFilename, filename, true);
# File.SetAttributes(filename, FileAttributes.Normal); # remove read-only flag
#
# # Check the new file
#
# file = DfsFileFactory.Dfs2FileOpen(filename);
#
# # Check existing data
# itemInfo = file.ItemInfo[0];
# assert_equal("Landuse", itemInfo.Name);
# assert_equal(eumItem.eumIIntegerCode, itemInfo.Quantity.Item);
# assert_equal(eumUnit.eumUintCode, itemInfo.Quantity.Unit);
#
# file.Close();
#
# # Method that modifies the Landuse.dfs2 file
# ExamplesDfs2.ModifyDfs2ItemInfo(filename);
#
# # Check the new file
# file = DfsFileFactory.Dfs2FileOpen(filename);
#
# # Check existing data
# itemInfo = file.ItemInfo[0];
# assert_equal("GroundU", itemInfo.Name);
# assert_equal(eumItem.eumIAreaFraction, itemInfo.Quantity.Item);
# assert_equal(eumUnit.eumUPerCent, itemInfo.Quantity.Unit);
#
# file.Close();
def test_ModifyLanduseItemInfoTest(self):
originalFilename = "testdata/Landuse.dfs2"
filename = "testdata/testtmp/test_modifyItemInfo_Landuse.dfs2"

testUtil.copy_file(originalFilename, filename)

# Check existing data
file = DfsFileFactory.Dfs2FileOpen(filename)
itemInfo = file.ItemInfo[0]
assert_equal("Landuse", itemInfo.Name)
assert_equal(eumItem.eumIIntegerCode, itemInfo.Quantity.Item)
assert_equal(eumUnit.eumUintCode, itemInfo.Quantity.Unit)
file.Close()

# Method that modifies the Landuse.dfs2 file
ExamplesDfs2.ModifyDfs2ItemInfo(filename)

# Check the new file
file = DfsFileFactory.Dfs2FileOpen(filename)
itemInfo = file.ItemInfo[0]
assert_equal("GroundU", itemInfo.Name)
assert_equal(eumItem.eumIAreaFraction, itemInfo.Quantity.Item)
assert_equal(eumUnit.eumUPerCent, itemInfo.Quantity.Unit)
file.Close()

def test_ReadReshapedPastEndReturnsNone(self):
dfsFile = DfsFileFactory.Dfs2FileOpen("testdata/OresundHD.dfs2")
Expand Down
Loading
Loading