Skip to content

Commit 59773d3

Browse files
ryan-kipawaclaude
andcommitted
Write item name and quantity edits on Flush and Close
In a file opened for editing, the file remembers the dynamic items it read. Flush and Close write the ones whose Name changed or that were assigned a new Quantity, through dfsUpdateItemInfo. As in the C# library, a new name is truncated or padded to the stored length, and a Quantity changed in place is not written. Every change is checked before any is written, and Close still closes the file when it raises. Closes #38 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c6a85de commit 59773d3

3 files changed

Lines changed: 105 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- In a dfs file opened for editing, a dynamic item's changed `Name` or newly
13+
assigned `Quantity` is written to the file on `Close`, and in Edit mode
14+
also on `Flush`. As in the
15+
C# library, a new name is truncated or padded with spaces to the length of
16+
the old one, and changing `Quantity.Item` or `Quantity.Unit` in place is not
17+
written. A name that cp1252 cannot encode raises `UnicodeEncodeError`, and
18+
a name containing a NUL character or a unit that is not a unit of the item
19+
raises `ValueError`, before any item is written
20+
([#38](https://github.com/DHI/mikecore-python/issues/38)).
1221
- `DfsuFile.Flush()` flushes the file instead of raising `TypeError`, and
1322
`DfsFileFactory.DfsuFileOpenAppend` no longer requires `parameters`, like the
1423
other `Open` methods.

‎mikecore/DfsDLL.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ def Init(libfilepath: str | None = None):
210210
ctypes.c_void_p,
211211
ctypes.c_void_p,
212212
]
213+
lib.dfsUpdateItemInfo.argtypes = [
214+
ctypes.c_void_p,
215+
ctypes.c_void_p,
216+
ctypes.c_int32,
217+
ctypes.c_char_p,
218+
ctypes.c_int32,
219+
]
220+
lib.dfsUpdateItemInfo.restype = ctypes.c_int32
213221

214222
lib.dfsAddCustomBlock.argtypes = [
215223
ctypes.c_void_p,

‎mikecore/DfsFile.py‎

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,9 @@ def __init__(self):
779779
self.fpTimeStepIndex = 0
780780
self.headPointer = ctypes.c_void_p(0)
781781
self.filePointer = ctypes.c_void_p(0)
782+
# The dynamic items of an editable file, each with the Quantity it was
783+
# read with. Flush and Close write their changes.
784+
self.__itemsRead = []
782785

783786
def __del__(self):
784787
self.Close()
@@ -855,6 +858,8 @@ def Open(self, filename, mode=DfsFileMode.Read, parameters=None):
855858
self.ItemInfo = []
856859
for i in range(noOfItems):
857860
self.ItemInfo.append(self.__DynamicItemInfoReadAndCreate(i + 1, noOfItems))
861+
if mode != DfsFileMode.Read:
862+
self.__itemsRead = [(item, item.Quantity) for item in self.ItemInfo]
858863

859864
if mode == DfsFileMode.Read:
860865
# file pointer is after header part
@@ -894,13 +899,68 @@ def Close(self):
894899
"""
895900
Close the file and release all ressources associated with it. The header information
896901
is still valid (for reading) even though the file has been closed.
902+
903+
In a file opened for editing, a changed item Name or Quantity is
904+
written first, see Flush. If a value is rejected, no item is written,
905+
and the file is still closed.
897906
"""
898-
if self.filePointer.value is not None:
899-
DfsDLL.Wrapper.dfsFileClose(
900-
self.headPointer, ctypes.byref(self.filePointer)
907+
try:
908+
self.__WriteItemInfo()
909+
finally:
910+
self.__itemsRead = []
911+
if self.filePointer.value is not None:
912+
DfsDLL.Wrapper.dfsFileClose(
913+
self.headPointer, ctypes.byref(self.filePointer)
914+
)
915+
if self.headPointer.value is not None:
916+
DfsDLL.Wrapper.dfsHeaderDestroy(ctypes.byref(self.headPointer))
917+
918+
def __WriteItemInfo(self):
919+
"""
920+
Write the Name and Quantity of the dynamic items that changed since
921+
the file was opened. As in the C# library, assigning a new Quantity
922+
writes it, changing its Item or Unit in place does not, and a new name
923+
is truncated or padded with spaces to the length of the name in the
924+
file. Every change is checked before any is written.
925+
"""
926+
changes = []
927+
for item, quantityRead in self.__itemsRead:
928+
nameBytes = DfsDLLUtil.GetItemNameBytes(item.ItemPointer)
929+
nameChanged = item.Name != nameBytes.decode("cp1252", "replace")
930+
if not nameChanged and item.Quantity is quantityRead:
931+
continue
932+
if nameChanged:
933+
# The native library reads the name up to the first NUL
934+
if "\0" in item.Name:
935+
raise ValueError(
936+
f"Name of item {item.ItemNumber} contains a NUL character: "
937+
f"{item.Name!r}"
938+
)
939+
try:
940+
newNameBytes = item.Name.encode("cp1252")
941+
except UnicodeEncodeError as error:
942+
error.add_note(f"Name of item {item.ItemNumber}: {item.Name!r}")
943+
raise
944+
length = len(nameBytes)
945+
nameBytes = newNameBytes[:length].ljust(length, b" ")
946+
quantity = item.Quantity
947+
if not eumWrapper.eumItemUnitEqv(quantity.Item, quantity.Unit):
948+
raise ValueError(
949+
f"Item {item.ItemNumber} has a Quantity whose unit "
950+
f"{quantity.Unit!r} is not a unit of {quantity.Item!r}"
951+
)
952+
changes.append((item, nameBytes, quantity))
953+
for item, nameBytes, quantity in changes:
954+
rok = DfsDLL.Wrapper.dfsUpdateItemInfo(
955+
self.headPointer,
956+
item.ItemPointer,
957+
ctypes.c_int32(quantity.Item),
958+
nameBytes,
959+
ctypes.c_int32(quantity.Unit),
901960
)
902-
if self.headPointer.value is not None:
903-
DfsDLL.Wrapper.dfsHeaderDestroy(ctypes.byref(self.headPointer))
961+
DfsDLL.CheckReturnCode(rok)
962+
item.Name = nameBytes.decode("cp1252", "replace")
963+
self.__itemsRead = [(item, item.Quantity) for item, _ in self.__itemsRead]
904964

905965
def GetNextItemNumber(self):
906966
return self.fpItemNumber
@@ -1296,8 +1356,14 @@ def Flush(self):
12961356
Flush cached data to the file. This will especially update the header information,
12971357
such that if another process is reading the file while it is being read, the new
12981358
header information can be retrieved.
1359+
1360+
In a file opened for editing, a dynamic item's changed Name or newly
1361+
assigned Quantity is written to the header first. If a value is
1362+
rejected, this raises without writing any item. In Append mode, the
1363+
native library writes the header to the file only on Close.
12991364
"""
13001365
self.__CheckIfOpen()
1366+
self.__WriteItemInfo()
13011367
DfsDLL.Wrapper.dfsFileFlush(self.headPointer, self.filePointer)
13021368

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

1641+
@staticmethod
1642+
def GetItemNameBytes(itemPointer) -> bytes:
1643+
"""The name of an item as stored in the header, without decoding."""
1644+
itemNameP = ctypes.c_char_p()
1645+
DfsDLL.Wrapper.dfsGetItemInfo(
1646+
itemPointer,
1647+
ctypes.byref(ctypes.c_int32()),
1648+
ctypes.byref(ctypes.c_char_p()),
1649+
ctypes.byref(itemNameP),
1650+
ctypes.byref(ctypes.c_int32()),
1651+
ctypes.byref(ctypes.c_char_p()),
1652+
ctypes.byref(ctypes.c_int()),
1653+
)
1654+
if itemNameP.value is None:
1655+
raise ValueError("Native library returned a null string")
1656+
return itemNameP.value
1657+
15751658
@staticmethod
15761659
def EncodeFileName(filename: str | os.PathLike[str]) -> bytes:
15771660
# The native library opens char* paths, which Windows interprets in the

0 commit comments

Comments
 (0)