From 463b5e37675f6bd846b1508ef2ff4ab10cd96b11 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 04:23:52 -0400 Subject: [PATCH 1/9] More constructors for `PropertyDict` --- Project.toml | 2 +- src/PropertyDicts.jl | 58 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 12 deletions(-) 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/src/PropertyDicts.jl b/src/PropertyDicts.jl index d427314..c5e3c18 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -23,20 +23,52 @@ end end end -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{K,V}(d::AbstractDict{K,V}) where {K,V} = new{K,V,typeof(d)}(d) + function PropertyDict{K,V}(d::AbstractDict) where {K,V} + dsym = Dict{K,V}() for (k,v) in d - dsym[Symbol(k)] = v + dsym[K(k)] = v end PropertyDict(dsym) end + PropertyDict{K,V}(d::PropertyDict{K,V}) where {K,V} = d + function PropertyDict{K,V}(@nospecialize(d::PropertyDict)) where {K,V} + PropertyDict{K,V}(getfield(d, :d)) + 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...) + PropertyDict{K}(@nospecialize(d::AbstractDict)) where {K} = PropertyDict{K,valtype(d)}(d) + 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 @@ -68,11 +100,15 @@ 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)) @@ -131,9 +167,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))) From c999afb7b2656ff47f7c0461ce0bb2410d4f5a7c Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 06:43:46 -0400 Subject: [PATCH 2/9] More tests + README badges --- README.md | 3 + src/PropertyDicts.jl | 11 ++- test/runtests.jl | 217 ++++++++++++++++++++++--------------------- 3 files changed, 123 insertions(+), 108 deletions(-) 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 c5e3c18..497c629 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -27,13 +27,14 @@ struct PropertyDict{K<:Union{String,Symbol}, V, D <: Union{AbstractDict{K,V},Nam d::D # PropertyDict{K,V}(args...) - PropertyDict{K,V}(d::AbstractDict{K,V}) where {K,V} = new{K,V,typeof(d)}(d) + PropertyDict{Symbol,V}(d::AbstractDict{K,V}) where {V} = new{Symbol,V,typeof(d)}(d) + PropertyDict{String,V}(d::AbstractDict{K,V}) where {V} = new{String,V,typeof(d)}(d) function PropertyDict{K,V}(d::AbstractDict) where {K,V} - dsym = Dict{K,V}() + dsym = PropertyDict(Dict{K,V}()) for (k,v) in d dsym[K(k)] = v end - PropertyDict(dsym) + dsym end PropertyDict{K,V}(d::PropertyDict{K,V}) where {K,V} = d function PropertyDict{K,V}(@nospecialize(d::PropertyDict)) where {K,V} @@ -49,7 +50,9 @@ struct PropertyDict{K<:Union{String,Symbol}, V, D <: Union{AbstractDict{K,V},Nam PropertyDict{K,V}(; kwargs...) where {K,V} = PropertyDict{K,V}(values(kwargs)) # PropertyDict{K}(args...) - PropertyDict{K}(@nospecialize(d::AbstractDict)) where {K} = PropertyDict{K,valtype(d)}(d) + 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 diff --git a/test/runtests.jl b/test/runtests.jl index 866bb9c..6254892 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,131 +1,140 @@ -using OrderedCollections: OrderedDict +sing 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 + strpd = PropertyDict{String}("foo" => 1, "bar" => 2) + sympd = PropertyDict{Symbol}(foo = 1, bar = 2) - str_props = PropertyDict("foo" => 1, "bar" => 2) - sym_props = PropertyDict(:foo => 1, :bar => 2) + @test PropertyDict{String}(strpd) == strpd + @test sympd == + PropertyDict{Symbol}(sympd) == + PropertyDict{Symbol,Int}(sympd) == + PropertyDict{Symbol,Int}(foo = 1, y = 2.0) # convert eltype of NamedTuple at construction +end - nt = (d =1, ) - ntpd = PropertyDict(nt) +d = Dict("foo"=>1, :bar=>2) +_keys = collect(keys(d)) +pd = PropertyDict(d) - @test length(pd) == length(d) +str_props = PropertyDict("foo" => 1, "bar" => 2) +sym_props = PropertyDict(:foo => 1, :bar => 2) - @test values(PropertyDict(ntpd)) === values(nt) +nt = (d =1, ) +ntpd = PropertyDict(nt) +@test length(pd) == length(d) - @test empty!(PropertyDict(Dict("foo"=>1, :bar=>2))) isa PropertyDict - @test empty(pd) == PropertyDict(empty(d)) +@test values(PropertyDict(ntpd)) === values(nt) - @test propertynames(PropertyDict(ntpd)) === propertynames(nt) - @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 empty!(PropertyDict(Dict("foo"=>1, :bar=>2))) isa PropertyDict +@test empty(pd) == PropertyDict(empty(d)) - @testset "convert" begin - expected = OrderedDict - result = convert(expected, pd) +@test propertynames(PropertyDict(ntpd)) === propertynames(nt) - @test result isa expected - end +@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 + +@testset "convert" begin + expected = OrderedDict + result = convert(expected, pd) + + @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 "$(typeof(key))" for key in _keys - @test get(pd, key, "DNE") == d[key] - 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 +@test reverse(PropertyDict((a=1, b=2, c=3))) === PropertyDict(reverse((a=1, b=2, c=3))) - @testset "NamedProperties" begin - pd = PropertyDict(x=1) - @test copy(pd) == pd - @test empty(pd) === PropertyDict() - @test pd[:x] == 1 - 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 - @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 "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, 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 From 3f54d08e61937e1443ca374c92c710fd587a461f Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 07:39:49 -0400 Subject: [PATCH 3/9] Fix ambiguities --- src/PropertyDicts.jl | 13 +++++++------ test/runtests.jl | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PropertyDicts.jl b/src/PropertyDicts.jl index 497c629..53ba68a 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -27,8 +27,13 @@ struct PropertyDict{K<:Union{String,Symbol}, V, D <: Union{AbstractDict{K,V},Nam d::D # PropertyDict{K,V}(args...) - PropertyDict{Symbol,V}(d::AbstractDict{K,V}) where {V} = new{Symbol,V,typeof(d)}(d) - PropertyDict{String,V}(d::AbstractDict{K,V}) where {V} = new{String,V,typeof(d)}(d) + 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 @@ -36,10 +41,6 @@ struct PropertyDict{K<:Union{String,Symbol}, V, D <: Union{AbstractDict{K,V},Nam end dsym end - PropertyDict{K,V}(d::PropertyDict{K,V}) where {K,V} = d - function PropertyDict{K,V}(@nospecialize(d::PropertyDict)) where {K,V} - PropertyDict{K,V}(getfield(d, :d)) - end function PropertyDict{Symbol,V}(nt::NamedTuple{syms,<:Tuple{Vararg{V}}}) where {syms,V} new{Symbol,V,typeof(nt)}(nt) end diff --git a/test/runtests.jl b/test/runtests.jl index 6254892..d0997bc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -sing OrderedCollections: OrderedDict +using OrderedCollections: OrderedDict using PropertyDicts using Test @@ -10,7 +10,7 @@ using Test @test sympd == PropertyDict{Symbol}(sympd) == PropertyDict{Symbol,Int}(sympd) == - PropertyDict{Symbol,Int}(foo = 1, y = 2.0) # convert eltype of NamedTuple at construction + PropertyDict{Symbol,Int}(foo = 1, bar = 2.0) # convert eltype of NamedTuple at construction end d = Dict("foo"=>1, :bar=>2) From 2c5f2f64b08c905170c4b173e6739cd7b8d068b1 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 17:07:58 -0400 Subject: [PATCH 4/9] remove unused type variable --- src/PropertyDicts.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PropertyDicts.jl b/src/PropertyDicts.jl index 53ba68a..de84db5 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -150,7 +150,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 From 7be7ba6ca1fea87896e6696f1c100846fa7ffc0f Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 18:32:16 -0400 Subject: [PATCH 5/9] Ensure constructors return proper type --- test/runtests.jl | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index d0997bc..b03cc2a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,14 +3,19 @@ using PropertyDicts using Test @testset "constructors" begin - strpd = PropertyDict{String}("foo" => 1, "bar" => 2) - sympd = PropertyDict{Symbol}(foo = 1, bar = 2) - - @test PropertyDict{String}(strpd) == strpd - @test sympd == - PropertyDict{Symbol}(sympd) == - PropertyDict{Symbol,Int}(sympd) == - PropertyDict{Symbol,Int}(foo = 1, bar = 2.0) # convert eltype of NamedTuple at construction + @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), PropertyDict{String,Int,Dict{String,Int}}) + @test isa(PropertyDict{String,Int}(Dict("foo" => 1, "bar" => 2)), PropertyDict{String,Int,Dict{String,Int}}) + # don't nest PropertyDict + @test isa(PropertyDict{String,Int}(PropertyDict{String,Int}(Dict("foo" => 1, "bar" => 2))), PropertyDict{String,Int,Dict{String,Int}}) end d = Dict("foo"=>1, :bar=>2) From ee52a216bf5be5ec3d73c621549e08d875e9e9d2 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 21:36:56 -0400 Subject: [PATCH 6/9] test mergewith when only one dictionary provided --- test/runtests.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index b03cc2a..bfb115b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,12 +12,16 @@ using Test @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), PropertyDict{String,Int,Dict{String,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}}) - # don't nest PropertyDict + @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 +# PropertyDict{K,V}(arg, args...) where {K,V} = PropertyDict{K,V}(Dict(arg, args...)) +# PropertyDict{String}(@nospecialize(pd::PropertyDict{String})) = pd + + d = Dict("foo"=>1, :bar=>2) _keys = collect(keys(d)) pd = PropertyDict(d) @@ -135,6 +139,7 @@ end @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)" From d127a792f0abd0f80b275ebacb5a88176ed393d8 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Fri, 23 Sep 2022 21:57:09 -0400 Subject: [PATCH 7/9] Base.setindex for `NamedProperties` --- src/PropertyDicts.jl | 10 ++++++++++ test/runtests.jl | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/PropertyDicts.jl b/src/PropertyDicts.jl index de84db5..1e6a724 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -79,6 +79,16 @@ end const NamedProperties{syms,T<:Tuple,V} = PropertyDict{Symbol,V,NamedTuple{syms,T}} +@inline function Base.setindex(npd::NamedProperties{syms}, v, key::Symbol) where {syms} + nt = getfield(npd, :d) + idx = Base.fieldindex(typeof(nt), key, false) + if idx === 0 + return PropertyDict(NamedTuple{(syms..., key)}((values(nt)..., v))) + else + return PropertyDict(NamedTuple{syms}(ntuple(i -> idx === i ? v : getfield(nt, i), Val{nfields(syms)}()))) + end +end + Base.IteratorSize(@nospecialize T::Type{<:PropertyDict}) = Base.IteratorSize(fieldtype(T, :d)) Base.IteratorEltype(@nospecialize T::Type{<:PropertyDict}) = Base.IteratorEltype(eltype(T)) diff --git a/test/runtests.jl b/test/runtests.jl index bfb115b..6457567 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -148,3 +148,9 @@ end @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 "setindex" begin + npd = Base.setindex(Base.setindex(PropertyDict(), 1, :x), 2, :y) + @test values(npd) == (1, 2) + @test keys(npd) == (:x, :y) +end From e6f5797b7f890a090eb4584770af31226d5bad78 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sun, 25 Sep 2022 02:08:51 -0400 Subject: [PATCH 8/9] Doesn't make sense to have reverse for dictionary --- src/PropertyDicts.jl | 9 ++------- test/runtests.jl | 6 ------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/PropertyDicts.jl b/src/PropertyDicts.jl index 1e6a724..fd85445 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -2,9 +2,6 @@ module PropertyDicts export PropertyDict -@static if !hasmethod(reverse, Tuple{NamedTuple}) - Base.reverse(nt::NamedTuple) = NamedTuple{reverse(keys(nt))}(reverse(values(nt))) -end @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) @@ -103,9 +100,9 @@ Base.keytype(@nospecialize T::Type{<:PropertyDict{String}}) = String Base.keytype(@nospecialize T::Type{<:PropertyDict{Symbol}}) = Symbol _tokey(@nospecialize(pd::PropertyDict{String}), k::AbstractString) = k -_tokey(@nospecialize(pd::PropertyDict{String}), k) = String(k) +_tokey(@nospecialize(pd::PropertyDict{String}), k::Symbol) = String(k) _tokey(@nospecialize(pd::PropertyDict{Symbol}), k::Symbol) = k -_tokey(@nospecialize(pd::PropertyDict{Symbol}), k) = Symbol(k) +_tokey(@nospecialize(pd::PropertyDict{Symbol}), k::AbstractString) = Symbol(k) Base.pop!(pd::PropertyDict, k) = pop!(getfield(pd, :d), _tokey(pd, k)) Base.pop!(pd::PropertyDict, k, d) = pop!(getfield(pd, :d), _tokey(pd, k), d) @@ -151,8 +148,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 diff --git a/test/runtests.jl b/test/runtests.jl index 6457567..31671a4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,10 +18,6 @@ using Test @test isa(PropertyDict{String,Int}(PropertyDict{String,Int}(Dict("foo" => 1, "bar" => 2))), PropertyDict{String,Int,Dict{String,Int}}) end -# PropertyDict{K,V}(arg, args...) where {K,V} = PropertyDict{K,V}(Dict(arg, args...)) -# PropertyDict{String}(@nospecialize(pd::PropertyDict{String})) = pd - - d = Dict("foo"=>1, :bar=>2) _keys = collect(keys(d)) pd = PropertyDict(d) @@ -109,8 +105,6 @@ end @test Base.IteratorEltype(pd) == Base.IteratorEltype(d) end -@test reverse(PropertyDict((a=1, b=2, c=3))) === PropertyDict(reverse((a=1, b=2, c=3))) - push!(pd, :buz => 10) @test pop!(pd, :buz) == 10 @test pop!(pd, :buz, 20) == 20 From 22b354b79d0a2bc6cb5214511f90a6b03e29a424 Mon Sep 17 00:00:00 2001 From: "Zachary P. Christensen" Date: Sun, 25 Sep 2022 04:57:18 -0400 Subject: [PATCH 9/9] Support non-mutating modifiers --- src/PropertyDicts.jl | 92 ++++++++++++++++++++++++++++++++++++-------- test/runtests.jl | 10 +++-- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/src/PropertyDicts.jl b/src/PropertyDicts.jl index fd85445..2218e20 100644 --- a/src/PropertyDicts.jl +++ b/src/PropertyDicts.jl @@ -2,6 +2,7 @@ module PropertyDicts export PropertyDict +#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) @@ -19,6 +20,72 @@ export PropertyDict 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{K,V},NamedTuple{<:Any,<:Tuple{Vararg{V}}}}} <: AbstractDict{K, V} d::D @@ -76,14 +143,11 @@ end const NamedProperties{syms,T<:Tuple,V} = PropertyDict{Symbol,V,NamedTuple{syms,T}} -@inline function Base.setindex(npd::NamedProperties{syms}, v, key::Symbol) where {syms} - nt = getfield(npd, :d) - idx = Base.fieldindex(typeof(nt), key, false) - if idx === 0 - return PropertyDict(NamedTuple{(syms..., key)}((values(nt)..., v))) - else - return PropertyDict(NamedTuple{syms}(ntuple(i -> idx === i ? v : getfield(nt, i), Val{nfields(syms)}()))) - end +@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)) @@ -100,9 +164,9 @@ Base.keytype(@nospecialize T::Type{<:PropertyDict{String}}) = String Base.keytype(@nospecialize T::Type{<:PropertyDict{Symbol}}) = Symbol _tokey(@nospecialize(pd::PropertyDict{String}), k::AbstractString) = k -_tokey(@nospecialize(pd::PropertyDict{String}), k::Symbol) = String(k) +_tokey(@nospecialize(pd::PropertyDict{String}), k) = String(k) _tokey(@nospecialize(pd::PropertyDict{Symbol}), k::Symbol) = k -_tokey(@nospecialize(pd::PropertyDict{Symbol}), k::AbstractString) = Symbol(k) +_tokey(@nospecialize(pd::PropertyDict{Symbol}), k) = Symbol(k) Base.pop!(pd::PropertyDict, k) = pop!(getfield(pd, :d), _tokey(pd, k)) Base.pop!(pd::PropertyDict, k, d) = pop!(getfield(pd, :d), _tokey(pd, k), d) @@ -126,15 +190,11 @@ function Base.delete!(pd::PropertyDict, 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 diff --git a/test/runtests.jl b/test/runtests.jl index 31671a4..1a1d56f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -143,8 +143,10 @@ end PropertyDict((a = "1 and 1", b = "2 and 4 and 2", c = 3, d = 5)) end -@testset "setindex" begin - npd = Base.setindex(Base.setindex(PropertyDict(), 1, :x), 2, :y) - @test values(npd) == (1, 2) - @test keys(npd) == (:x, :y) +@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