Skip to content
9 changes: 5 additions & 4 deletions src/mikeio/dfs/_dfs1.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ class Dfs1(_Dfs123):
def __init__(self, filename: str | Path) -> None:
super().__init__(filename)

self._dfs = DfsFileFactory.Dfs1FileOpen(str(filename))
self._x0: float = self._dfs.SpatialAxis.X0
self._dx: float = self._dfs.SpatialAxis.Dx
self._nx: int = self._dfs.SpatialAxis.XCount
dfs = DfsFileFactory.Dfs1FileOpen(str(filename))
self._x0: float = dfs.SpatialAxis.X0
self._dx: float = dfs.SpatialAxis.Dx
self._nx: int = dfs.SpatialAxis.XCount
dfs.Close()

origin = self._longitude, self._latitude
self._geometry = Grid1D(
Expand Down
25 changes: 12 additions & 13 deletions src/mikeio/dfs/_dfs3.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,20 @@ def __init__(self, filename: str | Path):
)

def _read_dfs3_header(self, read_x0y0z0: bool = False) -> None:
self._dfs = DfsFileFactory.Dfs3FileOpen(self._filename)

self._source = self._dfs
dfs = DfsFileFactory.Dfs3FileOpen(self._filename)

if read_x0y0z0:
self._x0 = self._dfs.SpatialAxis.X0
self._y0 = self._dfs.SpatialAxis.Y0
self._z0 = self._dfs.SpatialAxis.Z0

self._dx = self._dfs.SpatialAxis.Dx
self._dy = self._dfs.SpatialAxis.Dy
self._dz = self._dfs.SpatialAxis.Dz
self._nx = self._dfs.SpatialAxis.XCount
self._ny = self._dfs.SpatialAxis.YCount
self._nz = self._dfs.SpatialAxis.ZCount
self._x0 = dfs.SpatialAxis.X0
self._y0 = dfs.SpatialAxis.Y0
self._z0 = dfs.SpatialAxis.Z0

self._dx = dfs.SpatialAxis.Dx
self._dy = dfs.SpatialAxis.Dy
self._dz = dfs.SpatialAxis.Dz
self._nx = dfs.SpatialAxis.XCount
self._ny = dfs.SpatialAxis.YCount
self._nz = dfs.SpatialAxis.ZCount
dfs.Close()

def read(
self,
Expand Down
1 change: 1 addition & 0 deletions src/mikeio/dfsu/_dfsu.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ def _dfs_read_item_time_func(
) -> tuple[np.ndarray, pd.Timestamp]:
dfs = DfsuFile.Open(self._filename)
itemdata = dfs.ReadItemTimeStep(item + 1, step)
dfs.Close()

return itemdata.Data, itemdata.Time

Expand Down
4 changes: 4 additions & 0 deletions src/mikeio/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,8 @@ def avg_time(
outdatalist[item][has_value] += d[has_value]
steps_list[item][has_value] += 1

dfs_i.Close()

for item in range(n_items):
darray = np.zeros_like(outdatalist[item], dtype=np.float32)
if skipna:
Expand Down Expand Up @@ -945,6 +947,8 @@ def quantile(
# TODO should this be static Z coordinates instead?
dfs_o.WriteItemTimeStepNext(0.0, znitemdata.Data)

dfs_i.Close()

for item in range(n_items_out):
darray = outdatalist[item].astype(np.float32)
dfs_o.WriteItemTimeStepNext(0.0, darray)
Expand Down
48 changes: 48 additions & 0 deletions tests/test_file_handle_close.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests verifying that file handles are properly closed after use."""

import gc
import os
import platform

import pytest

import mikeio


def _count_open_fds() -> int:
"""Count open file descriptors on Linux via /proc/self/fd."""
return len(os.listdir("/proc/self/fd"))


pytestmark = pytest.mark.skipif(
platform.system() != "Linux",
reason="File descriptor counting via /proc only works on Linux",
)


def test_dfs1_init_closes_file_handle() -> None:
gc.collect()
baseline = _count_open_fds()
for _ in range(50):
mikeio.Dfs1("tests/testdata/random.dfs1")
gc.collect()
assert _count_open_fds() - baseline == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that _count_open_fds() is actually working as expected

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_count_open_fds_sanity_check in 8f56eff which verifies that _count_open_fds() detects mikecore file handles: opening a DfsFile increases the FD count by 1, and closing it restores it. Also updated the other tests to hold references to the created instances (preventing GC from masking the leak) and removed gc.collect() before assertions so leaks are caught deterministically.



def test_dfs3_init_closes_file_handle() -> None:
gc.collect()
baseline = _count_open_fds()
for _ in range(50):
mikeio.Dfs3("tests/testdata/Grid1.dfs3")
gc.collect()
assert _count_open_fds() - baseline == 0


def test_dfsu_read_closes_file_handle() -> None:
gc.collect()
baseline = _count_open_fds()
for _ in range(50):
dfs = mikeio.open("tests/testdata/HD2D.dfsu")
dfs.read()
gc.collect()
assert _count_open_fds() - baseline == 0