@@ -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