Skip to content

Commit

Permalink
add metadata (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
bkamins authored Sep 19, 2022
1 parent 5c927b6 commit 32ef840
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "DataAPI"
uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
authors = ["quinnj <[email protected]>"]
version = "1.10.0"
version = "1.11.0"

[compat]
julia = "1"
Expand Down
154 changes: 154 additions & 0 deletions src/DataAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,158 @@ using a `sink` function to materialize the table.
"""
function allcombinations end

const STYLE_INFO = """
One of the uses of the metadata `style` is decision
how the metadata should be propagated when `x` is transformed. This interface
defines the `:default` style that indicates that metadata should not be propagated
under any operations (it is only preserved when a copy of the source table is
performed). All types supporting metadata allow at least this style.
"""

const COL_INFO = """
`col` must have a type that is supported by table `x` for column indexing.
Following the Tables.jl contract `Symbol` and `Int` are always allowed.
Passing `col` that is not a column of `x` throws an error.
"""

"""
metadata(x, key::AbstractString; style::Bool=false)
Return metadata value associated with object `x` for key `key`.
If `x` does not support metadata throw `ArgumentError`.
If `x` supports metadata, but does not have a mapping for `key` throw
`KeyError`.
If `style=true` return a tuple of metadata value and metadata style. Metadata
style is an additional information about the kind of metadata that is stored
for the `key`.
$STYLE_INFO
"""
metadata(::T, ::AbstractString; style::Bool=false) where {T} =
throw(ArgumentError("Objects of type $T do not support getting metadata"))

"""
metadatakeys(x)
Return an iterator of metadata keys for which `metadata(x, key)` returns a
metadata value. If `x` does not support metadata return `()`.
"""
metadatakeys(::Any) = ()

"""
metadata!(x, key::AbstractString, value; style)
Set metadata for object `x` for key `key` to have value `value`
and style `style` and return `x`.
If `x` does not support setting metadata throw `ArgumentError`.
$STYLE_INFO
"""
metadata!(::T, ::AbstractString, ::Any; style) where {T} =
throw(ArgumentError("Objects of type $T do not support setting metadata"))

"""
deletemetadata!(x, key::AbstractString)
Delete metadata for object `x` for key `key` and return `x`
(if metadata for `key` is not present do not perform any action).
If `x` does not support metadata deletion throw `ArgumentError`.
"""
deletemetadata!(::T, ::AbstractString) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))

"""
emptymetadata!(x)
Delete all metadata for object `x`.
If `x` does not support metadata deletion throw `ArgumentError`.
"""
emptymetadata!(::T) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))

"""
colmetadata(x, col, key::AbstractString; style::Bool=false)
Return metadata value associated with table `x` for column `col` and key `key`.
If `x` does not support metadata for column `col` throw `ArgumentError`. If `x`
supports metadata, but does not have a mapping for column `col` for `key` throw
`KeyError`.
$COL_INFO
If `style=true` return a tuple of metadata value and metadata style. Metadata
style is an additional information about the kind of metadata that is stored for
the `key`.
$STYLE_INFO
"""
colmetadata(::T, ::Int, ::AbstractString; style::Bool=false) where {T} =
throw(ArgumentError("Objects of type $T do not support getting column metadata"))
colmetadata(::T, ::Symbol, ::AbstractString; style::Bool=false) where {T} =
throw(ArgumentError("Objects of type $T do not support getting column metadata"))

"""
colmetadatakeys(x, [col])
If `col` is passed return an iterator of metadata keys for which `metadata(x,
col, key)` returns a metadata value. If `x` does not support metadata for column
`col` return `()`.
`col` must have a type that is supported by table `x` for column indexing.
Following the Tables.jl contract `Symbol` and `Int` are always allowed. Passing
`col` that is not a column of `x` either throws an error (this is a
preferred behavior if it is possible) or returns `()` (this duality is allowed
as some Tables.jl tables do not have a schema).
If `col` is not passed return an iterator of `col => colmetadatakeys(x, col)`
pairs for all columns that have metadata, where `col` are `Symbol`.
If `x` does not support column metadata return `()`.
"""
colmetadatakeys(::Any, ::Int) = ()
colmetadatakeys(::Any, ::Symbol) = ()
colmetadatakeys(::Any) = ()

"""
colmetadata!(x, col, key::AbstractString, value; style)
Set metadata for table `x` for column `col` for key `key` to have value `value`
and style `style` and return `x`.
If `x` does not support setting metadata for column `col` throw `ArgumentError`.
$COL_INFO
$STYLE_INFO
"""
colmetadata!(::T, ::Int, ::AbstractString, ::Any; style) where {T} =
throw(ArgumentError("Objects of type $T do not support setting metadata"))
colmetadata!(::T, ::Symbol, ::AbstractString, ::Any; style) where {T} =
throw(ArgumentError("Objects of type $T do not support setting metadata"))

"""
deletecolmetadata!(x, col, key::AbstractString)
Delete metadata for table `x` for column `col` for key `key` and return `x`
(if metadata for `key` is not present do not perform any action).
If `x` does not support metadata deletion for column `col` throw `ArgumentError`.
"""
deletecolmetadata!(::T, ::Symbol, ::AbstractString) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))
deletecolmetadata!(::T, ::Int, ::AbstractString) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))

"""
emptycolmetadata!(x, [col])
Delete all metadata for table `x` for column `col`.
If `col` is not passed delete all column level metadata for table `x`.
If `x` does not support metadata deletion for column `col` throw `ArgumentError`.
"""
emptycolmetadata!(::T, ::Symbol) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))
emptycolmetadata!(::T, ::Int) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))
emptycolmetadata!(::T) where {T} =
throw(ArgumentError("Objects of type $T do not support metadata deletion"))

end # module
131 changes: 131 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,79 @@ Base.size(x::TestArray) = size(x.x)
Base.getindex(x::TestArray, i) = x.x[i]
DataAPI.levels(x::TestArray) = reverse(DataAPI.levels(x.x))

# An example implementation of metadata
# For simplicity Int col indexing is not implemented
# and no checking if col is a column of a table is performed

struct TestMeta
table::Dict{String, Any}
col::Dict{Symbol, Dict{String, Any}}

TestMeta() = new(Dict{String, Any}(), Dict{Symbol, Dict{String, Any}}())
end

function DataAPI.metadata(x::TestMeta, key::AbstractString; style::Bool=false)
return style ? x.table[key] : x.table[key][1]
end

DataAPI.metadatakeys(x::TestMeta) = keys(x.table)

function DataAPI.metadata!(x::TestMeta, key::AbstractString, value; style)
x.table[key] = (value, style)
return x
end

function DataAPI.metadata!(x::TestMeta, key::AbstractString, value; style)
x.table[key] = (value, style)
return x
end

DataAPI.deletemetadata!(x::TestMeta, key::AbstractString) = delete!(x.table, key)
DataAPI.emptymetadata!(x::TestMeta) = empty!(x.table)

function DataAPI.colmetadata(x::TestMeta, col::Symbol, key::AbstractString; style::Bool=false)
return style ? x.col[col][key] : x.col[col][key][1]
end

function DataAPI.colmetadatakeys(x::TestMeta, col::Symbol)
haskey(x.col, col) && return keys(x.col[col])
return ()
end

function DataAPI.colmetadatakeys(x::TestMeta)
isempty(x.col) && return ()
return (col => keys(x.col[col]) for col in keys(x.col))
end

function DataAPI.colmetadata!(x::TestMeta, col::Symbol, key::AbstractString, value; style)
if haskey(x.col, col)
x.col[col][key] = (value, style)
else
x.col[col] = Dict{Any, Any}(key => (value, style))
end
return x
end

function DataAPI.deletecolmetadata!(x::TestMeta, col::Symbol, key::AbstractString)
if haskey(x.col, col)
delete!(x.col[col], key)
else
throw(ArgumentError("column $col not found"))
end
return x
end

function DataAPI.emptycolmetadata!(x::TestMeta, col::Symbol)
if haskey(x.col, col)
delete!(x.col, col)
else
throw(ArgumentError("column $col not found"))
end
return x
end

DataAPI.emptycolmetadata!(x::TestMeta) = empty!(x.col)

@testset "DataAPI" begin

@testset "defaultarray" begin
Expand Down Expand Up @@ -173,4 +246,62 @@ end
@test DataAPI.unwrap(missing) === missing
end

@testset "metadata" begin
@test_throws ArgumentError DataAPI.metadata!(1, "a", 10, style=:default)
@test_throws ArgumentError DataAPI.deletemetadata!(1, "a")
@test_throws ArgumentError DataAPI.emptymetadata!(1)
@test_throws ArgumentError DataAPI.metadata(1, "a")
@test_throws ArgumentError DataAPI.metadata(1, "a", style=true)
@test DataAPI.metadatakeys(1) == ()

@test_throws ArgumentError DataAPI.colmetadata!(1, :col, "a", 10, style=:default)
@test_throws ArgumentError DataAPI.deletecolmetadata!(1, :col, "a")
@test_throws ArgumentError DataAPI.emptycolmetadata!(1, :col)
@test_throws ArgumentError DataAPI.deletecolmetadata!(1, 1, "a")
@test_throws ArgumentError DataAPI.emptycolmetadata!(1, 1)
@test_throws ArgumentError DataAPI.emptycolmetadata!(1)
@test_throws ArgumentError DataAPI.colmetadata(1, :col, "a")
@test_throws ArgumentError DataAPI.colmetadata(1, :col, "a", style=true)
@test_throws ArgumentError DataAPI.colmetadata!(1, 1, "a", 10, style=:default)
@test_throws ArgumentError DataAPI.colmetadata(1, 1, "a")
@test_throws ArgumentError DataAPI.colmetadata(1, 1, "a", style=true)
@test DataAPI.colmetadatakeys(1, :col) == ()
@test DataAPI.colmetadatakeys(1, 1) == ()
@test DataAPI.colmetadatakeys(1) == ()

tm = TestMeta()
@test isempty(DataAPI.metadatakeys(tm))
@test DataAPI.metadata!(tm, "a", "100", style=:note) == tm
@test collect(DataAPI.metadatakeys(tm)) == ["a"]
@test_throws KeyError DataAPI.metadata(tm, "b")
@test_throws KeyError DataAPI.metadata(tm, "b", style=true)
@test DataAPI.metadata(tm, "a") == "100"
@test DataAPI.metadata(tm, "a", style=true) == ("100", :note)
DataAPI.deletemetadata!(tm, "a")
@test isempty(DataAPI.metadatakeys(tm))
@test DataAPI.metadata!(tm, "a", "100", style=:note) == tm
DataAPI.emptymetadata!(tm)
@test isempty(DataAPI.metadatakeys(tm))

@test DataAPI.colmetadatakeys(tm) == ()
@test DataAPI.colmetadatakeys(tm, :col) == ()
@test DataAPI.colmetadata!(tm, :col, "a", "100", style=:note) == tm
@test [k => collect(v) for (k, v) in DataAPI.colmetadatakeys(tm)] == [:col => ["a"]]
@test collect(DataAPI.colmetadatakeys(tm, :col)) == ["a"]
@test_throws KeyError DataAPI.colmetadata(tm, :col, "b")
@test_throws KeyError DataAPI.colmetadata(tm, :col, "b", style=true)
@test_throws KeyError DataAPI.colmetadata(tm, :col2, "a")
@test_throws KeyError DataAPI.colmetadata(tm, :col2, "a", style=true)
@test DataAPI.colmetadata(tm, :col, "a") == "100"
@test DataAPI.colmetadata(tm, :col, "a", style=true) == ("100", :note)
DataAPI.deletecolmetadata!(tm, :col, "a")
@test isempty(DataAPI.colmetadatakeys(tm, :col))
@test DataAPI.colmetadata!(tm, :col, "a", "100", style=:note) == tm
DataAPI.emptycolmetadata!(tm, :col)
@test isempty(DataAPI.colmetadatakeys(tm, :col))
@test DataAPI.colmetadata!(tm, :col, "a", "100", style=:note) == tm
DataAPI.emptycolmetadata!(tm)
@test isempty(DataAPI.colmetadatakeys(tm))
end

end # @testset "DataAPI"

2 comments on commit 32ef840

@bkamins
Copy link
Member Author

Choose a reason for hiding this comment

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

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/68585

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.11.0 -m "<description of version>" 32ef8404f076f82635d7ccc9e316a97bc09450af
git push origin v1.11.0

Please sign in to comment.