diff --git a/Project.toml b/Project.toml index 6ae5064..9bd31f2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PropertyDicts" uuid = "f8a19df8-e894-5f55-a973-672c1158cbca" license = "MIT" -version = "0.2" +version = "0.2.1" [compat] julia = "1.6" diff --git a/README.md b/README.md index f5f2ed3..dd1fe74 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # PropertyDicts.jl +[![Build Status](https://github.com/JuliaCollections/PropertyDicts.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaCollections/PropertyDicts.jl/actions/workflows/CI.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/JuliaCollections/PropertyDicts.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaCollections/PropertyDicts.jl) + Wrap an `AbstractDict` to add `getproperty` support for `Symbol` and `String` keys. ```julia diff --git a/src/PropertyDicts.jl b/src/PropertyDicts.jl index d427314..2218e20 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -2,9 +2,7 @@ module PropertyDicts export PropertyDict -@static if !hasmethod(reverse, Tuple{NamedTuple}) - Base.reverse(nt::NamedTuple) = NamedTuple{reverse(keys(nt))}(reverse(values(nt))) -end +#region catch Julia versions missing these @static if !hasmethod(mergewith, Tuple{Any,NamedTuple,NamedTuple}) function Base.mergewith(combine, a::NamedTuple{an}, b::NamedTuple{bn}) where {an, bn} names = Base.merge_names(an, bn) @@ -22,27 +20,136 @@ end end) end end +@static if !hasmethod(Base.setindex, Tuple{AbstractDict,Any,Any}) + function setindex(d::Base.ImmutableDict, v, k) + if isdefined(d, :parent) + if isequal(d.key, k) + d0 = d.parent + v0 = v + k0 = k + else + d0 = setindex(d.parent, v, k) + v0 = d.value + k0 = d.key + end + else + d0 = d + v0 = v + k0 = k + end + K = promote_type(keytype(d0), typeof(k0)) + V = promote_type(valtype(d0), typeof(v0)) + Base.ImmutableDict{K,V}(d0, k0, v0) + end + function setindex(src::AbstractDict{K,V}, val::V, key::K) where {K,V} + dst = copy(src) + dst[key] = val + return dst + end + function setindex(src::AbstractDict{K,V}, val, key) where {K,V} + dst = empty(src, promote_type(K,typeof(key)), promote_type(V,typeof(val))) + if haslength(src) + sizehint!(dst, length(src)) + end + for (k,v) in src + dst[k] = v + end + dst[key] = val + return dst + end +end +if isdefined(Base, :delete) + import Base: delete +else + delete(collection, k) = delete!(copy(collection), k) + function delete(d::Base.ImmutableDict{K,V}, key) where {K,V} + if isdefined(d, :parent) + if isequal(d.key, key) + d.parent + else + Base.ImmutableDict{K,V}(delete(d.parent, key), d.key, d.value) + end + else + d + end + end + function delete(nt::NamedTuple{syms}, key::Symbol) where {syms} + idx = Base.fieldindex(typeof(nt), key, false) + if idx === 0 + return nt + else + nv = Val{nfields(syms) - 1}() + NamedTuple{ + ntuple(j -> j < idx ? getfield(syms, j) : getfield(syms, j + 1), nv) + }(ntuple(j -> j < idx ? getfield(nt, j) : getfield(nt, j + 1), nv)) + end + end +end +#endregion -struct PropertyDict{K<:Union{String,Symbol}, V, D <: Union{AbstractDict,NamedTuple}} <: AbstractDict{K, V} +struct PropertyDict{K<:Union{String,Symbol}, V, D <: Union{AbstractDict{K,V},NamedTuple{<:Any,<:Tuple{Vararg{V}}}}} <: AbstractDict{K, V} d::D - PropertyDict(@nospecialize pd::PropertyDict) = pd - PropertyDict(d::AbstractDict{String,V}) where {V} = new{String,V,typeof(d)}(d) - PropertyDict(d::AbstractDict{Symbol,V}) where {V} = new{Symbol,V,typeof(d)}(d) - PropertyDict(nt::NamedTuple) = new{Symbol,eltype(nt),typeof(nt)}(nt) - function PropertyDict(d::AbstractDict) - dsym = Dict{Symbol,valtype(d)}() + # PropertyDict{K,V}(args...) + PropertyDict{Symbol,V}(d::AbstractDict{Symbol,V}) where {V} = new{Symbol,V,typeof(d)}(d) + PropertyDict{String,V}(d::AbstractDict{String,V}) where {V} = new{String,V,typeof(d)}(d) + PropertyDict{Symbol,V}(pd::PropertyDict{Symbol,V}) where {V} = pd + PropertyDict{String,V}(pd::PropertyDict{String,V}) where {V} = pd + function PropertyDict{K,V}(@nospecialize d::PropertyDict) where {K,V} + PropertyDict{K,V}(getfield(d, :d)) + end + function PropertyDict{K,V}(d::AbstractDict) where {K,V} + dsym = PropertyDict(Dict{K,V}()) for (k,v) in d - dsym[Symbol(k)] = v + dsym[K(k)] = v end - PropertyDict(dsym) + dsym + end + function PropertyDict{Symbol,V}(nt::NamedTuple{syms,<:Tuple{Vararg{V}}}) where {syms,V} + new{Symbol,V,typeof(nt)}(nt) + end + function PropertyDict{Symbol,V}(nt::NamedTuple{syms}) where {V,syms} + PropertyDict{Symbol,V}(NamedTuple{syms}(Tuple{Vararg{V}}(Tuple(nt)))) + end + PropertyDict{K,V}(arg, args...) where {K,V} = PropertyDict{K,V}(Dict(arg, args...)) + PropertyDict{K,V}(; kwargs...) where {K,V} = PropertyDict{K,V}(values(kwargs)) + + # PropertyDict{K}(args...) + function PropertyDict{K}(@nospecialize(d::AbstractDict)) where {K} + PropertyDict{K,valtype(d)}(d) end + function PropertyDict{String}(@nospecialize(d::AbstractDict{String})) + new{String,valtype(d),typeof(d)}(d) + end + function PropertyDict{Symbol}(@nospecialize(d::AbstractDict{Symbol})) + new{Symbol,valtype(d),typeof(d)}(d) + end + PropertyDict{Symbol}(@nospecialize(d::NamedTuple)) = new{Symbol,eltype(d),typeof(d)}(d) + PropertyDict{Symbol}(@nospecialize(pd::PropertyDict{Symbol})) = pd + PropertyDict{String}(@nospecialize(pd::PropertyDict{String})) = pd + PropertyDict{K}(arg, args...) where {K} = PropertyDict{K}(Dict(arg, args...)) + PropertyDict{K}(; kwargs...) where {K} = PropertyDict{K}(values(kwargs)) + + # PropertyDict(args...) + PropertyDict(@nospecialize pd::PropertyDict) = pd + PropertyDict(@nospecialize d::AbstractDict{String}) = PropertyDict{String}(d) + function PropertyDict(@nospecialize d::Union{AbstractDict{Symbol},NamedTuple}) + PropertyDict{Symbol}(d) + end + PropertyDict(@nospecialize d::AbstractDict) = PropertyDict{Symbol}(d) PropertyDict(arg, args...) = PropertyDict(Dict(arg, args...)) PropertyDict(; kwargs...) = PropertyDict(values(kwargs)) end const NamedProperties{syms,T<:Tuple,V} = PropertyDict{Symbol,V,NamedTuple{syms,T}} +@inline function Base.setindex(pd::PropertyDict, val, key::Union{Symbol,AbstractString}) + Base.setindex(getfield(pd, :d), val, _tokey(pd, key)) +end +@inline function delete(pd::PropertyDict, key::Union{Symbol,AbstractString}) + delete(getfield(pd, :d), _tokey(pd, key)) +end + Base.IteratorSize(@nospecialize T::Type{<:PropertyDict}) = Base.IteratorSize(fieldtype(T, :d)) Base.IteratorEltype(@nospecialize T::Type{<:PropertyDict}) = Base.IteratorEltype(eltype(T)) @@ -68,26 +175,26 @@ function Base.empty!(pd::PropertyDict) empty!(getfield(pd, :d)) return pd end +Base.isempty(::NamedProperties{(),Tuple{},Union{}}) = true +Base.isempty(@nospecialize(npd::NamedProperties)) = false Base.isempty(pd::PropertyDict) = isempty(getfield(pd, :d)) function Base.empty(pd::PropertyDict, ::Type{K}=keytype(pd), ::Type{V}=valtype(pd)) where {K,V} PropertyDict(empty(getfield(pd, :d), K, V)) end -Base.empty(pd::NamedProperties, ::Type{K}, ::Type{V}) where {K,V} = PropertyDict() +function Base.empty(@nospecialize(pd::NamedProperties), ::Type{K}, ::Type{V}) where {K,V} + PropertyDict() +end function Base.delete!(pd::PropertyDict, k) delete!(getfield(pd, :d), _tokey(pd, k)) return pd end -function Base.get(pd::PropertyDict, k, d) - get(getfield(pd, :d), _tokey(pd, k), d) -end +Base.get(pd::PropertyDict, k, d) = get(getfield(pd, :d), _tokey(pd, k), d) function Base.get(f::Union{Function,Type}, pd::PropertyDict, k) get(f, getfield(pd, :d), _tokey(pd, k)) end -function Base.get!(pd::PropertyDict, k, d) - get!(getfield(pd, :d), _tokey(pd, k), d) -end +Base.get!(pd::PropertyDict, k, d) = get!(getfield(pd, :d), _tokey(pd, k), d) function Base.get!(f::Union{Function,Type}, pd::PropertyDict, k) get!(f, getfield(pd, :d), _tokey(pd, k)) end @@ -101,8 +208,6 @@ Base.@propagate_inbounds function Base.setindex!(pd::PropertyDict, v, k) setindex!(getfield(pd, :d), v, _tokey(pd, k)) end -Base.reverse(pd::PropertyDict) = PropertyDict(reverse(getfield(pd, :d))) - @inline function Base.iterate(pd::NamedProperties) if isempty(pd) nothing @@ -110,7 +215,7 @@ Base.reverse(pd::PropertyDict) = PropertyDict(reverse(getfield(pd, :d))) Pair{Symbol,valtype(pd)}(getfield(keys(pd), 1), getfield(getfield(pd, :d), 1)), 2 end end -@inline function Base.iterate(pd::NamedProperties, s::Int) where {V} +@inline function Base.iterate(pd::NamedProperties, s::Int) if length(pd) < s nothing else @@ -131,9 +236,9 @@ Base.hasproperty(pd::PropertyDict, k::AbstractString) = haskey(pd, _tokey(pd, k) Base.propertynames(pd::PropertyDict) = keys(getfield(pd, :d)) Base.getproperty(pd::NamedProperties, k::Symbol) = getfield(getfield(pd, :d), k) Base.getproperty(pd::PropertyDict, k::Symbol) = getindex(pd, k) -Base.getproperty(pd::PropertyDict, k::String) = getindex(pd, k) +Base.getproperty(pd::PropertyDict, k::AbstractString) = getindex(pd, k) Base.setproperty!(pd::PropertyDict, k::Symbol, v) = setindex!(pd, v, k) -Base.setproperty!(pd::PropertyDict, k::String, v) = setindex!(pd, v, k) +Base.setproperty!(pd::PropertyDict, k::AbstractString, v) = setindex!(pd, v, k) Base.copy(pd::NamedProperties) = pd Base.copy(pd::PropertyDict) = PropertyDict(copy(getfield(pd, :d))) diff --git a/test/runtests.jl b/test/runtests.jl index 866bb9c..1a1d56f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,130 +2,151 @@ using OrderedCollections: OrderedDict using PropertyDicts using Test -@testset "PropertyDicts" begin - d = Dict("foo"=>1, :bar=>2) - _keys = collect(keys(d)) - pd = PropertyDict(d) +@testset "constructors" begin + @test isa(PropertyDict{Symbol}(PropertyDict(foo = 1, bar = 2)), PropertyDict{Symbol}) + @test isa(PropertyDict{Symbol,Int}(PropertyDict(foo = 1, bar = 2)), PropertyDict{Symbol,Int}) + @test isa(PropertyDict{Symbol,Int}(PropertyDict(foo = 1, bar = 2)), PropertyDict{Symbol, Int64, NamedTuple{(:foo, :bar), Tuple{Int64, Int64}}}) + @test isa(PropertyDict{Symbol,Int}(foo = 1, bar = 2.0), PropertyDict{Symbol, Int64, NamedTuple{(:foo, :bar), Tuple{Int64, Int64}}}) + @test isa(PropertyDict{Symbol,Int}(foo = 1, bar = 2.0), PropertyDict{Symbol,Int}) + @test isa(PropertyDict{Symbol,Int}(Dict{Symbol,Int}()), PropertyDict{Symbol,Int}) + @test isa(PropertyDict{Symbol,Int}(PropertyDict{Symbol}(foo = 1, bar = 2.0)), PropertyDict{Symbol,Int}) + @test isa(PropertyDict{Symbol,Int}(Dict(:foo => 1, :bar => 2)), PropertyDict{Symbol,Int}) + + @test isa(PropertyDict{String}("foo" => 1, "bar" => 2, "buz" => 3), PropertyDict{String,Int,Dict{String,Int}}) + @test isa(PropertyDict{String,Int}(Dict("foo" => 1, "bar" => 2)), PropertyDict{String,Int,Dict{String,Int}}) + @test isa(PropertyDict{String}(PropertyDict{String,Int}("foo" => 1, "bar" => 2)), PropertyDict{String,Int,Dict{String,Int}}) + @test isa(PropertyDict{String,Int}(PropertyDict{String,Int}(Dict("foo" => 1, "bar" => 2))), PropertyDict{String,Int,Dict{String,Int}}) +end - str_props = PropertyDict("foo" => 1, "bar" => 2) - sym_props = PropertyDict(:foo => 1, :bar => 2) +d = Dict("foo"=>1, :bar=>2) +_keys = collect(keys(d)) +pd = PropertyDict(d) - nt = (d =1, ) - ntpd = PropertyDict(nt) +str_props = PropertyDict("foo" => 1, "bar" => 2) +sym_props = PropertyDict(:foo => 1, :bar => 2) - @test length(pd) == length(d) +nt = (d =1, ) +ntpd = PropertyDict(nt) - @test values(PropertyDict(ntpd)) === values(nt) +@test length(pd) == length(d) +@test values(PropertyDict(ntpd)) === values(nt) - @test empty!(PropertyDict(Dict("foo"=>1, :bar=>2))) isa PropertyDict - @test empty(pd) == PropertyDict(empty(d)) - @test propertynames(PropertyDict(ntpd)) === propertynames(nt) +@test empty!(PropertyDict(Dict("foo"=>1, :bar=>2))) isa PropertyDict +@test empty(pd) == PropertyDict(empty(d)) - @test ntpd.d === nt.d - @test keys(PropertyDict(ntpd)) === keys(nt) - @test hasproperty(sym_props, "bar") - @test keytype(str_props) <: String - @test keytype(sym_props) <: Symbol - @test hasproperty(str_props, :bar) - @test hasproperty(pd, :foo) - @test hasproperty(pd, "bar") - @test haskey(pd, :foo) - @test getkey(pd, :buz, nothing) === nothing +@test propertynames(PropertyDict(ntpd)) === propertynames(nt) - @testset "convert" begin - expected = OrderedDict - result = convert(expected, pd) +@test ntpd.d === nt.d +@test keys(PropertyDict(ntpd)) === keys(nt) +@test hasproperty(sym_props, "bar") +@test keytype(str_props) <: String +@test keytype(sym_props) <: Symbol +@test hasproperty(str_props, :bar) +@test hasproperty(pd, :foo) +@test hasproperty(pd, "bar") +@test haskey(pd, :foo) +@test getkey(pd, :buz, nothing) === nothing - @test result isa expected - end +@testset "convert" begin + expected = OrderedDict + result = convert(expected, pd) - @testset "get" begin - @testset "default value" begin - default = "baz" - - @test get(pd, "DNE", default) == default - @test get(() -> 3, str_props, :foo) == 1 - @test get(() -> 3, str_props, :baz) == 3 - @test get(str_props, :baz, 3) == 3 - @test get(sym_props, "baz", 3) == 3 - @test get!(str_props, :baz, 3) == 3 - @test get!(sym_props, "baz", 3) == 3 - @test get!(() -> 4, str_props, :baz) == 3 - @test get!(() -> 4, sym_props, "baz") == 3 - @test get!(() -> 4, sym_props, "buz") == 4 - end - - @testset "$(typeof(key))" for key in _keys - @test get(pd, key, "DNE") == d[key] - end + @test result isa expected +end + +@testset "get" begin + @testset "default value" begin + default = "baz" + + @test get(pd, "DNE", default) == default + @test get(() -> 3, str_props, :foo) == 1 + @test get(() -> 3, str_props, :baz) == 3 + @test get(str_props, :baz, 3) == 3 + @test get(sym_props, "baz", 3) == 3 + @test get!(str_props, :baz, 3) == 3 + @test get!(sym_props, "baz", 3) == 3 + @test get!(() -> 4, str_props, :baz) == 3 + @test get!(() -> 4, sym_props, "baz") == 3 + @test get!(() -> 4, sym_props, "buz") == 4 end - @testset "getindex - $key" for key in _keys - @test getindex(pd, key) == getindex(d, key) + @testset "$(typeof(key))" for key in _keys + @test get(pd, key, "DNE") == d[key] end +end - @testset "getproperty" begin - @test pd.foo == 1 - @test pd.bar == 2 - sym_props."spam" = 4 - @test sym_props."spam" == 4 +@testset "getindex - $key" for key in _keys + @test getindex(pd, key) == getindex(d, key) +end - str_props.spam = 4 - @test str_props.spam == 4 - end +@testset "getproperty" begin + @test pd.foo == 1 + @test pd.bar == 2 + sym_props."spam" = 4 + @test sym_props."spam" == 4 - @testset "iterate" begin - @test iterate(pd) == iterate(d) - @test iterate(pd, 1) == iterate(d, 1) - @test iterate(pd, 2) == iterate(d, 2) - end + str_props.spam = 4 + @test str_props.spam == 4 +end - @testset "iteratorsize" begin - @test Base.IteratorSize(pd) == Base.IteratorSize(d) - end +@testset "iterate" begin + @test iterate(pd) == iterate(d) + @test iterate(pd, 1) == iterate(d, 1) + @test iterate(pd, 2) == iterate(d, 2) +end - @testset "iteratoreltype" begin - @test Base.IteratorEltype(pd) == Base.IteratorEltype(d) - end +@testset "iteratorsize" begin + @test Base.IteratorSize(pd) == Base.IteratorSize(d) +end - @test reverse(PropertyDict((a=1, b=2, c=3))) === PropertyDict(reverse((a=1, b=2, c=3))) +@testset "iteratoreltype" begin + @test Base.IteratorEltype(pd) == Base.IteratorEltype(d) +end - push!(pd, :buz => 10) - @test pop!(pd, :buz) == 10 - @test pop!(pd, :buz, 20) == 20 - @test sizehint!(pd, 5) === pd - @test get(pd, delete!(pd, "foo"), 10) == 10 +push!(pd, :buz => 10) +@test pop!(pd, :buz) == 10 +@test pop!(pd, :buz, 20) == 20 +@test sizehint!(pd, 5) === pd +@test get(pd, delete!(pd, "foo"), 10) == 10 + +@testset "NamedProperties" begin + pd = PropertyDict(x=1) + @test copy(pd) == pd + @test empty(pd) === PropertyDict() + @test pd[:x] == 1 +end - @testset "NamedProperties" begin - pd = PropertyDict(x=1) - @test copy(pd) == pd - @test empty(pd) === PropertyDict() - @test pd[:x] == 1 - end +@testset "merge & mergewith" begin + a = PropertyDict((a=1, b=2, c=3)) + b = PropertyDict((b=4, d=5)) + c = PropertyDict((a=1, b=2)) + d = PropertyDict((b=3, c=(d=1,))) + e = PropertyDict((c=(d=2,),)) + f = PropertyDict(Dict("foo"=>1, "bar"=>2)) + + @test merge(a) === a + @test f !== merge(f) == f + @test @inferred(merge(a, b)) == PropertyDict((a = 1, b = 4, c = 3, d = 5)) + @test @inferred(merge(c, d, e)) == PropertyDict((a = 1, b = 3, c = (d = 2,))) + @test merge(a, f, c) == merge(f, a, c) + + @test mergewith(+, a) == a + @test mergewith(+, f) == PropertyDict(Dict("foo"=>1, "bar"=>2)) + @test mergewith(+, f, f) == PropertyDict(Dict("foo"=>2, "bar"=>4)) + @test mergewith(+, a, b) == PropertyDict(a=1, b=6, c=3, d=5) + combiner(x, y) = "$(x) and $(y)" + @test mergewith(combiner, a, f, c, PropertyDict()) == + PropertyDict(:a=>"1 and 1", :b=>"2 and 2", :c=>3, :bar=>2, :foo=>1) + @test @inferred(mergewith(combiner, a, b, c, PropertyDict())) == + PropertyDict((a = "1 and 1", b = "2 and 4 and 2", c = 3, d = 5)) +end - @testset "merge & mergewith" begin - a = PropertyDict((a=1, b=2, c=3)) - b = PropertyDict((b=4, d=5)) - c = PropertyDict((a=1, b=2)) - d = PropertyDict((b=3, c=(d=1,))) - e = PropertyDict((c=(d=2,),)) - f = PropertyDict(Dict("foo"=>1, "bar"=>2)) - - @test merge(a) === a - @test f !== merge(f) == f - @test @inferred(merge(a, b)) == PropertyDict((a = 1, b = 4, c = 3, d = 5)) - @test @inferred(merge(c, d, e)) == PropertyDict((a = 1, b = 3, c = (d = 2,))) - @test merge(a, f, c) == merge(f, a, c) - - @test mergewith(+, a) == a - @test mergewith(+, f, f) == PropertyDict(Dict("foo"=>2, "bar"=>4)) - @test mergewith(+, a, b) == PropertyDict(a=1, b=6, c=3, d=5) - combiner(x, y) = "$(x) and $(y)" - @test mergewith(combiner, a, f, c, PropertyDict()) == - PropertyDict(:a=>"1 and 1", :b=>"2 and 2", :c=>3, :bar=>2, :foo=>1) - @test @inferred(mergewith(combiner, a, b, c, PropertyDict())) == - PropertyDict((a = "1 and 1", b = "2 and 4 and 2", c = 3, d = 5)) - end +@testset "non-mutating modifiers" begin + pd1 = Base.setindex(PropertyDict(), 1, :x) + pd2 = Base.setindex(pd1, 2, :y) + @test values(pd2) == (1, 2) + @test keys(pd2) == (:x, :y) + @test PropertyDicts.delete(pd2, :y) == pd1 end