Skip to content

Commit 85b68cb

Browse files
committed
Enhance block API to automatically close resources and add tests for error handling
1 parent fd7e04f commit 85b68cb

5 files changed

Lines changed: 126 additions & 55 deletions

File tree

lib/hdf5.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
module HDF5
77
class Error < StandardError; end
8+
DEFAULT_PROPERTY_LIST = 0
89

910
class << self
1011
attr_accessor :lib_path

lib/hdf5/dataset.rb

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,45 @@
11
module HDF5
22
class Dataset
3-
H5P_DEFAULT = 0
4-
53
class << self
64
def create(parent_id, name, data)
75
values = normalize_data(data)
86
dims = ::FFI::MemoryPointer.new(:ulong_long, 1)
97
dims.write_array_of_ulong_long([values.length])
108
datatype_id = datatype_id_for(values)
119
dataspace_id = HDF5::FFI.H5Screate_simple(1, dims, nil)
12-
raise "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
10+
raise HDF5::Error, "Failed to create dataspace for dataset: #{name}" if dataspace_id < 0
1311

14-
dataset_id = HDF5::FFI.H5Dcreate2(parent_id, name, datatype_id, dataspace_id, H5P_DEFAULT, H5P_DEFAULT,
15-
H5P_DEFAULT)
16-
dataset = from_id(dataset_id, name)
12+
dataset = from_id(
13+
HDF5::FFI.H5Dcreate2(parent_id, name, datatype_id, dataspace_id, HDF5::DEFAULT_PROPERTY_LIST,
14+
HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST), name
15+
)
1716
dataset.write(values)
18-
dataset
17+
return dataset unless block_given?
18+
19+
begin
20+
yield dataset
21+
ensure
22+
dataset.close
23+
end
1924
ensure
2025
HDF5::FFI.H5Sclose(dataspace_id) if dataspace_id && dataspace_id >= 0
2126
end
2227

2328
def open(parent_id, name)
24-
from_id(HDF5::FFI.H5Dopen2(parent_id, name, H5P_DEFAULT), name)
29+
dataset = from_id(HDF5::FFI.H5Dopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
30+
return dataset unless block_given?
31+
32+
begin
33+
yield dataset
34+
ensure
35+
dataset.close
36+
end
2537
end
2638

2739
def normalize_data(data)
2840
values = data.is_a?(Array) ? data : [data]
29-
raise 'Dataset data must not be empty' if values.empty?
30-
raise 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
41+
raise HDF5::Error, 'Dataset data must not be empty' if values.empty?
42+
raise HDF5::Error, 'Nested arrays are not supported' if values.any? { |value| value.is_a?(Array) }
3143

3244
values
3345
end
@@ -38,7 +50,7 @@ def datatype_id_for(data)
3850
elsif data.all? { |value| value.is_a?(Numeric) }
3951
HDF5::FFI.H5T_NATIVE_DOUBLE
4052
else
41-
raise 'Only numeric dataset data is supported'
53+
raise HDF5::Error, 'Only numeric dataset data is supported'
4254
end
4355
end
4456

@@ -64,7 +76,7 @@ def from_id(dataset_id, name)
6476
end
6577

6678
def initialize(parent_id, name)
67-
initialize_from_id(HDF5::FFI.H5Dopen2(parent_id, name, H5P_DEFAULT), name)
79+
initialize_from_id(HDF5::FFI.H5Dopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
6880
end
6981

7082
def attrs
@@ -75,19 +87,23 @@ def write(data)
7587
values = self.class.normalize_data(data)
7688
buffer = self.class.buffer_for(values)
7789
mem_type_id = self.class.datatype_id_for(values)
78-
status = HDF5::FFI.H5Dwrite(@dataset_id, mem_type_id, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT, buffer)
79-
raise 'Failed to write dataset' if status < 0
90+
status = HDF5::FFI.H5Dwrite(@dataset_id, mem_type_id, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
91+
HDF5::DEFAULT_PROPERTY_LIST, buffer)
92+
raise HDF5::Error, 'Failed to write dataset' if status < 0
8093

8194
data
8295
end
8396

8497
def close
98+
return if @dataset_id.nil?
99+
85100
HDF5::FFI.H5Dclose(@dataset_id)
101+
@dataset_id = nil
86102
end
87103

88104
def dtype
89105
datatype_id = HDF5::FFI.H5Dget_type(@dataset_id)
90-
raise 'Failed to get datatype' if datatype_id < 0
106+
raise HDF5::Error, 'Failed to get datatype' if datatype_id < 0
91107

92108
HDF5::FFI.H5Tget_class(datatype_id)
93109
ensure
@@ -96,10 +112,10 @@ def dtype
96112

97113
def shape
98114
dataspace_id = HDF5::FFI.H5Dget_space(@dataset_id)
99-
raise 'Failed to get dataspace' if dataspace_id < 0
115+
raise HDF5::Error, 'Failed to get dataspace' if dataspace_id < 0
100116

101117
ndims = HDF5::FFI.H5Sget_simple_extent_ndims(dataspace_id)
102-
raise 'Failed to get number of dimensions' if ndims < 0
118+
raise HDF5::Error, 'Failed to get number of dimensions' if ndims < 0
103119

104120
dims = ::FFI::MemoryPointer.new(:ulong_long, ndims)
105121
HDF5::FFI.H5Sget_simple_extent_dims(dataspace_id, dims, nil)
@@ -110,35 +126,36 @@ def shape
110126
end
111127

112128
def read
113-
dtype = dtype()
114-
shape = shape()
129+
current_dtype = dtype
130+
current_shape = shape
115131

116-
total_elements = shape.inject(:*)
117-
case dtype
132+
total_elements = current_shape.inject(:*)
133+
case current_dtype
118134
when :H5T_INTEGER
119135
read_integer_data(total_elements)
120136
when :H5T_FLOAT
121137
read_float_data(total_elements)
122138
when :H5T_STRING
123139
read_string_data(total_elements)
124140
else
125-
raise 'Unsupported datatype'
141+
raise HDF5::Error, 'Unsupported datatype'
126142
end
127143
end
128144

129145
def read_integer_data(total_elements)
130146
buffer = ::FFI::MemoryPointer.new(:int, total_elements)
131-
status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_INT, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT, buffer)
132-
raise 'Failed to read integer dataset' if status < 0
147+
status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_INT, HDF5::DEFAULT_PROPERTY_LIST,
148+
HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
149+
raise HDF5::Error, 'Failed to read integer dataset' if status < 0
133150

134151
buffer.read_array_of_int(total_elements)
135152
end
136153

137154
def read_float_data(total_elements)
138155
buffer = ::FFI::MemoryPointer.new(:double, total_elements)
139-
status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_DOUBLE, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT,
140-
buffer)
141-
raise 'Failed to read float dataset' if status < 0
156+
status = HDF5::FFI.H5Dread(@dataset_id, HDF5::FFI.H5T_NATIVE_DOUBLE, HDF5::DEFAULT_PROPERTY_LIST,
157+
HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST, buffer)
158+
raise HDF5::Error, 'Failed to read float dataset' if status < 0
142159

143160
buffer.read_array_of_double(total_elements)
144161
end
@@ -150,7 +167,7 @@ def read_string_data(total_elements)
150167
private
151168

152169
def initialize_from_id(dataset_id, name)
153-
raise "Failed to open dataset: #{name}" if dataset_id < 0
170+
raise HDF5::Error, "Failed to open dataset: #{name}" if dataset_id < 0
154171

155172
@dataset_id = dataset_id
156173
@name = name

lib/hdf5/file.rb

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,29 @@ class File
33
H5F_ACC_RDONLY = 0x0000
44
H5F_ACC_RDWR = 0x0001
55
H5F_ACC_TRUNC = 0x0002
6-
H5P_DEFAULT = 0
76

87
class << self
98
def create(filename, flags = H5F_ACC_TRUNC)
10-
file_id = HDF5::FFI.H5Fcreate(filename, flags, H5P_DEFAULT, H5P_DEFAULT)
11-
from_id(file_id, filename, flags)
9+
file = from_id(HDF5::FFI.H5Fcreate(filename, flags, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST),
10+
filename, flags)
11+
return file unless block_given?
12+
13+
begin
14+
yield file
15+
ensure
16+
file.close
17+
end
1218
end
1319

1420
def open(filename, mode = H5F_ACC_RDONLY)
15-
from_id(HDF5::FFI.H5Fopen(filename, mode, H5P_DEFAULT), filename, mode)
21+
file = from_id(HDF5::FFI.H5Fopen(filename, mode, HDF5::DEFAULT_PROPERTY_LIST), filename, mode)
22+
return file unless block_given?
23+
24+
begin
25+
yield file
26+
ensure
27+
file.close
28+
end
1629
end
1730

1831
private
@@ -25,19 +38,22 @@ def from_id(file_id, filename, mode)
2538
end
2639

2740
def initialize(filename, mode = H5F_ACC_RDONLY)
28-
initialize_from_id(HDF5::FFI.H5Fopen(filename, mode, H5P_DEFAULT), filename, mode)
41+
initialize_from_id(HDF5::FFI.H5Fopen(filename, mode, HDF5::DEFAULT_PROPERTY_LIST), filename, mode)
2942
end
3043

3144
def close
45+
return if @file_id.nil?
46+
3247
HDF5::FFI.H5Fclose(@file_id)
48+
@file_id = nil
3349
end
3450

35-
def create_group(name)
36-
Group.create(@file_id, name)
51+
def create_group(name, &block)
52+
Group.create(@file_id, name, &block)
3753
end
3854

39-
def create_dataset(name, data)
40-
Dataset.create(@file_id, name, data)
55+
def create_dataset(name, data, &block)
56+
Dataset.create(@file_id, name, data, &block)
4157
end
4258

4359
def list_entries
@@ -47,10 +63,8 @@ def list_entries
4763
0 # continue
4864
end
4965

50-
case FFI::MiV
51-
when 10 then HDF5::FFI.H5Literate(@file_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, nil, callback, nil)
52-
when 14 then HDF5::FFI.H5Literate2(@file_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, nil, callback, nil)
53-
end.negative? && raise('Failed to iterate over file entries')
66+
HDF5::FFI.H5Literate2(@file_id, :H5_INDEX_NAME, :H5_ITER_NATIVE, nil, callback,
67+
nil).negative? && raise(HDF5::Error, 'Failed to iterate over file entries')
5468

5569
list
5670
end
@@ -61,7 +75,7 @@ def [](name)
6175
elsif dataset?(name)
6276
Dataset.open(@file_id, name)
6377
else
64-
raise 'Unknown object type'
78+
raise HDF5::Error, 'Unknown object type'
6579
end
6680
end
6781

@@ -72,7 +86,7 @@ def attrs
7286
private
7387

7488
def initialize_from_id(file_id, filename, mode)
75-
raise "Failed to open file: #{filename}" if file_id < 0
89+
raise HDF5::Error, "Failed to open file: #{filename}" if file_id < 0
7690

7791
@filename = filename
7892
@mode = mode

lib/hdf5/group.rb

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
module HDF5
22
class Group
3-
H5P_DEFAULT = 0
4-
53
class << self
64
def create(parent_id, name)
7-
group_id = HDF5::FFI.H5Gcreate2(parent_id, name, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)
8-
from_id(group_id, name)
5+
group = from_id(
6+
HDF5::FFI.H5Gcreate2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
7+
HDF5::DEFAULT_PROPERTY_LIST), name
8+
)
9+
return group unless block_given?
10+
11+
begin
12+
yield group
13+
ensure
14+
group.close
15+
end
916
end
1017

1118
def open(parent_id, name)
12-
from_id(HDF5::FFI.H5Gopen2(parent_id, name, H5P_DEFAULT), name)
19+
group = from_id(HDF5::FFI.H5Gopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
20+
return group unless block_given?
21+
22+
begin
23+
yield group
24+
ensure
25+
group.close
26+
end
1327
end
1428

1529
private
@@ -22,19 +36,22 @@ def from_id(group_id, name)
2236
end
2337

2438
def initialize(file_id, name)
25-
initialize_from_id(HDF5::FFI.H5Gopen2(file_id, name, H5P_DEFAULT), name)
39+
initialize_from_id(HDF5::FFI.H5Gopen2(file_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
2640
end
2741

2842
def close
43+
return if @group_id.nil?
44+
2945
HDF5::FFI.H5Gclose(@group_id)
46+
@group_id = nil
3047
end
3148

32-
def create_group(name)
33-
self.class.create(@group_id, name)
49+
def create_group(name, &block)
50+
self.class.create(@group_id, name, &block)
3451
end
3552

36-
def create_dataset(name, data)
37-
Dataset.create(@group_id, name, data)
53+
def create_dataset(name, data, &block)
54+
Dataset.create(@group_id, name, data, &block)
3855
end
3956

4057
def list_datasets
@@ -56,7 +73,7 @@ def [](name)
5673
elsif dataset?(name)
5774
Dataset.open(@group_id, name)
5875
else
59-
raise 'Group or Dataset not found'
76+
raise HDF5::Error, 'Group or Dataset not found'
6077
end
6178
end
6279

@@ -67,7 +84,7 @@ def attrs
6784
private
6885

6986
def initialize_from_id(group_id, name)
70-
raise "Failed to open group: #{name}" if group_id < 0
87+
raise HDF5::Error, "Failed to open group: #{name}" if group_id < 0
7188

7289
@group_id = group_id
7390
@name = name

test/hdf5_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,26 @@ class HDF5Test < Test::Unit::TestCase
7373
reopened.close
7474
end
7575
end
76+
77+
test 'block API closes resources automatically' do
78+
Dir.mktmpdir do |dir|
79+
path = File.join(dir, 'block.h5')
80+
81+
HDF5::File.create(path) do |file|
82+
file.create_group('values') do |group|
83+
group.create_dataset('ints', [10, 20, 30])
84+
end
85+
end
86+
87+
HDF5::File.open(path) do |file|
88+
assert_equal([10, 20, 30], file['values']['ints'].read)
89+
end
90+
end
91+
end
92+
93+
test 'raises HDF5 error for missing file' do
94+
assert_raise(HDF5::Error) do
95+
HDF5::File.open('/tmp/does-not-exist-ruby-hdf5.h5')
96+
end
97+
end
7698
end

0 commit comments

Comments
 (0)