From 996deb2a9532300b1ca0ac78f1cb47e42c1f0656 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:45:33 +0200 Subject: [PATCH 01/11] Apply ruff's safe autofixes for the new correctness and complexity rules Output of `ruff check --fix` followed by `ruff format`, unedited, for the rules the next commits enable: W605 (invalid escape sequence), PLR1730 (if-stmt-min-max), FURB122 (for-loop-writes) and FURB105 (print-empty-string). Co-Authored-By: Claude Opus 5.5 --- buildUtil/eumXMLProcess.py | 8 ++++---- mikecore/DfsuFile.py | 9 +++------ miketools/Dfs0ToAscii.py | 25 ++++++++++++------------- tests/Test.py | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/buildUtil/eumXMLProcess.py b/buildUtil/eumXMLProcess.py index 3083feb..94c9582 100644 --- a/buildUtil/eumXMLProcess.py +++ b/buildUtil/eumXMLProcess.py @@ -16,8 +16,8 @@ eumLines = eumFile.readlines() eumFile.close() -recItem = re.compile('"(eumI\w+)" MzId="(\w*)"') -recUnit = re.compile('"(eumU\w+)" MzId="(\w*)"') +recItem = re.compile(r'"(eumI\w+)" MzId="(\w*)"') +recUnit = re.compile(r'"(eumU\w+)" MzId="(\w*)"') itemDict = {} unitDict = {} @@ -38,12 +38,12 @@ print("# Predefined enums of EUM item types.") print("#") print("# Must be updated with every new release, or if the EUM.xml is updated") -print("# Run buildUtil\eumXMLProcess.py to create the lists") +print(r"# Run buildUtil\eumXMLProcess.py to create the lists") print("class eumItem(IntEnum):") for key in itemKeys: print(f" {itemDict[key]} = {key}") -print("") +print() print("# Predefined enums of EUM units.") print("#") print("# Must be updated with every new release, or if the EUM.xml is updated") diff --git a/mikecore/DfsuFile.py b/mikecore/DfsuFile.py index 1918cef..4e12997 100644 --- a/mikecore/DfsuFile.py +++ b/mikecore/DfsuFile.py @@ -655,8 +655,7 @@ def FindTopLayerElementsXY(elementTable, x, y): dx = x2 - x1 dy = y2 - y1 nodeDist2 = dx * dx + dy * dy - if nodeDist2 > maxNodeDist2: - maxNodeDist2 = nodeDist2 + maxNodeDist2 = max(maxNodeDist2, nodeDist2) # Check if element center coordinates differ more than a tolerance # times the reference lenght - the maximum node distance. @@ -681,8 +680,7 @@ def FindMaxNumberOfLayers(topLayerElements): maxLayers = topLayerElements[0] + 1 for i in range(1, topLayerElements.size): layers = topLayerElements[i] - topLayerElements[i - 1] - if layers > maxLayers: - maxLayers = layers + maxLayers = max(maxLayers, layers) return maxLayers @staticmethod @@ -697,6 +695,5 @@ def FindMinNumberOfLayers(topLayerElements): minLayers = topLayerElements[0] + 1 for i in range(1, topLayerElements.size): layers = topLayerElements[i] - topLayerElements[i - 1] - if layers < minLayers: - minLayers = layers + minLayers = min(minLayers, layers) return minLayers diff --git a/miketools/Dfs0ToAscii.py b/miketools/Dfs0ToAscii.py index bb00dfa..53025d7 100644 --- a/miketools/Dfs0ToAscii.py +++ b/miketools/Dfs0ToAscii.py @@ -14,25 +14,24 @@ def Dfs0ToAscii(dfs0FileName, txtFileName): txt.write("\n") txt.write(" Time") - for item in dfs.ItemInfo: - txt.write(f" {item.Name}") + txt.writelines(f" {item.Name}" for item in dfs.ItemInfo) txt.write("\n") txt.write(" Item") - for item in dfs.ItemInfo: - txt.write( - f" {item.Quantity.Item.value:11} {item.Quantity.Unit.value:11} {item.ValueType.name:11}" - ) + txt.writelines( + f" {item.Quantity.Item.value:11} {item.Quantity.Unit.value:11} {item.ValueType.name:11}" + for item in dfs.ItemInfo + ) txt.write("\n") txt.write(" Item") - for item in dfs.ItemInfo: - txt.write( - " {:11} {:11} {:11}".format( - '"' + item.Quantity.ItemDescription + '"', - '"' + item.Quantity.UnitDescription + '"', - item.ValueType.name, - ) + txt.writelines( + " {:11} {:11} {:11}".format( + '"' + item.Quantity.ItemDescription + '"', + '"' + item.Quantity.UnitDescription + '"', + item.ValueType.name, ) + for item in dfs.ItemInfo + ) txt.write("\n") # Only set for calendar time axes; otherwise times are written as offsets. diff --git a/tests/Test.py b/tests/Test.py index 6ce46bf..7f85e5a 100644 --- a/tests/Test.py +++ b/tests/Test.py @@ -1,5 +1,5 @@ # To run all test in a Python environment: import sys -sys.path.insert(0, "C:\Work\python\mikecore") +sys.path.insert(0, r"C:\Work\python\mikecore") import tests.test_run_all From f9e877b57308ef2e2649797bc73f84ee490a6190 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:46:37 +0200 Subject: [PATCH 02/11] Test that reading an item's value type reports a DFS error DfsDLLUtil.dfsGetItemValueType ignores the native return code, so an invalid item pointer yields value type 0 instead of an error. Fails until the next commit. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfs_basic.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_dfs_basic.py b/tests/test_dfs_basic.py index 497b012..fb70f39 100644 --- a/tests/test_dfs_basic.py +++ b/tests/test_dfs_basic.py @@ -1,7 +1,9 @@ +import ctypes + import pytest from mikecore.DfsDLL import DfsDLL -from mikecore.DfsFile import DfsFile, DfsFileMode, TimeAxisType +from mikecore.DfsFile import DfsDLLUtil, DfsFile, DfsFileMode, TimeAxisType from mikecore.DfsFileFactory import DfsFileFactory @@ -58,6 +60,11 @@ def test_error_reporting(): print("Exception:", e) +def test_item_value_type_reports_dfs_error(): + with pytest.raises(Exception, match="DFS error code 1000"): + DfsDLLUtil.dfsGetItemValueType(ctypes.c_void_p()) + + def test_open_with_integer_mode(): dfs = DfsFile() From 475c6203a447cbdc43b492f8c961367f9e7f48c8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:46:49 +0200 Subject: [PATCH 03/11] Check the return code when reading an item's value type Every other DfsDLLUtil wrapper passes the native return code to DfsDLL.CheckReturnCode; dfsGetItemValueType stored it in an unused variable (ruff F841), so a failed read returned value type 0 silently. Co-Authored-By: Claude Opus 5.5 --- mikecore/DfsFile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mikecore/DfsFile.py b/mikecore/DfsFile.py index d6fbd5f..23c6f19 100644 --- a/mikecore/DfsFile.py +++ b/mikecore/DfsFile.py @@ -2112,7 +2112,8 @@ def ToDfsDateStrings(datetime): @staticmethod def dfsGetItemValueType(itemPointer): valueTypeInt = ctypes.c_int() - err = DfsDLL.Wrapper.dfsGetItemValueType( + rok = DfsDLL.Wrapper.dfsGetItemValueType( itemPointer, ctypes.byref(valueTypeInt) ) + DfsDLL.CheckReturnCode(rok) return DataValueType(valueTypeInt.value) From b68746411b3d1fe2ed315e5fdbb533eb16490560 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:48:20 +0200 Subject: [PATCH 04/11] Assert the values the reading examples only read ReadingDfs2File and ReadingDfsuFile read header values into variables they never used (ruff F841), with the expected value only in a comment. Assert those values so the example tests check them, and drop the unused timeSpan and ok results in the dfsu examples. Co-Authored-By: Claude Opus 5.5 --- tests/examples_dfs2.py | 4 ++++ tests/examples_dfsu.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/examples_dfs2.py b/tests/examples_dfs2.py index fd45ea2..a7229c7 100644 --- a/tests/examples_dfs2.py +++ b/tests/examples_dfs2.py @@ -16,16 +16,20 @@ def ReadingDfs2File(filename): axisEqD2 = dfs2File.SpatialAxis dx = axisEqD2.Dx # 900 dy = axisEqD2.Dy # 900 + assert (dx, dy) == (900, 900) # Header information is contained in the IDfsFileInfo fileInfo = dfs2File.FileInfo steps = fileInfo.TimeAxis.NumberOfTimeSteps # 13 projectionString = fileInfo.Projection.WKTString # "UTM-33" + assert projectionString == "UTM-33" # Information on each of the dynamic items, here the first one dynamicItemInfo = dfs2File.ItemInfo[0] nameOfFirstDynamicItem = dynamicItemInfo.Name # "H Water Depth m" typeOfFirstDynamicItem = dynamicItemInfo.DataType # Float + assert nameOfFirstDynamicItem == "H Water Depth m" + assert typeOfFirstDynamicItem == DfsSimpleType.Float # Read data of first item, third time step (items start by 1, timesteps by 0), # assuming data is of type float. diff --git a/tests/examples_dfsu.py b/tests/examples_dfsu.py index 312aace..17466e6 100644 --- a/tests/examples_dfsu.py +++ b/tests/examples_dfsu.py @@ -23,15 +23,22 @@ def ReadingDfsuFile(filename): numberOfNodes = file.NumberOfNodes # 2057 firstNodeXCoordinate = file.X[0] # 359978.8 firstElementNodes = file.ElementTable[0] # [1, 2, 3] + assert (numberOfElements, numberOfNodes) == (3636, 2057) + assert math.isclose(firstNodeXCoordinate, 359978.8, abs_tol=0.1) + assert list(firstElementNodes) == [1, 2, 3] # Read dynamic item info firstItemName = file.ItemInfo[0].Name # "Surface elevation" quantity = file.ItemInfo[0].Quantity # eumISurfaceElevation in eumUmeter + assert firstItemName == "Surface elevation" + assert quantity.Item == eumItem.eumISurfaceElevation + assert quantity.Unit == eumUnit.eumUmeter # load data for the first item, 6th timestep itemTimeStepData = file.ReadItemTimeStep(1, 5).Data # Read the value of the third element thirdElementValue = itemTimeStepData[2] # 0.0014070312 + assert math.isclose(thirdElementValue, 0.0014070312, rel_tol=1e-6) file.Close() @@ -158,7 +165,6 @@ def ExtractDfs0FromDfsu(dfsuFileNamePath, elmtsIndices): # Create a dfs0 file for each element in elmtsIndices dfs0Files = [] - timeSpan = source.TimeStepInSeconds * source.NumberOfTimeSteps for k in range(elmtsIndices.size): # Index of element to create dfs0 for elmtsIndex = elmtsIndices[k] @@ -216,7 +222,7 @@ def ExtractDfs0FromDfsu(dfsuFileNamePath, elmtsIndices): for i in range(source.NumberOfTimeSteps): for j in range(len(source.ItemInfo)): dfsuItemData = dfsuItemDatas[j] - ok = source.ReadItemTimeStep(dfsuItemData, i) + source.ReadItemTimeStep(dfsuItemData, i) floats = dfsuItemData.Data # write data to dfs0's @@ -413,7 +419,7 @@ def ExtractDfsu2DLayerFrom3D(filenameDfsu3, filenameDfsu2, layerNumber): for j in range(len(dfsu3File.ItemInfo)): # Read data from 3D dfsu data3Item = dfsu3ItemDatas[j] - ok = dfsu3File.ReadItemTimeStep(data3Item, i) + dfsu3File.ReadItemTimeStep(data3Item, i) # 3D data data3 = data3Item.Data From b7b8de4ee9f57786d43e1ad6f64076531ca7c1d6 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:48:27 +0200 Subject: [PATCH 05/11] Remove unused variables Ruff F841 leftovers: static items DfsuBuilder creates for their side effect, the result of eumSetupLoadLinux, a calendar flag DfsShowInfo never reads, and files the builder tests fetch only to trigger GetFile. Co-Authored-By: Claude Opus 5.5 --- mikecore/DfsuBuilder.py | 12 +++--------- mikecore/eum.py | 2 +- miketools/DfsShowInfo.py | 2 -- tests/test_dfsbuilder.py | 8 ++++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/mikecore/DfsuBuilder.py b/mikecore/DfsuBuilder.py index adbf75e..5ec6ffd 100644 --- a/mikecore/DfsuBuilder.py +++ b/mikecore/DfsuBuilder.py @@ -730,19 +730,13 @@ def CreateDfsu(self, dfsBuilder, elementType, nodesPerElmt, connectivityArray): ) # Element type - elmtTypeItem = dfsBuilder.AddCreateStaticItem( - "Element type", intCode, elementType - ) + dfsBuilder.AddCreateStaticItem("Element type", intCode, elementType) # No of nodes (per element) - nodesPerElmtItem = dfsBuilder.AddCreateStaticItem( - "No of nodes", intCode, nodesPerElmt - ) + dfsBuilder.AddCreateStaticItem("No of nodes", intCode, nodesPerElmt) # Connectivity - connectivityItem = dfsBuilder.AddCreateStaticItem( - "Connectivity", intCode, connectivityArray - ) + dfsBuilder.AddCreateStaticItem("Connectivity", intCode, connectivityArray) # Spectral frequencyItem = ( diff --git a/mikecore/eum.py b/mikecore/eum.py index b21117d..89bcf66 100644 --- a/mikecore/eum.py +++ b/mikecore/eum.py @@ -1452,7 +1452,7 @@ def Init(libfilepath=None, libfilename=None): # TODO: Should this not be simpler? eumFilePath = path + "/EUM.xml" eumFilePathP = ctypes.c_char_p(eumFilePath.encode("ascii")) - res = lib.eumSetupLoadLinux(eumFilePathP) + lib.eumSetupLoadLinux(eumFilePathP) lib.eumUnitGetParameters.argtypes = [ ctypes.c_int32, diff --git a/miketools/DfsShowInfo.py b/miketools/DfsShowInfo.py index 6845924..288a6c0 100644 --- a/miketools/DfsShowInfo.py +++ b/miketools/DfsShowInfo.py @@ -22,7 +22,6 @@ def DfsShowInfo(dfsFileName, showAxis=False): dfs.FileInfo.TimeAxis.TimeAxisType == TimeAxisType.CalendarEquidistant or dfs.FileInfo.TimeAxis.TimeAxisType == TimeAxisType.CalendarNonEquidistant ): - isCalendarTime = True startDateTime = dfs.FileInfo.TimeAxis.StartDateTime txt.write( f"Time : {dfs.FileInfo.TimeAxis.TimeAxisType}, {dfs.FileInfo.TimeAxis.NumberOfTimeSteps}, {startDateTime}\n" @@ -74,7 +73,6 @@ def DfsuShowInfo(dfsFileName, showMesh=False): dfs.FileInfo.TimeAxis.TimeAxisType == TimeAxisType.CalendarEquidistant or dfs.FileInfo.TimeAxis.TimeAxisType == TimeAxisType.CalendarNonEquidistant ): - isCalendarTime = True startDateTime = dfs.FileInfo.TimeAxis.StartDateTime txt.write( f"Time : {dfs.FileInfo.TimeAxis.TimeAxisType}, {dfs.FileInfo.TimeAxis.NumberOfTimeSteps}, {startDateTime}\n" diff --git a/tests/test_dfsbuilder.py b/tests/test_dfsbuilder.py index 82263b8..681b77f 100644 --- a/tests/test_dfsbuilder.py +++ b/tests/test_dfsbuilder.py @@ -58,7 +58,7 @@ def test_validate_empty_builder(): def test_empty_title(landuse: DfsBuilder, tmp_path): # Create and get file landuse.CreateFile(str(tmp_path / "notused.dfs2")) - file = landuse.GetFile() + landuse.GetFile() def test_getfile_not_possible_if_not_created(landuse: DfsBuilder): @@ -75,7 +75,7 @@ def test_getfiletwice_not_allowed(landuse: DfsBuilder, tmp_path): # Create and get file builder.CreateFile(str(tmp_path / "notused.dfs2")) - file = builder.GetFile() + builder.GetFile() with pytest.raises(Exception, match="File has been returned"): builder.GetFile() @@ -86,7 +86,7 @@ def test_filetitle_after_create_not_possible(landuse: DfsBuilder, tmp_path): # Create and get file builder.CreateFile(str(tmp_path / "notused.dfs2")) - file = builder.GetFile() + builder.GetFile() with pytest.raises(Exception, match="File has been returned"): builder.SetFileTitle("too late") @@ -97,7 +97,7 @@ def test_apptitle_after_create_not_possible(landuse: DfsBuilder, tmp_path): # Create and get file builder.CreateFile(str(tmp_path / "notused.dfs2")) - file = builder.GetFile() + builder.GetFile() with pytest.raises(Exception, match="File has been returned"): builder.SetApplicationTitle("too late") From 79a4b464e50a2237910d99eb1be0ddf60369ce1f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:48:51 +0200 Subject: [PATCH 06/11] Apply ruff's unsafe autofixes for TRY201, C408 and SIM102 Output of `ruff check --fix --unsafe-fixes` for TRY201 (verbose-raise), C408 (unnecessary dict() call) and the one SIM102 (collapsible-if) ruff could rewrite, plus its F841 fix for the exception names a bare `raise` no longer needs. Each was read: `raise` re-raises the same exception and traceback as `raise e`, `{}` equals `dict()`, and the merged condition in MapProjection.__del__ short-circuits the same way. Co-Authored-By: Claude Opus 5.5 --- mikecore/DfsBuilder.py | 8 ++++---- mikecore/Projections.py | 5 ++--- mikecore/eum.py | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mikecore/DfsBuilder.py b/mikecore/DfsBuilder.py index 9b48cbd..afee788 100644 --- a/mikecore/DfsBuilder.py +++ b/mikecore/DfsBuilder.py @@ -329,11 +329,11 @@ def CreateFile(self, filename): fnp.value, headerPointer, ctypes.byref(filePointer) ) - except Exception as e: + except Exception: # In case of any exception, destroy the header. if headerPointer.value is not None: DfsDLL.Wrapper.dfsHeaderDestroy(ctypes.byref(headerPointer)) - raise e + raise # The create function will free ressources, if something fails. dfsFile = DfsFile() @@ -468,12 +468,12 @@ def AddStaticItem(self, staticItem): DfsBuilder.__SetValuesToItem(dfsFile.headPointer, itemPointer, staticItem) # From now on the responsibility of the staticVectorPointer is taken over bye the DfsStaticItem (ending the try-catch) - except Exception as e: + except Exception: # As long as the static vector pointer is not null, the # responsibility for destroying the header structure is here. if staticVectorPointer.value is not None: DfsDLL.Wrapper.dfsStaticDestroy(ctypes.byref(staticVectorPointer)) - raise e + raise # Create a DfsStaticItem that belongs to the current dfs file myStaticItem = DfsStaticItem(dfsFile, staticVectorPointer, itemPointer) diff --git a/mikecore/Projections.py b/mikecore/Projections.py index 6b5d859..84aa7ad 100644 --- a/mikecore/Projections.py +++ b/mikecore/Projections.py @@ -1137,9 +1137,8 @@ def CreatePointer( # / def __del__(self): # Release ressources on the unmanaged side when garbage collected - if self._mustFree: - if self._mzMapProjPointer.value is not None: - MzCartDLL.MzMapProjDestroy(self._mzMapProjPointer) + if self._mustFree and self._mzMapProjPointer.value is not None: + MzCartDLL.MzMapProjDestroy(self._mzMapProjPointer) _mzMapProjPointer = ctypes.c_void_p() ##/ diff --git a/mikecore/eum.py b/mikecore/eum.py index 89bcf66..9624ca1 100644 --- a/mikecore/eum.py +++ b/mikecore/eum.py @@ -1536,7 +1536,7 @@ def DecodeString( # / A hashtable with item type text description, numeric key pairs. @staticmethod def CreateItemHashtable() -> dict: - itemHash = dict() + itemHash = {} n = eumWrapper.eumGetItemTypeCount() for i in range(1, n + 1): ok, key, desc = eumWrapper.eumGetItemTypeSeq(i) @@ -1554,7 +1554,7 @@ def CreateItemHashtable() -> dict: # / @staticmethod def CreateUnitHashTable(abbreviations: bool = False): - unitHash = dict() # current number of unique desc/abbrs. + unitHash = {} # current number of unique desc/abbrs. prevKey = eumUnit.eumUUnitUndefined ok, unitKey, desc = eumWrapper.eumGetNextUnit(prevKey) while ok: From 684bd2f32bd4403566bbe38871e90c6912f211d1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:49:58 +0200 Subject: [PATCH 07/11] Collapse nested ifs that ruff could not rewrite SIM102 (collapsible-if), by hand where ruff offers no fix: each `if A:` holding only `if B:` becomes `if A and B:`. In ReadDfs0DataDouble the equidistant time and calendar branches did the same thing, so they become one condition. Evaluation order is unchanged, so __FpDynamicIncrement still runs on every write. Co-Authored-By: Claude Opus 5.5 --- mikecore/DfsFile.py | 43 ++++++++++++++++++++-------------------- mikecore/DfsuFile.py | 9 ++++----- mikecore/Projections.py | 20 ++++++++----------- miketools/DfsShowInfo.py | 9 ++++----- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/mikecore/DfsFile.py b/mikecore/DfsFile.py index 23c6f19..fc80ea9 100644 --- a/mikecore/DfsFile.py +++ b/mikecore/DfsFile.py @@ -1167,16 +1167,15 @@ def WriteItemTimeStep(self, itemNumber, timestepIndex, time, data): # More elaborate action is required, when appending to file. If appending then # (itemNumber == _fpItemNumber && timestepIndex == _fileInfo.TimeAxis.NumberOfTimeSteps == _fpTimeStepIndex) - if timestepIndex == self.FileInfo.TimeAxis.NumberOfTimeSteps: - if ( - timestepIndex == self.fpTimeStepIndex - and itemNumber != self.fpItemNumber - or timestepIndex != self.fpTimeStepIndex - and itemNumber != 1 - ): - raise IndexError( - "Wrong item number while trying to append item data to file. Item data must be appended in order, and for all items in the time step" - ) + if timestepIndex == self.FileInfo.TimeAxis.NumberOfTimeSteps and ( + timestepIndex == self.fpTimeStepIndex + and itemNumber != self.fpItemNumber + or timestepIndex != self.fpTimeStepIndex + and itemNumber != 1 + ): + raise IndexError( + "Wrong item number while trying to append item data to file. Item data must be appended in order, and for all items in the time step" + ) # Position file pointer self.__FpFindItemTimeStep(itemNumber, timestepIndex) @@ -1249,11 +1248,13 @@ def WriteItemTimeStepNext(self, time, data): self.headPointer, self.filePointer, ctypes.c_double(time), data.ctypes.data ) - if self.__FpDynamicIncrement(): - if self.fpTimeStepIndex > self.FileInfo.TimeAxis.NumberOfTimeSteps: - # One entire time step (all items) has just been written to file, - # increment the number of time steps - self.FileInfo.TimeAxis.IncrementNumberOfTimeSteps(time) + if ( + self.__FpDynamicIncrement() + and self.fpTimeStepIndex > self.FileInfo.TimeAxis.NumberOfTimeSteps + ): + # One entire time step (all items) has just been written to file, + # increment the number of time steps + self.FileInfo.TimeAxis.IncrementNumberOfTimeSteps(time) def Reset(self): """ @@ -1365,12 +1366,12 @@ def ReadDfs0DataDouble(self, itemsToLoad=None): # Need to handle StartTimeOffset similar to in __GetTime timeaxis = self.FileInfo.TimeAxis - if timeaxis.TimeAxisType == TimeAxisType.TimeEquidistant: - if timeaxis.StartTimeOffset != 0.0: - data[:, 0] = data[:, 0] + timeaxis.StartTimeOffset - elif timeaxis.TimeAxisType == TimeAxisType.CalendarEquidistant: - if timeaxis.StartTimeOffset != 0.0: - data[:, 0] = data[:, 0] + timeaxis.StartTimeOffset + if ( + timeaxis.TimeAxisType + in (TimeAxisType.TimeEquidistant, TimeAxisType.CalendarEquidistant) + and timeaxis.StartTimeOffset != 0.0 + ): + data[:, 0] = data[:, 0] + timeaxis.StartTimeOffset return data diff --git a/mikecore/DfsuFile.py b/mikecore/DfsuFile.py index 4e12997..9e8883d 100644 --- a/mikecore/DfsuFile.py +++ b/mikecore/DfsuFile.py @@ -272,11 +272,10 @@ def __Init(self, dfsFile, build=False): self.NumberOfElements = self.ElementIds.size # self.NumberOfNodes = self.X.size - if not build: - # In append mode, move the file pointer to end of file - # It was moved elsewhere when reading static data... - if self.dfsFile.FileMode == DfsFileMode.Append: - self.dfsFile.FindTimeStep(self.NumberOfTimeSteps) + # In append mode, move the file pointer to end of file + # It was moved elsewhere when reading static data... + if not build and self.dfsFile.FileMode == DfsFileMode.Append: + self.dfsFile.FindTimeStep(self.NumberOfTimeSteps) def Dispose(self): """ diff --git a/mikecore/Projections.py b/mikecore/Projections.py index 84aa7ad..37df105 100644 --- a/mikecore/Projections.py +++ b/mikecore/Projections.py @@ -1109,9 +1109,8 @@ def Create(projectionString: str, validateProjectionString: bool = True): raise Exception( "Projection string cannot be null or empty", "projectionString" ) - if validateProjectionString: - if not MapProjection.IsValid(projectionString): - raise Exception("Not a valid projection string", "projectionString") + if validateProjectionString and not MapProjection.IsValid(projectionString): + raise Exception("Not a valid projection string", "projectionString") return MapProjection(projectionString, mustFree=True) # / @@ -1522,9 +1521,8 @@ def __init__( self._mzCartPointer = ctypes.c_void_p(0) - if validateProjectionString: - if not MapProjection.IsValid(projectionString): - raise Exception("Not a valid projection string", "projectionString") + if validateProjectionString and not MapProjection.IsValid(projectionString): + raise Exception("Not a valid projection string", "projectionString") if lonOrigin is None: lonOrigin, latOrigin = MapProjection.ProjectionOrigin( @@ -1696,9 +1694,8 @@ def CreateGeoOrigin( ): if projectionString is None or projectionString == "": raise ValueError("Projection string can not be null or empty") - if validateProjectionString: - if not MapProjection.IsValid(projectionString): - raise ValueError("Not a valid projection string") + if validateProjectionString and not MapProjection.IsValid(projectionString): + raise ValueError("Not a valid projection string") cart = Cartography( projectionString, lonOrigin, @@ -1732,9 +1729,8 @@ def CreateProjOrigin( ): if projectionString is None or projectionString == "": raise ValueError("Projection string cannot be null or empty") - if validateProjectionString: - if not MapProjection.IsValid(projectionString): - raise ValueError("Not a valid projection string") + if validateProjectionString and not MapProjection.IsValid(projectionString): + raise ValueError("Not a valid projection string") proj = MapProjection.Create(projectionString, validateProjectionString) lonOrigin, latOrigin = proj.Proj2Geo(east, north) orientationGeo = proj.Proj2GeoRotation(east, north, orientationProj) diff --git a/miketools/DfsShowInfo.py b/miketools/DfsShowInfo.py index 288a6c0..25b189f 100644 --- a/miketools/DfsShowInfo.py +++ b/miketools/DfsShowInfo.py @@ -46,11 +46,10 @@ def DfsShowInfo(dfsFileName, showAxis=False): txt.write( f"item {item.ItemNumber!s:>2}: {item.Name!s:<40}: {item.ElementCount!s:>5}: {item.DataType.name!s:>6} {item.SpatialAxis.AxisType.name!s:>6} ({item.Quantity.ItemDescription!s:>11}: {item.Quantity.UnitDescription!s:>11}) \n" ) - if showAxis: - if item.SpatialAxis.AxisType is SpaceAxisType.EqD1: - txt.write( - f" axis : dx = {item.SpatialAxis.Dx}, xCount = {item.SpatialAxis.XCount}, x0 = {item.SpatialAxis.X0} ({item.SpatialAxis.AxisUnit.name})\n" - ) + if showAxis and item.SpatialAxis.AxisType is SpaceAxisType.EqD1: + txt.write( + f" axis : dx = {item.SpatialAxis.Dx}, xCount = {item.SpatialAxis.XCount}, x0 = {item.SpatialAxis.X0} ({item.SpatialAxis.AxisUnit.name})\n" + ) dfs.Close() From c0a899d90024246dae45758a6094e2445ddd3743 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:50:30 +0200 Subject: [PATCH 08/11] Fail tests with self.fail and combine nested with statements PLR0133 (comparison-of-constant): `assert "message" == ""` was a C#-style Assert.Fail; self.fail says so and keeps the message. SIM117: one `with` for the subtest and the expected error. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfs_static_item.py | 4 ++-- tests/test_dfsu2D.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_dfs_static_item.py b/tests/test_dfs_static_item.py index 701e902..5217b67 100644 --- a/tests/test_dfs_static_item.py +++ b/tests/test_dfs_static_item.py @@ -32,13 +32,13 @@ def test_static_item(self): elif eumItem.eumIGeographicalCoordinate == staticItem.Quantity.Item: assert eumUnit.eumUmeter == staticItem.Quantity.Unit else: - assert "X coordinate axis item type mismatch" == "" + self.fail("X coordinate axis item type mismatch") if ( staticItem.DataType != DfsSimpleType.Double and staticItem.DataType != DfsSimpleType.Float ): - assert "DataType of X static item mismatch" == "" + self.fail("DataType of X static item mismatch") # assert deleteValueFloat == staticItem.ReferenceCoordinateX # assert deleteValueFloat == staticItem.ReferenceCoordinateY diff --git a/tests/test_dfsu2D.py b/tests/test_dfsu2D.py index 3eac70e..5747cd9 100644 --- a/tests/test_dfsu2D.py +++ b/tests/test_dfsu2D.py @@ -447,13 +447,15 @@ def test_ExtractDfsu2DLayerFrom3DLayerOutOfRange(self): numberOfLayers = dfsu3.NumberOfLayers dfsu3.Close() for layerNumber in [0, numberOfLayers + 1, -(numberOfLayers + 1)]: - with self.subTest(layerNumber=layerNumber): - with self.assertRaisesRegex(Exception, "Layer number is out of range"): - ExamplesDfsu.ExtractDfsu2DLayerFrom3D( - "testdata/OdenseHD3D.dfsu", - "testdata/testtmp/test_OdenseHD3D_outofrange.dfsu", - layerNumber, - ) + with ( + self.subTest(layerNumber=layerNumber), + self.assertRaisesRegex(Exception, "Layer number is out of range"), + ): + ExamplesDfsu.ExtractDfsu2DLayerFrom3D( + "testdata/OdenseHD3D.dfsu", + "testdata/testtmp/test_OdenseHD3D_outofrange.dfsu", + layerNumber, + ) def test_ExtractDfsu2DLayerFrom2DFails(self): with self.assertRaisesRegex(Exception, "not a 3D dfsu file"): From 5581cb59ea2ee676dab0ccf1c12d2552c06a9cd5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:50:59 +0200 Subject: [PATCH 09/11] Note the item value type fix in the changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 541400b..073204d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,5 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 it, instead of raising `IndexError`. The native library ignores the key. - `DfsFile.WriteItemTimeStepNext` reports data shorter than the item with its size, instead of raising `AttributeError`. +- Reading an item's value type raises the DFS error when the native library + reports one, instead of returning `DataValueType.Instantaneous`. [Unreleased]: https://github.com/DHI/mikecore-python/compare/v0.3.a1...HEAD From 594dc329c489deebd61f17e9a3efc79e8eb18e3f Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 09:51:00 +0200 Subject: [PATCH 10/11] Enable ruff's correctness, complexity and performance rules Select every rule ruff 0.16.10 marks stable and tags correctness, complexity or performance in its rule metadata (`ruff rule --all --output-format json`, field "category"). F, UP, B and E9 already carried theirs; the other 165 are listed by code, grouped by linter, because ruff has no category selector. F841 leaves the ignore list now that the earlier commits removed every unused variable. TID255 is preview in 0.16.10, so it waits for a ruff bump. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 65 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4e1a816..923ec68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,69 @@ line-ending = "lf" # ruff takes its target version from requires-python. [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "UP", "B"] +select = [ + "E4", "E7", "E9", "F", "UP", "B", + # The other stable rules ruff tags correctness, complexity or performance + # (the "category" field of `ruff rule --all --output-format json`). F, UP, B and + # E9 already hold theirs. TID255 joins once CI's ruff has it as stable. + + # flake8-comprehensions + "C400", "C401", "C402", "C403", "C404", "C405", "C406", "C408", "C409", "C410", + "C411", "C413", "C414", "C415", "C417", "C418", "C419", "C420", + # flake8-executable + "EXE002", "EXE004", + # flake8-future-annotations + "FA102", + # FastAPI + "FAST001", + # flynt + "FLY002", + # refurb + "FURB105", "FURB110", "FURB122", "FURB129", "FURB132", "FURB136", "FURB157", + "FURB161", "FURB162", "FURB163", "FURB166", "FURB168", "FURB169", "FURB171", + "FURB177", "FURB181", "FURB188", "FURB192", + # flake8-logging-format + "G101", "G201", + # flake8-logging + "LOG001", + # pandas-vet + "PD012", + # Perflint + "PERF101", "PERF102", "PERF402", + # flake8-pie + "PIE794", "PIE800", "PIE807", "PIE808", "PIE810", + # Pylint + "PLC0131", "PLC0206", "PLC0207", "PLC0208", "PLC0414", "PLC3002", "PLE0100", + "PLE0101", "PLE0115", "PLE0116", "PLE0117", "PLE0118", "PLE0241", "PLE0604", + "PLE0605", "PLE0643", "PLE1132", "PLE1142", "PLE1205", "PLE1206", "PLE1300", + "PLE1307", "PLE1310", "PLE1507", "PLE1700", "PLR0133", "PLR0206", "PLR0402", + "PLR1708", "PLR1711", "PLR1730", "PLR1733", "PLR1736", "PLW0128", "PLW0129", + "PLW0131", "PLW0133", "PLW0177", "PLW0245", "PLW0604", "PLW1501", "PLW1507", + "PLW2101", + # flake8-pytest-style + "PT008", "PT010", "PT014", "PT024", "PT025", "PT026", + # flake8-use-pathlib + "PTH210", + # flake8-pyi + "PYI010", "PYI012", "PYI013", "PYI016", "PYI019", "PYI030", "PYI035", "PYI041", + "PYI044", "PYI045", "PYI048", "PYI055", "PYI059", "PYI064", + # Ruff-specific rules + "RUF007", "RUF010", "RUF015", "RUF016", "RUF017", "RUF019", "RUF020", "RUF026", + "RUF030", "RUF034", "RUF037", "RUF046", "RUF049", "RUF051", "RUF053", "RUF057", + "RUF058", "RUF060", "RUF068", "RUF200", + # flake8-simplify + "SIM101", "SIM102", "SIM103", "SIM107", "SIM110", "SIM113", "SIM117", "SIM118", + "SIM201", "SIM202", "SIM208", "SIM210", "SIM211", "SIM220", "SIM221", "SIM222", + "SIM223", "SIM401", "SIM905", "SIM911", + # flake8-debugger + "T100", + # flake8-type-checking + "TC004", "TC007", "TC010", + # tryceratops + "TRY201", "TRY203", "TRY401", + # pycodestyle + "W605", +] # The C#-style API is re-exported through star imports throughout. ignore = [ "F403", "F405", @@ -79,7 +141,6 @@ ignore = [ "E731", # lambda assignment "E741", # ambiguous variable name "F811", # duplicated test helpers whose bodies differ - "F841", # unused variables # Defaults like DfsParameters() and np.float32(...) are never mutated. "B008", ] From 3fd99405aa03e46476604d11e02ffb90b16be672 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 13:18:13 +0200 Subject: [PATCH 11/11] Leave out rules for frameworks mikecore does not use FAST001 (FastAPI) and PD012 (pandas-vet) can never fire here, as nothing imports either library. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 923ec68..a858f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,8 @@ select = [ "E4", "E7", "E9", "F", "UP", "B", # The other stable rules ruff tags correctness, complexity or performance # (the "category" field of `ruff rule --all --output-format json`). F, UP, B and - # E9 already hold theirs. TID255 joins once CI's ruff has it as stable. + # E9 already hold theirs. Rules for frameworks mikecore does not use (FastAPI, + # pandas) are left out. TID255 joins once CI's ruff has it as stable. # flake8-comprehensions "C400", "C401", "C402", "C403", "C404", "C405", "C406", "C408", "C409", "C410", @@ -82,8 +83,6 @@ select = [ "EXE002", "EXE004", # flake8-future-annotations "FA102", - # FastAPI - "FAST001", # flynt "FLY002", # refurb @@ -94,8 +93,6 @@ select = [ "G101", "G201", # flake8-logging "LOG001", - # pandas-vet - "PD012", # Perflint "PERF101", "PERF102", "PERF402", # flake8-pie