diff --git a/docs/src/api_bindings.md b/docs/src/api_bindings.md index 600de918e..b4974bc07 100644 --- a/docs/src/api_bindings.md +++ b/docs/src/api_bindings.md @@ -60,7 +60,9 @@ h5_set_free_list_limits - [`h5a_get_type`](@ref h5a_get_type) - [`h5a_iterate`](@ref h5a_iterate) - [`h5a_open`](@ref h5a_open) +- [`h5a_open_by_idx`](@ref h5a_open_by_idx) - [`h5a_read`](@ref h5a_read) +- [`h5a_rename`](@ref h5a_rename) - [`h5a_write`](@ref h5a_write) ```@docs h5a_close @@ -78,7 +80,9 @@ h5a_get_space h5a_get_type h5a_iterate h5a_open +h5a_open_by_idx h5a_read +h5a_rename h5a_write ``` diff --git a/docs/src/attributes.md b/docs/src/attributes.md index 06a188c5e..3277daead 100644 --- a/docs/src/attributes.md +++ b/docs/src/attributes.md @@ -1,18 +1,22 @@ # Attributes +## Dictionary interface + ```@docs -HDF5.Attribute +attrs attributes ``` ## Mid-level Interface ```@docs +HDF5.Attribute open_attribute create_attribute read_attribute write_attribute delete_attribute +rename_attribute ``` ## Convenience interface diff --git a/gen/api_defs.jl b/gen/api_defs.jl index ff86e5057..b27b54893 100644 --- a/gen/api_defs.jl +++ b/gen/api_defs.jl @@ -49,7 +49,9 @@ @bind h5a_get_type(attr_id::hid_t)::hid_t "Error getting attribute type" @bind h5a_iterate2(obj_id::hid_t, idx_type::Cint, order::Cint, n::Ptr{hsize_t}, op::Ptr{Cvoid}, op_data::Any)::herr_t string("Error iterating attributes in object ", h5i_get_name(obj_id)) @bind h5a_open(obj_id::hid_t, attr_name::Ptr{UInt8}, aapl_id::hid_t)::hid_t string("Error opening attribute ", attr_name, " for object ", h5i_get_name(obj_id)) +@bind h5a_open_by_idx(obj_id::hid_t, pathname::Ptr{UInt8}, idx_type::Cint, order::Cint, n::hsize_t, aapl_id::hid_t, lapl_id::hid_t)::hid_t string("Error opening attribute ", n, " of ", h5i_get_name(obj_id), "/", pathname) @bind h5a_read(attr_id::hid_t, mem_type_id::hid_t, buf::Ptr{Cvoid})::herr_t string("Error reading attribute ", h5a_get_name(attr_id)) +@bind h5a_rename(loc_id::hid_t, old_attr_name::Cstring, new_attr_name::Cstring)::herr_t string("Could not rename attribute") @bind h5a_write(attr_hid::hid_t, mem_type_id::hid_t, buf::Ptr{Cvoid})::herr_t "Error writing attribute data" ### diff --git a/src/HDF5.jl b/src/HDF5.jl index 806df46b2..58a9fa774 100644 --- a/src/HDF5.jl +++ b/src/HDF5.jl @@ -12,7 +12,7 @@ import Mmap export @read, @write, h5open, h5read, h5write, h5rewrite, h5writeattr, h5readattr, -create_attribute, open_attribute, read_attribute, write_attribute, delete_attribute, attributes, +create_attribute, open_attribute, read_attribute, write_attribute, delete_attribute, attributes, attrs, create_dataset, open_dataset, read_dataset, write_dataset, create_group, open_group, copy_object, open_object, delete_object, move_link, diff --git a/src/api/functions.jl b/src/api/functions.jl index 3c8c9a4a0..982ebdd8a 100644 --- a/src/api/functions.jl +++ b/src/api/functions.jl @@ -263,6 +263,17 @@ function h5a_open(obj_id, attr_name, aapl_id) return var"#status#" end +""" + h5a_open_by_idx(obj_id::hid_t, pathname::Ptr{UInt8}, idx_type::Cint, order::Cint, n::hsize_t, aapl_id::hid_t, lapl_id::hid_t) -> hid_t + +See `libhdf5` documentation for [`H5Aopen_by_idx`](https://portal.hdfgroup.org/display/HDF5/H5A_OPEN_BY_IDX). +""" +function h5a_open_by_idx(obj_id, pathname, idx_type, order, n, aapl_id, lapl_id) + var"#status#" = ccall((:H5Aopen_by_idx, libhdf5), hid_t, (hid_t, Ptr{UInt8}, Cint, Cint, hsize_t, hid_t, hid_t), obj_id, pathname, idx_type, order, n, aapl_id, lapl_id) + var"#status#" < 0 && @h5error(string("Error opening attribute ", n, " of ", h5i_get_name(obj_id), "/", pathname)) + return var"#status#" +end + """ h5a_read(attr_id::hid_t, mem_type_id::hid_t, buf::Ptr{Cvoid}) @@ -274,6 +285,17 @@ function h5a_read(attr_id, mem_type_id, buf) return nothing end +""" + h5a_rename(loc_id::hid_t, old_attr_name::Cstring, new_attr_name::Cstring) + +See `libhdf5` documentation for [`H5Arename`](https://portal.hdfgroup.org/display/HDF5/H5A_RENAME). +""" +function h5a_rename(loc_id, old_attr_name, new_attr_name) + var"#status#" = ccall((:H5Arename, libhdf5), herr_t, (hid_t, Cstring, Cstring), loc_id, old_attr_name, new_attr_name) + var"#status#" < 0 && @h5error(string("Could not rename attribute")) + return nothing +end + """ h5a_write(attr_hid::hid_t, mem_type_id::hid_t, buf::Ptr{Cvoid}) diff --git a/src/attributes.jl b/src/attributes.jl index 20c16a0e0..1447a762e 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -1,3 +1,5 @@ +# mid-level API + """ HDF5.Attribute @@ -77,7 +79,6 @@ Open the [`Attribute`](@ref) named `name` on the object `parent`. open_attribute(parent::Union{File,Object}, name::AbstractString, aapl::AttributeAccessProperties=AttributeAccessProperties()) = Attribute(API.h5a_open(checkvalid(parent), name, aapl), file(parent)) - """ create_attribute(parent::Union{File,Object}, name::AbstractString, dtype::Datatype, space::Dataspace) create_attribute(parent::Union{File,Object}, name::AbstractString, data) @@ -102,24 +103,25 @@ function create_attribute(parent::Union{File,Object}, name::AbstractString, dtyp return Attribute(attrid, file(parent)) end - +# generic method +write_attribute(attr::Attribute, memtype::Datatype, x) = API.h5a_write(attr, memtype, x) +# specific methods function write_attribute(attr::Attribute, memtype::Datatype, str::AbstractString) strbuf = Base.cconvert(Cstring, str) GC.@preserve strbuf begin buf = Base.unsafe_convert(Ptr{UInt8}, strbuf) - API.h5a_write(attr, memtype, buf) + write_attribute(attr, memtype, buf) end end function write_attribute(attr::Attribute, memtype::Datatype, x::T) where {T<:Union{ScalarType,Complex{<:ScalarType}}} tmp = Ref{T}(x) - API.h5a_write(attr, memtype, tmp) + write_attribute(attr, memtype, tmp) end function write_attribute(attr::Attribute, memtype::Datatype, strs::Array{<:AbstractString}) p = Ref{Cstring}(strs) - API.h5a_write(attr, memtype, p) + write_attribute(attr, memtype, p) end write_attribute(attr::Attribute, memtype::Datatype, ::EmptyArray) = nothing -write_attribute(attr::Attribute, memtype::Datatype, x) = API.h5a_write(attr, memtype, x) """ write_attribute(parent::Union{File,Object}, name::AbstractString, data) @@ -127,19 +129,26 @@ write_attribute(attr::Attribute, memtype::Datatype, x) = API.h5a_write(attr, mem Write `data` as an [`Attribute`](@ref) named `name` on the object `parent`. """ function write_attribute(parent::Union{File,Object}, name::AbstractString, data; pv...) - obj, dtype = create_attribute(parent, name, data; pv...) + attr, dtype = create_attribute(parent, name, data; pv...) try - write_attribute(obj, dtype, data) + write_attribute(attr, dtype, data) catch exc delete_attribute(parent, name) rethrow(exc) finally - close(obj) + close(attr) close(dtype) end nothing end +""" + rename_attribute(parent::Union{File,Object}, oldname::AbstractString, newname::AbstractString) + +Rename the [`Attribute`](@ref) of the object `parent` named `oldname` to `newname`. +""" +rename_attribute(parent::Union{File,Object}, oldname::AbstractString, newname::AbstractString) = + API.h5a_rename(checkvalid(parent), oldname, newname) """ delete_attribute(parent::Union{File,Object}, name::AbstractString) @@ -158,10 +167,7 @@ function h5writeattr(filename, name::AbstractString, data::Dict) file = h5open(filename, "r+") try obj = file[name] - attrs = attributes(obj) - for x in keys(data) - attrs[x] = data[x] - end + merge!(attrs(obj), data) close(obj) finally close(file) @@ -178,8 +184,7 @@ function h5readattr(filename, name::AbstractString) file = h5open(filename,"r") try obj = file[name] - a = attributes(obj) - dat = Dict(x => read(a[x]) for x in keys(a)) + dat = Dict(attrs(obj)) close(obj) finally close(file) @@ -188,6 +193,85 @@ function h5readattr(filename, name::AbstractString) end +struct AttributeDict <: AbstractDict{String,Any} + parent::Object +end + +""" + attrs(object::Union{File,Group,Dataset,Datatype}) + +The attributes dictionary of `object`. Returns an `AttributeDict`, a `Dict`-like +object for accessing the attributes of `object`. + +```julia +attrs(object)["name"] = value # create/overwrite an attribute +attr = attrs(object)["name"] # read an attribute +delete!(attrs(object), "name") # delete an attribute +keys(attrs(object)) # list the attribute names +``` +""" +AttributeDict(file::File) = AttributeDict(open_group(file, ".")) + +function attrs(parent) + return AttributeDict(parent) +end + +Base.haskey(attrdict::AttributeDict, path::AbstractString) = API.h5a_exists(checkvalid(attrdict.parent), path) +Base.length(attrdict::AttributeDict) = Int(object_info(attrdict.parent).num_attrs) + +function Base.getindex(x::AttributeDict, name::AbstractString) + haskey(x, name) || throw(KeyError(name)) + read_attribute(x.parent, name) +end +function Base.get(x::AttributeDict, name::AbstractString, default) + haskey(x, name) || return default + read_attribute(x.parent, name) +end +function Base.setindex!(attrdict::AttributeDict, val, name::AbstractString) + if haskey(attrdict, name) + # in case of an error, we write first to a temporary, then rename + _name = tempname() + try + write_attribute(attrdict.parent, _name, val) + delete_attribute(attrdict.parent, name) + rename_attribute(attrdict.parent, _name, name) + finally + haskey(attrdict, _name) && delete_attribute(attrdict.parent, _name) + end + else + write_attribute(attrdict.parent, name, val) + end +end +Base.delete!(attrdict::AttributeDict, path::AbstractString) = delete_attribute(attrdict.parent, path) + +function Base.keys(attrdict::AttributeDict) + # faster than iteratively calling h5a_get_name_by_idx + checkvalid(attrdict.parent) + keyvec = sizehint!(String[], length(attrdict)) + API.h5a_iterate(attrdict.parent, IDX_TYPE[], ORDER[]) do _, attr_name, _ + push!(keyvec, unsafe_string(attr_name)) + return false + end + return keyvec +end + +function Base.iterate(attrdict::AttributeDict) + # constuct key vector, then iterate + # faster than calling h5a_open_by_idx + iterate(attrdict, (keys(attrdict), 1)) +end +function Base.iterate(attrdict::AttributeDict, (keyvec, n)) + iter = iterate(keyvec, n) + if isnothing(iter) + return iter + end + key, nn = iter + return (key => attrdict[key]), (keyvec, nn) +end + + + + struct Attributes parent::Union{File,Object} end @@ -222,8 +306,7 @@ function Base.keys(x::Attributes) end Base.read(attr::Attributes, name::AbstractString) = read_attribute(attr.parent, name) - -# Datasets act like attributes +# Dataset methods which act like attributes Base.write(parent::Dataset, name::AbstractString, data; pv...) = write_attribute(parent, name, data; pv...) function Base.getindex(dset::Dataset, name::AbstractString) haskey(dset, name) || throw(KeyError(name)) diff --git a/src/show.jl b/src/show.jl index 204944cdb..42f4e813b 100644 --- a/src/show.jl +++ b/src/show.jl @@ -63,6 +63,16 @@ function Base.show(io::IO, attr::Attributes) print(io, "Attributes of ", attr.parent) end +Base.show(io::IO, attrdict::AttributeDict) = summary(io, attrdict) +function Base.summary(io::IO, attrdict::AttributeDict) + print(io, "AttributeDict of ", attrdict.parent) + if isvalid(attrdict.parent) + n = length(attrdict) + print(io, " with ", n, n == 1 ? " attribute" : " attributes") + end +end + + const ENDIAN_DICT = Dict( API.H5T_ORDER_LE => "little endian byte order", API.H5T_ORDER_BE => "big endian byte order", @@ -189,9 +199,9 @@ _tree_head(io::IO, obj) = print(io, _tree_icon(obj), " ", obj) _tree_head(io::IO, obj::Datatype) = print(io, _tree_icon(obj), " HDF5.Datatype: ", name(obj)) _tree_count(parent::Union{File,Group}, attributes::Bool) = - length(parent) + (attributes ? length(HDF5.attributes(parent)) : 0) + length(parent) + (attributes ? length(HDF5.attrs(parent)) : 0) _tree_count(parent::Dataset, attributes::Bool) = - attributes ? length(HDF5.attributes(parent)) : 0 + attributes ? length(HDF5.attrs(parent)) : 0 _tree_count(parent::Attributes, _::Bool) = length(parent) _tree_count(parent::Union{Attribute,Datatype}, _::Bool) = 0 diff --git a/test/attributes.jl b/test/attributes.jl new file mode 100644 index 000000000..5678c690c --- /dev/null +++ b/test/attributes.jl @@ -0,0 +1,74 @@ +using HDF5, Test + +function test_attrs(o::Union{HDF5.File, HDF5.Object}) + + @test attrs(o) isa HDF5.AttributeDict + + attrs(o)["a"] = 1 + @test haskey(attrs(o), "a") + @test attrs(o)["a"] == 1 + + attrs(o)["b"] = [2,3] + @test attrs(o)["b"] == [2,3] + @test haskey(attrs(o), "a") + @test length(attrs(o)) == 2 + @test sort(keys(attrs(o))) == ["a", "b"] + + @test !haskey(attrs(o), "c") + + # overwrite: same type + attrs(o)["a"] = 4 + @test attrs(o)["a"] == 4 + @test get(attrs(o), "a", nothing) == 4 + @test length(attrs(o)) == 2 + @test sort(keys(attrs(o))) == ["a", "b"] + + # overwrite: different size + attrs(o)["b"] = [4,5,6] + @test attrs(o)["b"] == [4,5,6] + @test length(attrs(o)) == 2 + @test sort(keys(attrs(o))) == ["a", "b"] + + # overwrite: different type + attrs(o)["b"] = "a string" + @test attrs(o)["b"] == "a string" + @test length(attrs(o)) == 2 + @test sort(keys(attrs(o))) == ["a", "b"] + + # delete a key + delete!(attrs(o), "a") + @test !haskey(attrs(o), "a") + @test length(attrs(o)) == 1 + @test sort(keys(attrs(o))) == ["b"] + + @test_throws KeyError attrs(o)["a"] + @test isnothing(get(attrs(o), "a", nothing)) + +end + + +@testset "attrs interface" begin + filename = tempname() + f = h5open(filename, "w") + + try + # Test attrs on a HDF5.File + test_attrs(f) + + # Test attrs on a HDF5.Group + g = create_group(f, "group_foo") + test_attrs(g) + + # Test attrs on a HDF5.Dataset + d = create_dataset(g, "dataset_bar", Int, (32, 32)) + test_attrs(d) + + # Test attrs on a HDF5.Datatype + t = commit_datatype(g, "datatype_int16", + HDF5.Datatype(HDF5.API.h5t_copy(HDF5.API.H5T_NATIVE_INT16)) + ) + test_attrs(t) + finally + close(f) + end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 64392cf67..22d31306f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -30,6 +30,8 @@ include("dataspace.jl") include("datatype.jl") @debug "hyperslab" include("hyperslab.jl") +@debug "attributes" +include("attributes.jl") @debug "readremote" include("readremote.jl") @debug "extend_test"