Skip to content

Commit 993c6e3

Browse files
authored
Do not expose open() and close() to user in dataset api (#448)
* do not expose open() and close() to user * update changelog * implement PR feedback * format code * fix comments * fix code after merging master * remove import
1 parent f874eae commit 993c6e3

File tree

8 files changed

+115
-219
lines changed

8 files changed

+115
-219
lines changed

webknossos/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ For upgrade instructions, please check the respective *Breaking Changes* section
1818
- Added dataset down- and upload as well as annotation download, see the examples `learned_segmenter.py` and `upload_image_data.py`. [#437](https://github.com/scalableminds/webknossos-libs/pull/452)
1919

2020
### Changed
21+
- `View`s now always open the `wkw.Dataset` lazily. All explicit calls to `View.open()` and `View.close()` must be removed. [#448](https://github.com/scalableminds/webknossos-libs/pull/448)
2122
### Fixed
2223

2324

webknossos/tests/test_dataset.py

Lines changed: 13 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import numpy as np
1111
import pytest
12-
from wkw.wkw import WKWException
1312

1413
from webknossos.dataset import (
1514
Dataset,
@@ -223,41 +222,18 @@ def test_modify_existing_dataset() -> None:
223222
assure_exported_properties(ds2)
224223

225224

226-
def test_view_read_with_open() -> None:
225+
def test_view_read() -> None:
227226
wk_view = (
228227
Dataset(TESTDATA_DIR / "simple_wk_dataset")
229228
.get_layer("color")
230229
.get_mag("1")
231230
.get_view(size=(16, 16, 16))
232231
)
233232

234-
assert not wk_view._is_opened
235-
236-
with wk_view.open():
237-
assert wk_view._is_opened
238-
239-
data = wk_view.read(size=(10, 10, 10))
240-
assert data.shape == (3, 10, 10, 10) # three channel
241-
242-
assert not wk_view._is_opened
243-
244-
245-
def test_view_read_without_open() -> None:
246-
wk_view = (
247-
Dataset(TESTDATA_DIR / "simple_wk_dataset")
248-
.get_layer("color")
249-
.get_mag("1")
250-
.get_view(size=(16, 16, 16))
251-
)
252-
253-
assert not wk_view._is_opened
254-
255-
# 'read()' checks if it was already opened. If not, it opens and closes automatically
233+
# 'read()' checks if it was already opened. If not, it opens it automatically
256234
data = wk_view.read(size=(10, 10, 10))
257235
assert data.shape == (3, 10, 10, 10) # three channel
258236

259-
assert not wk_view._is_opened
260-
261237

262238
def test_view_write() -> None:
263239
delete_dir(TESTOUTPUT_DIR / "simple_wk_dataset")
@@ -270,14 +246,13 @@ def test_view_write() -> None:
270246
.get_view(size=(16, 16, 16))
271247
)
272248

273-
with wk_view.open():
274-
np.random.seed(1234)
275-
write_data = (np.random.rand(3, 10, 10, 10) * 255).astype(np.uint8)
249+
np.random.seed(1234)
250+
write_data = (np.random.rand(3, 10, 10, 10) * 255).astype(np.uint8)
276251

277-
wk_view.write(write_data)
252+
wk_view.write(write_data)
278253

279-
data = wk_view.read(size=(10, 10, 10))
280-
assert np.array_equal(data, write_data)
254+
data = wk_view.read(size=(10, 10, 10))
255+
assert np.array_equal(data, write_data)
281256

282257

283258
def test_view_write_out_of_bounds() -> None:
@@ -293,11 +268,10 @@ def test_view_write_out_of_bounds() -> None:
293268
.get_view(size=(16, 16, 16))
294269
)
295270

296-
with view.open():
297-
with pytest.raises(AssertionError):
298-
view.write(
299-
np.zeros((200, 200, 5), dtype=np.uint8)
300-
) # this is bigger than the bounding_box
271+
with pytest.raises(AssertionError):
272+
view.write(
273+
np.zeros((200, 200, 5), dtype=np.uint8)
274+
) # this is bigger than the bounding_box
301275

302276

303277
def test_mag_view_write_out_of_bounds() -> None:
@@ -686,29 +660,6 @@ def test_chunking_wk_wrong_chunk_size() -> None:
686660
assure_exported_properties(ds)
687661

688662

689-
def test_view_write_without_open() -> None:
690-
ds_path = TESTOUTPUT_DIR / "wk_dataset_write_without_open"
691-
delete_dir(ds_path)
692-
693-
ds = Dataset.create(ds_path, scale=(1, 1, 1))
694-
layer = ds.add_layer("color", LayerCategories.COLOR_TYPE)
695-
layer.bounding_box = BoundingBox(
696-
(0, 0, 0), (64, 64, 64)
697-
) # This newly created dataset would otherwise have a "empty" bounding box
698-
mag = layer.add_mag("1")
699-
700-
wk_view = mag.get_view(size=(32, 64, 16))
701-
702-
assert not wk_view._is_opened
703-
704-
write_data = (np.random.rand(32, 64, 16) * 255).astype(np.uint8)
705-
wk_view.write(write_data)
706-
707-
assert not wk_view._is_opened
708-
709-
assure_exported_properties(ds)
710-
711-
712663
def test_typing_of_get_mag() -> None:
713664
ds = Dataset(TESTDATA_DIR / "simple_wk_dataset")
714665
layer = ds.get_layer("color")
@@ -1340,9 +1291,8 @@ def test_search_dataset_also_for_long_layer_name() -> None:
13401291
# rename the path from "long_layer_name/color/2" to "long_layer_name/color/2-2-2"
13411292
os.rename(short_mag_file_path, long_mag_file_path)
13421293

1343-
with pytest.raises(WKWException):
1344-
# the dataset has to be reopened to notice the changed directory
1345-
mag.read(offset=(10, 10, 10), size=(10, 10, 10))
1294+
# make sure that reading data still works
1295+
mag.read(offset=(10, 10, 10), size=(10, 10, 10))
13461296

13471297
# when opening the dataset, it searches both for the long and the short path
13481298
layer = Dataset(TESTOUTPUT_DIR / "long_layer_name").get_layer("color")

webknossos/webknossos/dataset/downsampling_utils.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,6 @@ def downsample_cube_job(
307307
)
308308
)
309309

310-
source_view.open()
311-
312310
for tile in tiles:
313311
target_offset = np.array(tile) * buffer_edge_len
314312
source_offset = (mag_factors * target_offset).astype(int)
@@ -348,7 +346,6 @@ def downsample_cube_job(
348346
buffer_offset[2] : buffer_end[2],
349347
] = data_cube
350348

351-
source_view.close()
352349
# Write the downsampled buffer to target
353350
if source_view.header.num_channels == 1:
354351
file_buffer = file_buffer[0] # remove channel dimension

webknossos/webknossos/dataset/layer.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,9 @@ def name(self, layer_name: str) -> None:
217217
# The MagViews need to be updated
218218
for mag in self._mags.values():
219219
mag._path = _find_mag_path_on_disk(self.dataset.path, self.name, mag.name)
220-
if mag._is_opened:
221-
# Reopen handle to dataset on disk
222-
mag.close()
223-
mag.open()
220+
# Deleting the dataset will close the file handle.
221+
# The new dataset will be opened automatically when needed.
222+
del mag._wkw_dataset
224223

225224
self.dataset._export_as_json()
226225

webknossos/webknossos/dataset/mag_view.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -244,22 +244,13 @@ def get_bounding_boxes_on_disk(
244244
- the bounding box in the properties is an "overall" bounding box, which abstracts from the files on disk
245245
"""
246246
cube_size = self._get_file_dimensions()
247-
was_opened = self._is_opened
248-
249-
if not was_opened:
250-
self.open() # opening the view is necessary to set the dataset
251-
252-
assert self._dataset is not None
253-
for filename in self._dataset.list_files():
247+
for filename in self._wkw_dataset.list_files():
254248
file_path = Path(os.path.splitext(filename)[0]).relative_to(self._path)
255249
cube_index = _extract_file_index(file_path)
256250
cube_offset = [idx * size for idx, size in zip(cube_index, cube_size)]
257251

258252
yield (cube_offset[0], cube_offset[1], cube_offset[2]), cube_size
259253

260-
if not was_opened:
261-
self.close()
262-
263254
def compress(
264255
self,
265256
target_path: Optional[Union[str, Path]] = None,
@@ -296,19 +287,13 @@ def compress(
296287
self.name, str(uncompressed_full_path)
297288
)
298289
)
299-
300-
was_opened = self._is_opened
301-
if not was_opened:
302-
self.open() # opening the view is necessary to set the dataset
303-
assert self._dataset is not None
304-
305290
# create empty wkw.Dataset
306-
self._dataset.compress(str(compressed_full_path))
291+
self._wkw_dataset.compress(str(compressed_full_path))
307292

308293
# compress all files to and move them to 'compressed_path'
309294
with get_executor_for_args(args) as executor:
310295
job_args = []
311-
for file in self._dataset.list_files():
296+
for file in self._wkw_dataset.list_files():
312297
rel_file = Path(file).relative_to(self.layer.dataset.path)
313298
job_args.append((Path(file), compressed_path / rel_file))
314299

@@ -318,9 +303,6 @@ def compress(
318303

319304
logging.info("Mag {0} successfully compressed".format(self.name))
320305

321-
if not was_opened:
322-
self.close()
323-
324306
if target_path is None:
325307
shutil.rmtree(uncompressed_full_path)
326308
shutil.move(str(compressed_full_path), uncompressed_full_path)

webknossos/webknossos/dataset/upsampling_utils.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ def upsample_cube_job(
5757
)
5858
)
5959

60-
source_view.open()
61-
6260
for tile in tiles:
6361
target_offset = np.array(tile) * buffer_edge_len
6462
source_offset = (mag_factors * target_offset).astype(int)
@@ -95,7 +93,6 @@ def upsample_cube_job(
9593
buffer_offset[2] : buffer_end[2],
9694
] = data_cube
9795

98-
source_view.close()
9996
# Write the upsampled buffer to target
10097
if source_view.header.num_channels == 1:
10198
file_buffer = file_buffer[0] # remove channel dimension

webknossos/webknossos/dataset/view.py

Lines changed: 24 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@ def __init__(
3838
"""
3939
Do not use this constructor manually. Instead use `webknossos.dataset.mag_view.MagView.get_view()` to get a `View`.
4040
"""
41-
self._dataset: Optional[Dataset] = None
4241
self._path = path_to_mag_view
4342
self._header: wkw.Header = header
4443
self._size: Vec3Int = Vec3Int(size)
4544
self._global_offset: Vec3Int = Vec3Int(global_offset)
4645
self._is_bounded = is_bounded
4746
self._read_only = read_only
48-
self._is_opened = False
47+
self._cached_wkw_dataset = None
4948
# The bounding box of the view is used to prevent warnings when writing compressed but unaligned data
5049
# directly at the borders of the bounding box.
5150
# A View is unable to get this information from the Dataset because it is detached from it.
@@ -73,37 +72,6 @@ def global_offset(self) -> Vec3Int:
7372
def read_only(self) -> bool:
7473
return self._read_only
7574

76-
def open(self) -> "View":
77-
"""
78-
Opens the actual handles to the data on disk.
79-
A `MagDataset` has to be opened before it can be read or written to. However, the user does not
80-
have to open it explicitly because the API automatically opens it when it is needed.
81-
The user can choose to open it explicitly to avoid that handles are opened and closed automatically
82-
each time data is read or written.
83-
"""
84-
if self._is_opened:
85-
raise Exception("Cannot open view: the view is already opened")
86-
else:
87-
self._dataset = Dataset.open(
88-
str(self._path)
89-
) # No need to pass the header to the wkw.Dataset
90-
self._is_opened = True
91-
return self
92-
93-
def close(self) -> None:
94-
"""
95-
Complementary to `open`, this closes the handles to the data.
96-
97-
See `open` for more information.
98-
"""
99-
if not self._is_opened:
100-
raise Exception("Cannot close View: the view is not opened")
101-
else:
102-
assert self._dataset is not None # because the View was opened
103-
self._dataset.close()
104-
self._dataset = None
105-
self._is_opened = False
106-
10775
def write(
10876
self,
10977
data: np.ndarray,
@@ -120,7 +88,6 @@ def write(
12088

12189
offset = Vec3Int(offset)
12290

123-
was_opened = self._is_opened
12491
# assert the size of the parameter data is not in conflict with the attribute self.size
12592
data_dims = Vec3Int(data.shape[-3:])
12693
_assert_positive_dimensions(offset, data_dims)
@@ -135,14 +102,7 @@ def write(
135102
if self._is_compressed():
136103
absolute_offset, data = self._handle_compressed_write(absolute_offset, data)
137104

138-
if not was_opened:
139-
self.open()
140-
assert self._dataset is not None # because the View was opened
141-
142-
self._dataset.write(absolute_offset.to_np(), data)
143-
144-
if not was_opened:
145-
self.close()
105+
self._wkw_dataset.write(absolute_offset.to_np(), data)
146106

147107
def read(
148108
self,
@@ -206,16 +166,7 @@ def _read_without_checks(
206166
absolute_offset: Vec3Int,
207167
size: Vec3Int,
208168
) -> np.ndarray:
209-
was_opened = self._is_opened
210-
if not was_opened:
211-
self.open()
212-
assert self._dataset is not None # because the View was opened
213-
214-
data = self._dataset.read(absolute_offset.to_np(), size.to_np())
215-
216-
if not was_opened:
217-
self.close()
218-
169+
data = self._wkw_dataset.read(absolute_offset.to_np(), size.to_np())
219170
return data
220171

221172
def get_view(
@@ -593,6 +544,9 @@ def get_dtype(self) -> type:
593544
return self.header.voxel_type
594545

595546
def __enter__(self) -> "View":
547+
warnings.warn(
548+
"[DEPRECATION] Entering a View to open it is deprecated. The internal dataset will be opened automatically."
549+
)
596550
return self
597551

598552
def __exit__(
@@ -601,7 +555,7 @@ def __exit__(
601555
_value: Optional[BaseException],
602556
_tb: Optional[TracebackType],
603557
) -> None:
604-
self.close()
558+
pass
605559

606560
def __repr__(self) -> str:
607561
return repr(
@@ -614,6 +568,23 @@ def _mag_view_bounding_box_at_creation(self) -> BoundingBox:
614568
assert self._mag_view_bbox_at_creation is not None
615569
return self._mag_view_bbox_at_creation
616570

571+
@property
572+
def _wkw_dataset(self) -> wkw.Dataset:
573+
if self._cached_wkw_dataset is None:
574+
self._cached_wkw_dataset = Dataset.open(
575+
str(self._path)
576+
) # No need to pass the header to the wkw.Dataset
577+
return self._cached_wkw_dataset
578+
579+
@_wkw_dataset.deleter
580+
def _wkw_dataset(self) -> None:
581+
if self._cached_wkw_dataset is not None:
582+
self._cached_wkw_dataset.close()
583+
self._cached_wkw_dataset = None
584+
585+
def __del__(self) -> None:
586+
del self._cached_wkw_dataset
587+
617588

618589
def _assert_positive_dimensions(offset: Vec3Int, size: Vec3Int) -> None:
619590
if any(x < 0 for x in offset):

0 commit comments

Comments
 (0)