Skip to content

Commit

Permalink
Merge pull request #238 from scipp/allow-passing-h5py-file
Browse files Browse the repository at this point in the history
Allow passing a h5py file when opening a file
  • Loading branch information
jl-wynen authored Sep 11, 2024
2 parents b294b50 + ea6b3c2 commit b335c82
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 4 deletions.
25 changes: 21 additions & 4 deletions src/scippnexus/file.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
# @author Simon Heybrock
import io
import os
from contextlib import AbstractContextManager

import h5py
Expand All @@ -17,6 +19,7 @@
class File(AbstractContextManager, Group):
def __init__(
self,
name: str | os.PathLike[str] | io.BytesIO | h5py.Group,
*args,
definitions: Definitions | DefaultDefinitionsType = DefaultDefinitions,
**kwargs,
Expand All @@ -27,22 +30,36 @@ def __init__(
Parameters
----------
name:
Specifies the file to open.
If this is a :class:`hyp5.File` object, the `:class:`File` will wrap
this file handle but will not close it when used as a context manager.
definitions:
Mapping of NX_class names to application-specific definitions.
The default is to use the base definitions as defined in the
NeXus standard.
"""
if definitions is DefaultDefinitions:
definitions = base_definitions()
self._file = h5py.File(*args, **kwargs)

if isinstance(name, h5py.File | h5py.Group):
if args or kwargs:
raise TypeError('Cannot provide both h5py.File and other arguments')
self._file = name
self._manage_file_context = False
else:
self._file = h5py.File(name, *args, **kwargs)
self._manage_file_context = True
super().__init__(self._file, definitions=definitions)

def __enter__(self):
self._file.__enter__()
if self._manage_file_context:
self._file.__enter__()
return self

def __exit__(self, exc_type, exc_value, traceback):
self._file.close()
if self._manage_file_context:
self._file.close()

def close(self):
self._file.close()
67 changes: 67 additions & 0 deletions tests/file_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)

import io
from pathlib import Path

import h5py as h5
import pytest

import scippnexus as snx


def is_closed(file: snx.File) -> bool:
try:
file.create_class('x', 'NXentry')
except ValueError as err:
return 'Unable to synchronously create group' in err.args[0]
return False


@pytest.mark.parametrize('path_type', [str, Path])
def test_load_entry_from_filename(tmp_path, path_type):
with h5.File(tmp_path / 'test.nxs', 'w') as f:
snx.create_class(f, 'entry', snx.NXentry)

with snx.File(path_type(tmp_path / 'test.nxs'), 'r') as f:
assert f.keys() == {'entry'}
assert is_closed(f)


def test_load_entry_from_buffer(tmp_path):
buffer = io.BytesIO()
with h5.File(buffer, 'w') as f:
snx.create_class(f, 'entry', snx.NXentry)
buffer.seek(0)

with snx.File(buffer, 'r') as f:
assert f.keys() == {'entry'}
assert is_closed(f)


def test_load_entry_from_h5py_group_root(tmp_path):
with h5.File('test.nxs', 'w', driver='core', backing_store=False) as h5_file:
snx.create_class(h5_file, 'entry', snx.NXentry)
with snx.File(h5_file) as snx_file:
assert snx_file.keys() == {'entry'}
# Not great, but we don't want to close the h5py file ourselves.
assert not is_closed(snx_file)


def test_load_entry_from_h5py_group_not_root(tmp_path):
with h5.File('test.nxs', 'w', driver='core', backing_store=False) as h5_file:
entry = snx.create_class(h5_file, 'entry', snx.NXentry)
snx.create_class(entry, 'user', snx.NXuser)
with snx.File(h5_file['entry']) as snx_file:
assert snx_file.keys() == {'user'}
# Not great, but we don't want to close the h5py file ourselves.
assert not is_closed(snx_file)


def test_file_from_h5py_group_does_not_allow_extra_args(tmp_path):
with h5.File('test.nxs', 'w', driver='core', backing_store=False) as h5_file:
snx.create_class(h5_file, 'entry', snx.NXentry)
with pytest.raises(
TypeError, match='Cannot provide both h5py.File and other arguments'
):
snx.File(h5_file, 'r')

0 comments on commit b335c82

Please sign in to comment.