From 47ea4cda8b77e0115796dd67109199a55a0d08b2 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 10:25:09 -0400 Subject: [PATCH 1/7] rewrite working --- Project.toml | 7 +- src/EasyConfig.jl | 727 +++++++++++++++++++++++++++++++++++++++------- test/runtests.jl | 288 +++++++++--------- 3 files changed, 780 insertions(+), 242 deletions(-) diff --git a/Project.toml b/Project.toml index 8ac7d10..a1ac81f 100644 --- a/Project.toml +++ b/Project.toml @@ -4,14 +4,11 @@ authors = ["joshday "] version = "0.1.16" [deps] -JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" +StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b" [compat] -JSON3 = "1.4.0" -OrderedCollections = "1.3.2" -StructTypes = "1.1.0" +StyledStrings = "1.11.0" julia = "1" [extras] diff --git a/src/EasyConfig.jl b/src/EasyConfig.jl index d132ede..5d6e8ec 100644 --- a/src/EasyConfig.jl +++ b/src/EasyConfig.jl @@ -1,138 +1,661 @@ module EasyConfig -using OrderedCollections -using StructTypes -using JSON3 +export Config -export Config, delete_empty!, @config - -#-----------------------------------------------------------------------------# Config -""" - Config(; kw...) - Config(::AbstractDict) +#------------------------------------------------------------------------------# Undefined +# Lazy path of nested dicts +struct Undefined{T <: AbstractDict{Symbol, Any}} + parent::T + keys::Vector{Symbol} +end +parent(o::Undefined) = getfield(o, :parent) +keys(o::Undefined) = getfield(o, :keys) -A wrapper around an `OrderedDict{Symbol, Any}` with the special properties: +Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [keys(o)..., k]) +function Base.setindex!(o::Undefined{T}, v, key::Symbol) where {T} + c = parent(o) + for k in keys(o) + c[k] = T() + c = c[k] + end + c[key] = v + return parent(o) +end +Base.getproperty(o::Undefined, k::Symbol) = getindex(o, k) +Base.setproperty!(o::Undefined, k::Symbol, v) = setindex!(o, v, k) -1. You can get/set items via `getproperty`/`setproperty!`: - c = Config() - c.one = 1 - c.one == 1 +#------------------------------------------------------------------------------# Settings +@kwdef struct Settings + autoconvert::Vector{Union{UnionAll, DataType}} = [AbstractDict{Symbol}, NamedTuple] +end +settings::Settings = Settings() -2. Properties that don't exist will be created on the fly: +#------------------------------------------------------------------------------# Config +struct Config{D <: AbstractDict{Symbol, Any}} <: AbstractDict{Symbol, Any} + dict::D +end +Config() = Config(Dict{Symbol,Any}()) +Config{D}() where {D} = Config(D()) - c = Config() - c.one.two.three = "neat!" -""" -struct Config <: AbstractDict{Symbol, Any} - d::OrderedDict{Symbol, Any} - function Config(x::Union{NamedTuple, AbstractDict}) - d = OrderedDict{Symbol,Any}() - for (k,v) in pairs(x) - d[Symbol(k)] = _config_value(v) - end - new(d) +function Config(x::Pair...; kw...) + out = Config() + for (k, v) in Iterators.flatten((x, kw)) + out[k] = v end - Config(x::Config) = x + out end -Config(; kw...) = Config(kw) -Config(x...) = Config(OrderedDict(x...)) -_config_value(x) = x -_config_value(x::AbstractDict) = Config(x) -_config_value(x::Pair) = Config(Symbol(x[1]) => x[2]) -_config_value(x::AbstractArray) = map(_config_value, x) +dict(o::Config) = getfield(o, :dict) -OrderedCollections.isordered(::Type{Config}) = true +## Getting and Setting ## +Base.getindex(o::Config, k::Symbol) = get(o, k, Undefined(o, [k])) +Base.setindex!(o::Config, v, k::Symbol) = setindex!(dict(o), v, k) -delete_empty!(x) = x -function delete_empty!(o::Config) - for (k,v) in pairs(o) - v isa Config && isempty(v) ? delete!(o, k) : delete_empty!(v) - end - o +Base.getindex(o::Config, k::AbstractString) = getindex(o, Symbol(k)) +Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) + +Base.getproperty(o::Config, k::Symbol) = getindex(o, k) +Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) + +## AbstractDict interface ## +Base.get(o::Config, k::Symbol, default) = get(dict(o), k, default) +Base.iterate(o::Config, s...) = iterate(dict(o), s...) +Base.length(o::Config) = length(dict(o)) + + + +#------------------------------------------------------------------------------# show +# What gets used to show nested Configs by the AbstractDict show method: +function Base.show(io::IO, o::Config) + print(io, "(") + join(io, map(((k, v),) -> "$(repr(k)) => $(repr(v; context=io))", collect(o)), ", ") + print(io, ')') end -dict(o::Config) = getfield(o, :d) +# #-----------------------------------------------------------------------------# utils +# # Turn struct into NamedTuple +# fields(x::T) where {T} = NamedTuple{fieldnames(T)}(getfield.(Ref(x), fieldnames(T))) -StructTypes.StructType(::Type{Config}) = StructTypes.DictType() -Base.getproperty(o::Config, k::Symbol) = get!(o, k, Config()) -Base.getproperty(o::Config, k) = getproperty(o, Symbol(k)) +# #-----------------------------------------------------------------------------# Key +# """ +# Key(x::AbstractString; sep = :auto) +# Key(parts::Symbol...; sep::Char) -Base.setproperty!(o::Config, k::Symbol, v) = dict(o)[Symbol(k)] = v -Base.setproperty!(o::Config, k, v) = setproperty!(o, Symbol(k), v) +# An AbstractString represented internally as a `Vector{Symbol}` and a separator (`Char`). +# """ +# struct Key <: AbstractString +# parts::Vector{Symbol} +# sep::Char +# end +# Key(x::Symbol...; sep::Char='.') = Key(collect(x), sep) +# function Key(x::AbstractString; sep = :auto) +# if sep === :auto +# nonletters = filter(!isletter, x) +# countmap = Dict(x => count(==(x), nonletters) for x in unique(nonletters)) +# isempty(countmap) && return Key([Symbol(x)], '.') +# _, char = findmax(countmap) +# Key(Symbol.(split(x, char; keepempty=false)), char) +# else +# Key(Symbol.(split(x, sep; keepempty=false)), sep) +# end +# end -Base.propertynames(o::Config) = collect(keys(dict(o))) +# parts(o::Key) = getfield(o, :parts) +# sep(o::Key) = getfield(o, :sep) -Base.getindex(o::Config, k) = getproperty(o, Symbol(k)) -Base.setindex!(o::Config, v, k) = setproperty!(o, Symbol(k), v) +# Base.getproperty(o::Key, x::Symbol) = Key([parts(o)..., x], sep(o)) -Base.iterate(o::Config, args...) = iterate(dict(o), args...) -Base.keys(o::Config) = keys(dict(o)) -Base.values(o::Config) = values(dict(o)) -Base.length(o::Config) = length(dict(o)) -Base.isempty(o::Config) = isempty(dict(delete_empty!(o))) -Base.pairs(o::Config) = pairs(dict(o)) -Base.empty!(o::Config) = (empty!(dict(o)); o) -Base.get(o::Config, k, default) = get(dict(o), Symbol(k), default) -Base.get!(o::Config, k, default) = get!(dict(o), Symbol(k), default) -Base.haskey(o::Config, k) = haskey(dict(o), Symbol(k)) +# _string(o::Key) = join(parts(o), sep(o)) +# Base.show(io::IO, o::Key) = print(io, styled"{bright_cyan:$(repr(_string(o)))}") +# Base.ncodeunits(o::Key) = ncodeunits(_string(o)) +# Base.iterate(o::Key, i::Integer...) = iterate(_string(o), i...) +# Base.length(o::Key) = length(_string(o)) -Base.delete!(o::Config, k) = (delete!(dict(o), Symbol(k)); o) +# function Base.:*(a::Key, b::Key) +# sep(a) == sep(b) || error("Cannot join `Key`s with different separators. Found $(sep(a)) and $(sep(b)).") +# Key(vcat(parts(a), parts(b)), sep(a)) +# end -Base.isequal(a::Config, b::Config) = dict(a) == dict(b) -Base.copy(o::Config) = Config(copy(dict(o))) +# function strip_prefix(o::Key, prefix::Key) +# sep(o) == sep(prefix) || error("Cannot join `Key`s with different separators. Found $(sep(o)) and $(sep(prefix)).") +# all(x==y for (x, y) in zip(parts(o), parts(prefix))) || error("$prefix is not a prefix of $o") +# Key(parts(o)[length(parts(prefix)) + 1:end], sep(o)) +# end -Base.merge(a::Config, b::Config) = Config(merge(dict(a), dict(b))) +# parent(o::Key) = Key(parts(o)[1:end-1], sep(o)) -Base.merge!(a::Config, b::Config) = Config(merge!(dict(a), dict(b))) -#-----------------------------------------------------------------------------# @config -""" - @config expr +# #-----------------------------------------------------------------------------# ref interface +# is_key(o::Ref, key::Key) = true +# is_val(o::Ref, key::Key, val) = true +# about(ref, key::Key) = "No information provided for $key of $ref." -Create a `Config` with a NamedTuple-like or block syntax. The following examples create equivalent `Config`s: - @config (x.one=1, x.two=2, z=3) - @config x.one=1 x.two=2 z=3 +# # about(o::KV, key::Key) = about(ref(o), key) - @config begin - x.one = 1 - x.two = 2 - z = 3 - end +# function check(ref::T, key::Key) where {T} +# idx = findall(startswith(key), ref_keys(ref)) +# length(idx) > 0 || error("Key $key not found in reference $ref.") +# end - let - c = Config() - c.x.one = 1 - c.x.two = 2 - c.z = 3 - end -""" -macro config(ex...) - exprs = collect(ex) - if length(exprs) == 1 - ex = only(exprs) - if ex.head == :block - ex = Expr(:tuple, filter(x -> !(x isa LineNumberNode), ex.args)...) +# # function check(ref::T, key::Key, val) where {T} +# # check(ref, key) +# # if !is_valid(ref, key, val) +# # error("Reference::$T invalid value for key $key: $val. See `about(ref, key)`.") +# # end +# # end + +# # # # ref methods that use interface +# # # is_full_path(ref, o::Key) = o in ref_keys(ref) +# # # is_partial_path(ref, o::Key) = any(startswith(o), ref_keys(ref)) + + +# #-----------------------------------------------------------------------------# Undefined +# struct Undefined +# about +# end +# Base.show(io::IO, o::Undefined) = print(io, "Undefined: ", o.about) + +# #-----------------------------------------------------------------------------# KV +# struct KV{R} <: AbstractDict{Key, Any} +# keys::Vector{Key} +# values::Vector +# prefix::Key +# ref::R +# function KV(pairs::Pair...; ref::R = nothing, path=Key()) where {R} +# keys = collect(Key.(first.(pairs))) +# vals = collect(Any, last.(pairs)) +# new{R}(keys, vals, path, ref) +# end +# end + +# prefix(o::KV) = getfield(o, :prefix) +# ref(o) = getfield(o, :ref) +# root(o::KV) = KV(getfield(o, :keys), getfield(o, :values), Key(), ref(o)) +# parent(o::KV) = KV(getfield(o, :keys), getfield(o, :values), parent(prefix(o)), ref(o)) + +# key_idxs(o::KV, prefix::Key) = findall(startswith(prefix), getfield(o, :keys)) +# key_idxs(o::KV) = key_idxs(o, prefix(o)) + +# ref_keys(o::KV) = ref_keys(ref(o)) +# about(o::KV, k::Key) = about(ref(o), k) + +# function Base.keys(o::KV) +# full_keys = getfield(o, :keys)[key_idxs(o)] +# strip_prefix.(full_keys, prefix(o)) +# end +# Base.values(o::KV) = getfield(o, :values)[key_idxs(o)] + +# Base.iterate(o::KV, x...) = iterate(keys(o) .=> values(o), x...) +# Base.length(o::KV) = length(keys(o)) + + + +# function Base.getindex(o::KV{Nothing}, _k::AbstractString) +# k = prefix(o) * Key(_k) +# kys = getfield(o, :keys) +# # Case 1) Key already exists +# i = findfirst(==(k), kys) +# !isnothing(i) && return values(o)[i] + +# # Case 2) Full key exists in ref +# k in ref_keys(o) && return Undefined(about(o, k)) + +# # Case 3) k is a prefix of another key in ref +# any(startswith(k), kys) && return KV(kys, getfield(o, :values), k, ref(o)) + +# error("Key :$k not found in reference $(ref(o)).") +# end + +# function Base.setindex!(o::KV, v, _k::AbstractString) +# k = Key(_k) +# is_valid(ref(o), k, v) || error("Reference::$ref invalid value for key $k: $v. See `about(ref, $(repr(k)))`.") +# i = findfirst(==(k), keys(o)) +# if isnothing(i) +# push!(keys(o), k) +# push!(values(o), v) +# else +# keys(o)[i] = k +# values(o)[i] = v +# end +# end + +# function Base.propertynames(o::KV) +# map(keys(o)) do k +# parts(k)[1] +# end +# end +# Base.getproperty(o::KV, k::Symbol) = getindex(o, string(k)) + +# #-----------------------------------------------------------------------------# ref interface +# ref_keys(::Nothing) = Key[] +# is_valid(::Nothing, key::Key, val) = true +# about(ref, key::Key) = "No information provided for $key of $ref." + + + +# ref_keys(o::KV) = ref_keys(ref(o)) +# about(o::KV, key::Key) = about(ref(o), key) + +# function check(ref::T, key::Key) where {T} +# idx = findall(startswith(key), ref_keys(ref)) +# length(idx) > 0 || error("Key $key not found in reference $ref.") +# end + +# function check(ref::T, key::Key, val) where {T} +# check(ref, key) +# if !is_valid(ref, key, val) +# error("Reference::$T invalid value for key $key: $val. See `about(ref, key)`.") +# end +# end + + + + + + +#-----------------------------------------------------------------------------# Undefined +# struct Undefined{T} +# parent::T +# path::Vector{Symbol} +# end +# parent(o::Undefined) = getfield(o, :parent) +# path(o::Undefined) = getfield(o, :path) +# Base.show(io::IO, o::Undefined) = print(io, styled"{bright_yellow:Undefined($(typeof(parent(o)))): $(join(path(o), '.'))}") + +# Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [path(o)..., k]) + +# Base.propertynames(o::Undefined) = +# Base.getproperty(o::Undefined, k::Symbol) = getindex(o, k) +# Base.setproperty!(o::Undefined, k::Symbol, v) = setindex!(o, v, k) + + +#-----------------------------------------------------------------------------# Reference interface +# ref_keys(ref) = keys(ref) +# ref_value(ref, k) = ref[k] + +# iskey(ref, k) = k in ref_keys(ref) +# isval(ref, k, v) = ref_ + +# iskey(::Nothing, k) = true +# isval(::Nothing, k, v) = true + +# key_error(ref, k) = error("$ref does not have key $k.") +# val_error(ref, k, v) = error("$ref does not allow value $v for key $k.") + +# check(o, k) = iskey(ref(o), k) || key_error(ref(o), k) +# check(o, k, v) = check(o, k) && isval(ref(o), k, v) || val_error(ref(o), k, v) + +#-----------------------------------------------------------------------------# prune! +prune!(o::AbstractDict) = prune!(isempty, o) + +function prune!(f, o::AbstractDict) + for (k, v) in o + if v isa AbstractDict + f(v) ? delete!(o, k) : prune!(f, v) end - else - ex = Expr(:tuple, exprs...) - end - ex.head == :tuple || error("@config input must be a tuple") - all(x -> x.head == :(=), ex.args) || error("Unexpected syntax in @config") - x = gensym() - out = Expr(:block, :(local $x = Config())) - for val in ex.args - lhs, rhs = val.args - push!(out.args, :($(_prepend(x, lhs)) = $rhs)) end - push!(out.args, x) - esc(out) + return o end -_prepend(val, e::Symbol) = :($val.$e) -_prepend(val, e) = e.head == :. && (e.args[1] = _prepend(val, e.args[1]); e) +# #-----------------------------------------------------------------------------# Config +# struct Config <: AbstractDict{Symbol, Any} +# dict::OrderedDict{Symbol, Any} +# end +# function Config(x::Pair...; kw...) +# Config(OrderedDict{Symbol,Any}((Symbol(x[1]) => _config(x[2]) for x in x)..., (k => _config(v) for (k,v) in kw)...)) +# end + +# Config(dict::Union{NamedTuple, AbstractDict}) = _config(dict) +# _config(x) = x +# _config(x::AbstractArray) = _config.(x) +# _config(x::AbstractDict) = Config(OrderedDict{Symbol, Any}((Symbol(k) => _config(v) for (k, v) in x)...)) +# _config(kw::NamedTuple) = Config(; kw...) +# _config(x::Pair{<:Union{AbstractString, Symbol}, T}) where {T} = Config(Symbol(x[1]) => _config(x[2])) + +# dict(o) = getfield(o, :dict) + +# for f in (:length, :keys, :values, :iterate, :setindex!, :get, :get!) +# @eval Base.$f(o::Config, x...) = $f(dict(o), x...) +# end +# Base.getindex(o::Config, k::Symbol) = get!(dict(o), k, Config()) +# for f in (:empty!, :delete!) +# @eval Base.$f(o::Config, x...) = ($f(dict(o), x...); o) +# end +# Base.merge!(a::Config, b::Config) = (merge!(dict(a), dict(b)); a) +# Base.merge(a::Config, b::Config) = merge!(copy(a), b) +# Base.delete!(o::Config, k::AbstractString) = delete!(o, Symbol(k)) +# Base.copy(o::Config) = Config(copy(dict(o))) +# Base.isempty(o::Config) = isempty(prune!(o)) + +# Base.getindex(o::Config, k::AbstractString) = getindex(o, Symbol(k)) +# Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) +# Base.get(o::Config, k::AbstractString, default) = get(o, Symbol(k), default) +# Base.get!(o::Config, k::AbstractString, default) = get!(o, Symbol(k), default) + +# Base.propertynames(o::Config) = Symbol.(keys(o)) +# Base.getproperty(o::Config, k::Symbol) = getindex(o, k) +# Base.getproperty(o::Config, k::AbstractString) = getindex(o, k) +# Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) +# Base.setproperty!(o::Config, k::AbstractString, v) = setindex!(o, v, k) + + +#-----------------------------------------------------------------------------# + + +# #-----------------------------------------------------------------------------# Key +# struct Key +# parts::Vector{Symbol} +# end +# parts(o::Key) = getfield(o, :parts) + +# Key(x::Symbol...) = Key(Symbol[x...]) +# Key(x::AbstractString) = Key(Symbol.(split(x, '.'))) +# Key(x::Key) = x +# Base.string(o::Key) = join(parts(o), '.') + +# Base.:(==)(a::Key, b::Key) = parts(a) == parts(b) +# Base.hash(o::Key, h::UInt) = hash(parts(o), h) + +# const KeyType = Union{Symbol, AbstractString, Key} + +# Base.:*(o::Key, k::KeyType) = Key([parts(o)..., parts(Key(k))...]) + +# Base.show(io::IO, o::Key) = print(io, styled"{bright_cyan:$(repr(string(o)))}") +# Base.getindex(o::Key, i::Integer) = parts(o)[i] +# Base.length(o::Key) = length(parts(o)) +# Base.iterate(o::Key, i::Int=1) = i > length(o) ? nothing : (o[i], i + 1) + +# Base.getproperty(o::Key, x::Symbol) = o * x + +# #-----------------------------------------------------------------------------# Undefined +# struct Undefined{T} +# parent::T +# path::Key +# end +# Base.show(io::IO, o::Undefined) = print(io, styled"{bright_red:Undefined($(string(o.path)))}") +# Base.getindex(o::Undefined, k::KeyType) = o.parent[k] + +# #-----------------------------------------------------------------------------# Config +# struct Config{R} <: AbstractDict{Symbol, Any} +# dict::OrderedDict{Key, Any} +# prefix::Key +# ref::R +# end + +# dict(o) = getfield(o, :dict) +# prefix(o) = getfield(o, :prefix) +# ref(o) = getfield(o, :ref) +# isdef(o) = getfield(o, :isdef) + +# function dict_subset(o::Config) +# filter(dict(o)) do (k, v) +# all(a == b for (a,b) in zip(k, prefix(o))) +# end +# end +# Base.iterate(o::Config, i=1) = iterate(dict_subset(o), i) +# Base.length(o::Config) = length(dict_subset(o)) +# Base.keys(o::Config) = keys(dict_subset(o)) +# Base.values(o::Config) = values(dict_subset(o)) + +# function Base.summary(io::IO, o::Config) +# isdef(o) ? +# print(io, styled"{bright_green:Config($(prefix(o)))} with $(length(o)) entries") : +# print(io, styled"{bright_yellow:Undefined Config($(prefix(o)))}") +# end + +# function Config(prefix = Key(), ref = nothing; kw...) +# out = Config(OrderedDict{Key, Any}(), prefix, ref) +# for (k, v) in kw +# out[k] = v +# end +# return out +# end + +# function Base.setindex!(o::Config, v, k::Key) +# iskey(ref(o), k) || return key_error(ref(o), k) +# isval(ref(o), k, v) || return val_error(ref(o), k, v) +# dict(o)[k] = v +# end +# Base.setindex!(o::Config, v, k::KeyType) = setindex!(o, v, prefix(o) * Key(k)) + + +# function Base.getindex(o::Config, k::Key) +# iskey(ref(o), k) || return key_error(ref(o), k) +# return get(dict(o), k, Undefined()) +# end +# Base.getindex(o::Config, k::KeyType) = getindex(o, prefix(o) * Key(k)) + +# indexing by Key: Always absolute path +# Indexing by Symbol: Relative to prefix +# Indexing by String: + + +# is_prefix(prefix, o::Key) = is_prefix(Key(prefix), o) +# is_prefix(prefix::Key, o::Key) = prefix.sep == o.sep && prefix.parts == o.parts[1:length(prefix.parts)] + +#-----------------------------------------------------------------------------# getters +# for f in (:dict, :prefix, :ref, :isd) + +# dict(o) = getfield(o, :dict) +# prefix(o) = getfield(o, :prefix) +# ref(o) = getfield(o, :ref) +# isdef(o) = getfield(o, :isdef) + +# #-----------------------------------------------------------------------------# Config +# struct Config{R} <: AbstractDict{Symbol, Any} +# dict::OrderedDict{Key, Any} +# prefix::Key +# ref::R +# isdef::Bool +# end +# for f in fieldnames(Config) +# @eval $f(o::Config) = getfield(o, $(QuoteNode(f))) +# end + +# function Config(prefix = Key(), ref = nothing; kw...) +# out = Config(OrderedDict{Key, Any}(), prefix, ref, true) +# for (k, v) in kw +# out[Key(k)] = v +# end +# return out +# end +# function (o::Config{T})(x::Pair...; kw...) where {T} +# for (k, v) in Iterators.flatten((x, kw)) +# o[k] = v +# end +# return o +# end + + +# _undef(o::Config, k) = Config(dict(o), prefix(o) * k, ref(o), false) + +# function Base.keys(o::Config) +# dict_keys = collect(keys(dict(o))) +# pre = prefix(o) +# out = filter(x -> isprefix(pre), dict_keys) +# map(out) do path +# path.parts[length(pre) + 1:end] +# end +# end +# Base.iterate(o::Config, i = 1) = i > length(o) ? nothing : (keys(o)[i], i + 1) + +# # for f in (:keys, :values, :iterate, :length) +# # @eval Base.$f(o::Config, x...) = $f(dict(o), x...) +# # end +# Base.merge!(a::Config, b::Config) = (merge!(dict(a), dict(b)); a) +# for f in (:empty!, :delete!) +# @eval Base.$f(o::Config{T}, x...) where {T} = ($f(dict(o), x...); o) +# end + +# # function Base.keys(o::Config) +# # out = collect(keys(dict(o))) +# # paths = filter!(x -> is_prefix(prefix(o), x), out) +# # [first(x) for x] +# # # end +# # Base.length(o::Config) = length(keys(o)) +# # function Base.iterate(o::Config, i::Int=1) +# # i > length(o) && return nothing +# # key = keys(o)[i] +# # return (key => o[key], i + 1) +# # end + +# # function Base.keys(o::Config) +# # out = collect(keys(dict(o))) +# # isnothing(prefix(o)) ? out : map(x -> replace(x, string(prefix(o), sep(o)) => ""), out) +# # end + +# # function Base.getindex(o::Config, k::Key) +# # iskey(ref(o), k) ? get(dict(o), k, _undef(o, k)) : key_error(ref(o), k) +# # end +# # Base.getindex(o::Config, k::KeyType) = getindex(o, prefix(o) * k) + + +# function Base.setindex!(o::Config, v, path::Key) +# k = prefix(o) * path +# iskey(ref(o), k) || return key_error(ref(o), k) +# isval(ref(o), k, v) || return val_error(ref(o), k, v) +# dict(o)[k] = v +# end +# # Base.setindex!(o::Config, v, k::KeyType) = setindex!(o, v, prefix(o) * k) + +# # function Base.getindex(o::Config, _k::KeyType) +# # k = Key(o, _k) +# # iskey(ref(o), k) || key_error(ref(o), k) +# # kys = filter(x -> is_prefix(k, x), keys(o)) +# # length(kys) == 0 && return Undefined(o, k) +# # length(kys) == 1 && k == only(kys) && return dict(o)[k] +# # return Config(filter(kv -> startswith(kv[1], k), dict(o)), k, sep(o), ref(o)) +# # end +# # Base.haskey(o::Config, k::KeyType) = any(startswith(key(o, k), keys(o))) + + + + + + +# struct Config{T, D <: AbstractDict{Symbol, T}, S} <: AbstractDict{Symbol, Union{T,S}} +# dict::OrderedDict{Symbol, T} +# default::S +# end +# dict(o) = getfield(o, :dict) + +# # Config(x::Pair...; kw...) = Config(OrderedDict{Symbol,Any}((Symbol(x[1]) => x[2] for x in x)..., pairs(kw)...)) + +# # Functions that map to the wrapped dict +# for f in (:keys, :values, :iterate, :length, :setindex!) +# @eval Base.$f(o::Config, x...) = $f(dict(o), x...) +# end + +# # Functions that map to the wrapped dict but return the Config +# for f in (:empty!, :merge!, :delete!) +# @eval Base.$f(o::Config, x...) = ($f(dict(o), x...); o) +# end + +# Base.propertynames(o::Config) = keys(o) +# Base.getproperty(o::Config, k::Symbol) = o[k] +# Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) + +# OrderedCollections.freeze(o::Config) = Config(OrderedCollections.freeze(dict(o))) + +# _config_value(x) = x +# _config_value(x::AbstractDict) = Config(x) +# _config_value(x::Pair) = Config(Symbol(x[1]) => x[2]) +# _config_value(x::AbstractArray) = map(_config_value, x) + +# OrderedCollections.isordered(::Type{Config}) = true + +# delete_empty!(x) = x +# function delete_empty!(o::Config) +# for (k,v) in pairs(o) +# v isa Config && isempty(v) ? delete!(o, k) : delete_empty!(v) +# end +# o +# end + +# dict(o::Config) = getfield(o, :d) + +# StructTypes.StructType(::Type{Config}) = StructTypes.DictType() + +# Base.getproperty(o::Config, k::Symbol) = get!(o, k, Config()) +# Base.getproperty(o::Config, k) = getproperty(o, Symbol(k)) + +# Base.setproperty!(o::Config, k::Symbol, v) = dict(o)[Symbol(k)] = v +# Base.setproperty!(o::Config, k, v) = setproperty!(o, Symbol(k), v) + +# Base.propertynames(o::Config) = collect(keys(dict(o))) + +# Base.getindex(o::Config, k) = getproperty(o, Symbol(k)) +# Base.setindex!(o::Config, v, k) = setproperty!(o, Symbol(k), v) + +# Base.iterate(o::Config, args...) = iterate(dict(o), args...) +# Base.keys(o::Config) = keys(dict(o)) +# Base.values(o::Config) = values(dict(o)) +# Base.length(o::Config) = length(dict(o)) +# Base.isempty(o::Config) = isempty(dict(delete_empty!(o))) +# Base.pairs(o::Config) = pairs(dict(o)) +# Base.empty!(o::Config) = (empty!(dict(o)); o) +# Base.get(o::Config, k, default) = get(dict(o), Symbol(k), default) +# Base.get!(o::Config, k, default) = get!(dict(o), Symbol(k), default) +# Base.haskey(o::Config, k) = haskey(dict(o), Symbol(k)) + +# Base.delete!(o::Config, k) = (delete!(dict(o), Symbol(k)); o) + +# Base.isequal(a::Config, b::Config) = dict(a) == dict(b) +# Base.copy(o::Config) = Config(copy(dict(o))) + +# Base.merge(a::Config, b::Config) = Config(merge(dict(a), dict(b))) + +# Base.merge!(a::Config, b::Config) = Config(merge!(dict(a), dict(b))) + +# #-----------------------------------------------------------------------------# @config +# """ +# @config expr + +# Create a `Config` with a NamedTuple-like or block syntax. The following examples create equivalent `Config`s: + +# @config (x.one=1, x.two=2, z=3) + +# @config x.one=1 x.two=2 z=3 + +# @config begin +# x.one = 1 +# x.two = 2 +# z = 3 +# end + +# let +# c = Config() +# c.x.one = 1 +# c.x.two = 2 +# c.z = 3 +# end +# """ +# macro config(ex...) +# exprs = collect(ex) +# if length(exprs) == 1 +# ex = only(exprs) +# if ex.head == :block +# ex = Expr(:tuple, filter(x -> !(x isa LineNumberNode), ex.args)...) +# end +# else +# ex = Expr(:tuple, exprs...) +# end +# ex.head == :tuple || error("@config input must be a tuple") +# all(x -> x.head == :(=), ex.args) || error("Unexpected syntax in @config") +# x = gensym() +# out = Expr(:block, :(local $x = Config())) +# for val in ex.args +# lhs, rhs = val.args +# push!(out.args, :($(_prepend(x, lhs)) = $rhs)) +# end +# push!(out.args, x) +# esc(out) +# end + +# _prepend(val, e::Symbol) = :($val.$e) +# _prepend(val, e) = e.head == :. && (e.args[1] = _prepend(val, e.args[1]); e) end # module diff --git a/test/runtests.jl b/test/runtests.jl index 588fac2..129cd1f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,142 +1,160 @@ using EasyConfig -using StructTypes -using OrderedCollections: OrderedCollections, OrderedDict using Test -@testset "Constructors" begin - @test Config() == Config() - c = Config() - c.x = 1 - @test Config(x=1) == Config(:x => 1) == Config(Dict(:x => 1)) == Config((; x=1)) == c - - @testset "AbstractDict values" begin - d = Dict(:x => Dict(:y => 1)) - c = Config(d) - @test c.x isa Config - end - - @testset "Vector{<:AbstractDict} values" begin - d = Dict(:x => [Dict(:y => 1), Dict(:y=> 2)]) - c = Config(d) - @test c.x isa Vector{Config} - end - - @testset "Pair values" begin - data = [Dict("x"=>1), "y" => 2] - c = Config(; data) - @test c.data[1].x == 1 - @test c.data[2].y == 2 - end +@testset "Key" begin + @test isempty(Key()) + @test Key() == Key() + @test Key("x") == Key("x") + @test Key("a.b.c") == Key("a.b.c") + @test Key("a.b.c") != Key("a/b/c") + @test Key("a").b == Key("a.b") + @test Key("a") * Key("b") == Key("a.b") + @test EasyConfig.strip_prefix(Key("a.b.c"), Key("a.b")) == Key("c") + @test string(Key("a.b.c")) == "a.b.c" + @test collect(Key("a.b.c")) == collect("a.b.c") + @test Key("a").b.c.d.e == Key("a.b.c.d.e") + @test EasyConfig.parent(Key("a.b.c")) == Key("a.b") + @test Key("a.b.c") < Key("a.b.z") end -@testset "Set/get property/field" begin - c = Config() - c.one."two"["three"][:four] = 5 - @test c.one.two.three.four == 5 - c."test" == Config() - @test propertynames(c) == [:one, :test] +@testset "KV" begin + @test KV() == KV() + @test KV("x" => 1)["x"] == 1 + # @test KV("x" => 1) == KV(:x => 1) end -@testset "Other base methods" begin - c = Config() - - @test get(c, "test", 1) == 1 - @test c."test" == Config() - - @test get!(c, "test2", 1) == 1 - @test c."test2" == 1 - delete!(c, "test") - @test propertynames(c) == [:test2] - - @test keys(Config(x = 1)) == keys(OrderedDict(:x => 1)) - @test all(values(Config(x = 1)) .== values(OrderedDict(:x => 1))) - @test pairs(Config(x = 1)) == pairs(OrderedDict(:x => 1)) - @test empty!(Config(x=1)) == Config() - @test haskey(Config(x=1), "x") == haskey(Config(x=1), :x) == true - - @test OrderedCollections.isordered(Config) - - @test Config(x=1) == Config(:x => 1) == Config("x" => 1) - - a = Config(x=1) - b = copy(a) - @test a !== b - - c = merge(a, Config(x=2,y=3)) - @test c.x == 2 - @test c.y == 3 - - # https://github.com/JuliaComputing/EasyConfig.jl/issues/8 - a = Config(x=1) - b = Config(y=Config(z=2)) - c = merge(a, b) - @test a == Config(x=1) - - merge!(a, b) - @test a == Config(x=1, y=Config(z=2)) -end - -@testset "from NamedTuple" begin - nt = (x=1, y=(x=1, y=(x=1, y=(x=1,y=2)))) - c = Config(nt) - @test c.y.y.y.y == 2 -end - -@testset "from Dict" begin - d = Dict(:x => Dict(:x => Dict(:x => Dict(:x => 2)))) - c = Config(d) - @test c.x.x.x.x == 2 -end - -@testset "merge!" begin - a = Config(x = 1, y = Config(x = 1)) - b = Config(x = 5, y = Config(x=5, z="hi")) - c = merge(a, b) - merge!(a, b) - @test a == c - @test a.x == 5 - @test a.y.x == 5 - @test a.y.z == "hi" -end - -@testset "isempty/delete_empty!" begin - c = Config() - c.x.x.x - @test isempty(c) - c.x.x.x - EasyConfig.delete_empty!(c) - @test isempty(c) -end - -@testset "StructTypes" begin - @test isempty(StructTypes.keyvaluepairs(Config())) - c = Config(x=1,y=2) - @test StructTypes.keyvaluepairs(c)[:x] == 1 - @test StructTypes.keyvaluepairs(c)[:y] == 2 -end - -@testset "@config" begin - val = 5 - c = @config (x.a=1, x.b=2, x.c.d.e.f.g = 3, z = val) - @test c.x.a == 1 - @test c.x.b == 2 - @test c.x.c.d.e.f.g == 3 - @test c.z == val - - c2 = @config x.a=1 x.b=2 x.c.d.e.f.g=3 z=val - - c3 = @config begin - x.a = 1 - x.b = 2 - x.c.d.e.f.g = 3 - z = val - end - - c4 = Config() - c4.x.a = 1 - c4.x.b = 2 - c4.x.c.d.e.f.g = 3 - c4.z = val - - @test c == c2 == c3 -end +# @testset "Constructors" begin +# @test Config() == Config() +# c = Config() +# c.x = 1 +# @test Config(x=1) == Config(:x => 1) == Config(Dict(:x => 1)) == Config((; x=1)) == c + +# @testset "AbstractDict values" begin +# d = Dict(:x => Dict(:y => 1)) +# c = Config(d) +# @test c.x isa Config +# end + +# @testset "Vector{<:AbstractDict} values" begin +# d = Dict(:x => [Dict(:y => 1), Dict(:y=> 2)]) +# c = Config(d) +# @test c.x isa Vector{Config} +# end + +# @testset "Pair values" begin +# data = [Dict("x"=>1), "y" => 2] +# c = Config(; data) +# @test c.data[1].x == 1 +# @test c.data[2].y == 2 +# end +# end + +# @testset "Set/get property/field" begin +# c = Config() +# c.one."two"["three"][:four] = 5 +# @test c.one.two.three.four == 5 +# c."test" == Config() +# @test propertynames(c) == [:one, :test] +# end + +# @testset "Other base methods" begin +# c = Config() + +# @test get(c, "test", 1) == 1 +# @test c."test" == Config() + +# @test get!(c, "test2", 1) == 1 +# @test c."test2" == 1 +# delete!(c, "test") +# @test propertynames(c) == [:test2] + +# @test keys(Config(x = 1)) == keys(OrderedDict(:x => 1)) +# @test all(values(Config(x = 1)) .== values(OrderedDict(:x => 1))) +# @test pairs(Config(x = 1)) == pairs(OrderedDict(:x => 1)) +# @test empty!(Config(x=1)) == Config() +# @test haskey(Config(x=1), "x") == haskey(Config(x=1), :x) == true + +# @test Config(x=1) == Config(:x => 1) == Config("x" => 1) + +# a = Config(x=1) +# b = copy(a) +# @test a !== b + +# c = merge(a, Config(x=2,y=3)) +# @test c.x == 2 +# @test c.y == 3 + +# # https://github.com/JuliaComputing/EasyConfig.jl/issues/8 +# a = Config(x=1) +# b = Config(y=Config(z=2)) +# c = merge(a, b) +# @test a == Config(x=1) + +# merge!(a, b) +# @test a == Config(x=1, y=Config(z=2)) +# end + +# @testset "from NamedTuple" begin +# nt = (x=1, y=(x=1, y=(x=1, y=(x=1,y=2)))) +# c = Config(nt) +# @test c.y.y.y.y == 2 +# end + +# @testset "from Dict" begin +# d = Dict(:x => Dict(:x => Dict(:x => Dict(:x => 2)))) +# c = Config(d) +# @test c.x.x.x.x == 2 +# end + +# @testset "merge!" begin +# a = Config(x = 1, y = Config(x = 1)) +# b = Config(x = 5, y = Config(x=5, z="hi")) +# c = merge(a, b) +# merge!(a, b) +# @test a == c +# @test a.x == 5 +# @test a.y.x == 5 +# @test a.y.z == "hi" +# end + +# @testset "isempty/delete_empty!" begin +# c = Config() +# c.x.x.x +# @test isempty(c) +# c.x.x.x +# EasyConfig.delete_empty!(c) +# @test isempty(c) +# end + +# @testset "StructTypes" begin +# @test isempty(StructTypes.keyvaluepairs(Config())) +# c = Config(x=1,y=2) +# @test StructTypes.keyvaluepairs(c)[:x] == 1 +# @test StructTypes.keyvaluepairs(c)[:y] == 2 +# end + +# @testset "@config" begin +# val = 5 +# c = @config (x.a=1, x.b=2, x.c.d.e.f.g = 3, z = val) +# @test c.x.a == 1 +# @test c.x.b == 2 +# @test c.x.c.d.e.f.g == 3 +# @test c.z == val + +# c2 = @config x.a=1 x.b=2 x.c.d.e.f.g=3 z=val + +# c3 = @config begin +# x.a = 1 +# x.b = 2 +# x.c.d.e.f.g = 3 +# z = val +# end + +# c4 = Config() +# c4.x.a = 1 +# c4.x.b = 2 +# c4.x.c.d.e.f.g = 3 +# c4.z = val + +# @test c == c2 == c3 +# end From 0fdfa6a3b847c4546b50b5551da912a7edb82ba0 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 10:35:06 -0400 Subject: [PATCH 2/7] finishing methods, cleaning up commented out code --- src/EasyConfig.jl | 560 ++-------------------------------------------- 1 file changed, 15 insertions(+), 545 deletions(-) diff --git a/src/EasyConfig.jl b/src/EasyConfig.jl index 5d6e8ec..16a8b7f 100644 --- a/src/EasyConfig.jl +++ b/src/EasyConfig.jl @@ -6,15 +6,15 @@ export Config # Lazy path of nested dicts struct Undefined{T <: AbstractDict{Symbol, Any}} parent::T - keys::Vector{Symbol} + path::Vector{Symbol} end parent(o::Undefined) = getfield(o, :parent) -keys(o::Undefined) = getfield(o, :keys) +path(o::Undefined) = getfield(o, :keys) -Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [keys(o)..., k]) +Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [path(o)..., k]) function Base.setindex!(o::Undefined{T}, v, key::Symbol) where {T} c = parent(o) - for k in keys(o) + for k in path(o) c[k] = T() c = c[k] end @@ -37,6 +37,7 @@ struct Config{D <: AbstractDict{Symbol, Any}} <: AbstractDict{Symbol, Any} end Config() = Config(Dict{Symbol,Any}()) Config{D}() where {D} = Config(D()) +Config{D}(x::AbstractDict{Symbol}) where {D} = Config(D((k => string(v) for (k,v) in x)...)) function Config(x::Pair...; kw...) out = Config() @@ -51,566 +52,35 @@ dict(o::Config) = getfield(o, :dict) ## Getting and Setting ## Base.getindex(o::Config, k::Symbol) = get(o, k, Undefined(o, [k])) Base.setindex!(o::Config, v, k::Symbol) = setindex!(dict(o), v, k) +Base.setindex!(o::T, v::AbstractDict{Symbol}, k::Symbol) where {T <: Config} = setindex!(dict(o), T(v), k) Base.getindex(o::Config, k::AbstractString) = getindex(o, Symbol(k)) Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) +Base.propertynames(o::Config) = keys(o) Base.getproperty(o::Config, k::Symbol) = getindex(o, k) Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) ## AbstractDict interface ## Base.get(o::Config, k::Symbol, default) = get(dict(o), k, default) +Base.get(o::Config, k::AbstractString, default) = get(o, Symbol(k), default) Base.iterate(o::Config, s...) = iterate(dict(o), s...) Base.length(o::Config) = length(dict(o)) - - +Base.empty!(o::Config) = (empty!(dict(o)); o) +Base.delete!(o::Config, k::Symbol) = (delete!(dict(o), k); o) +Base.delete!(o::Config, k::AbstractString) = delete!(o, Symbol(k)) +Base.merge(a::Config, b::Config) = Config(merge(dict(a), dict(b))) +Base.merge!(a::Config, b::Config) = Config(merge!(dict(a), dict(b))) +Base.copy(o::Config) = Config(copy(dict(o))) #------------------------------------------------------------------------------# show # What gets used to show nested Configs by the AbstractDict show method: function Base.show(io::IO, o::Config) - print(io, "(") + print(io, "Config(") join(io, map(((k, v),) -> "$(repr(k)) => $(repr(v; context=io))", collect(o)), ", ") print(io, ')') end -# #-----------------------------------------------------------------------------# utils -# # Turn struct into NamedTuple -# fields(x::T) where {T} = NamedTuple{fieldnames(T)}(getfield.(Ref(x), fieldnames(T))) - - -# #-----------------------------------------------------------------------------# Key -# """ -# Key(x::AbstractString; sep = :auto) -# Key(parts::Symbol...; sep::Char) - -# An AbstractString represented internally as a `Vector{Symbol}` and a separator (`Char`). -# """ -# struct Key <: AbstractString -# parts::Vector{Symbol} -# sep::Char -# end -# Key(x::Symbol...; sep::Char='.') = Key(collect(x), sep) -# function Key(x::AbstractString; sep = :auto) -# if sep === :auto -# nonletters = filter(!isletter, x) -# countmap = Dict(x => count(==(x), nonletters) for x in unique(nonletters)) -# isempty(countmap) && return Key([Symbol(x)], '.') -# _, char = findmax(countmap) -# Key(Symbol.(split(x, char; keepempty=false)), char) -# else -# Key(Symbol.(split(x, sep; keepempty=false)), sep) -# end -# end - -# parts(o::Key) = getfield(o, :parts) -# sep(o::Key) = getfield(o, :sep) - -# Base.getproperty(o::Key, x::Symbol) = Key([parts(o)..., x], sep(o)) - -# _string(o::Key) = join(parts(o), sep(o)) -# Base.show(io::IO, o::Key) = print(io, styled"{bright_cyan:$(repr(_string(o)))}") -# Base.ncodeunits(o::Key) = ncodeunits(_string(o)) -# Base.iterate(o::Key, i::Integer...) = iterate(_string(o), i...) -# Base.length(o::Key) = length(_string(o)) - -# function Base.:*(a::Key, b::Key) -# sep(a) == sep(b) || error("Cannot join `Key`s with different separators. Found $(sep(a)) and $(sep(b)).") -# Key(vcat(parts(a), parts(b)), sep(a)) -# end - -# function strip_prefix(o::Key, prefix::Key) -# sep(o) == sep(prefix) || error("Cannot join `Key`s with different separators. Found $(sep(o)) and $(sep(prefix)).") -# all(x==y for (x, y) in zip(parts(o), parts(prefix))) || error("$prefix is not a prefix of $o") -# Key(parts(o)[length(parts(prefix)) + 1:end], sep(o)) -# end - -# parent(o::Key) = Key(parts(o)[1:end-1], sep(o)) - - -# #-----------------------------------------------------------------------------# ref interface -# is_key(o::Ref, key::Key) = true -# is_val(o::Ref, key::Key, val) = true -# about(ref, key::Key) = "No information provided for $key of $ref." - - - -# # about(o::KV, key::Key) = about(ref(o), key) - -# function check(ref::T, key::Key) where {T} -# idx = findall(startswith(key), ref_keys(ref)) -# length(idx) > 0 || error("Key $key not found in reference $ref.") -# end - -# # function check(ref::T, key::Key, val) where {T} -# # check(ref, key) -# # if !is_valid(ref, key, val) -# # error("Reference::$T invalid value for key $key: $val. See `about(ref, key)`.") -# # end -# # end - -# # # # ref methods that use interface -# # # is_full_path(ref, o::Key) = o in ref_keys(ref) -# # # is_partial_path(ref, o::Key) = any(startswith(o), ref_keys(ref)) - - -# #-----------------------------------------------------------------------------# Undefined -# struct Undefined -# about -# end -# Base.show(io::IO, o::Undefined) = print(io, "Undefined: ", o.about) - -# #-----------------------------------------------------------------------------# KV -# struct KV{R} <: AbstractDict{Key, Any} -# keys::Vector{Key} -# values::Vector -# prefix::Key -# ref::R -# function KV(pairs::Pair...; ref::R = nothing, path=Key()) where {R} -# keys = collect(Key.(first.(pairs))) -# vals = collect(Any, last.(pairs)) -# new{R}(keys, vals, path, ref) -# end -# end - -# prefix(o::KV) = getfield(o, :prefix) -# ref(o) = getfield(o, :ref) -# root(o::KV) = KV(getfield(o, :keys), getfield(o, :values), Key(), ref(o)) -# parent(o::KV) = KV(getfield(o, :keys), getfield(o, :values), parent(prefix(o)), ref(o)) - -# key_idxs(o::KV, prefix::Key) = findall(startswith(prefix), getfield(o, :keys)) -# key_idxs(o::KV) = key_idxs(o, prefix(o)) - -# ref_keys(o::KV) = ref_keys(ref(o)) -# about(o::KV, k::Key) = about(ref(o), k) - -# function Base.keys(o::KV) -# full_keys = getfield(o, :keys)[key_idxs(o)] -# strip_prefix.(full_keys, prefix(o)) -# end -# Base.values(o::KV) = getfield(o, :values)[key_idxs(o)] - -# Base.iterate(o::KV, x...) = iterate(keys(o) .=> values(o), x...) -# Base.length(o::KV) = length(keys(o)) - - - -# function Base.getindex(o::KV{Nothing}, _k::AbstractString) -# k = prefix(o) * Key(_k) -# kys = getfield(o, :keys) -# # Case 1) Key already exists -# i = findfirst(==(k), kys) -# !isnothing(i) && return values(o)[i] - -# # Case 2) Full key exists in ref -# k in ref_keys(o) && return Undefined(about(o, k)) - -# # Case 3) k is a prefix of another key in ref -# any(startswith(k), kys) && return KV(kys, getfield(o, :values), k, ref(o)) - -# error("Key :$k not found in reference $(ref(o)).") -# end - -# function Base.setindex!(o::KV, v, _k::AbstractString) -# k = Key(_k) -# is_valid(ref(o), k, v) || error("Reference::$ref invalid value for key $k: $v. See `about(ref, $(repr(k)))`.") -# i = findfirst(==(k), keys(o)) -# if isnothing(i) -# push!(keys(o), k) -# push!(values(o), v) -# else -# keys(o)[i] = k -# values(o)[i] = v -# end -# end - -# function Base.propertynames(o::KV) -# map(keys(o)) do k -# parts(k)[1] -# end -# end -# Base.getproperty(o::KV, k::Symbol) = getindex(o, string(k)) - -# #-----------------------------------------------------------------------------# ref interface -# ref_keys(::Nothing) = Key[] -# is_valid(::Nothing, key::Key, val) = true -# about(ref, key::Key) = "No information provided for $key of $ref." - - - -# ref_keys(o::KV) = ref_keys(ref(o)) -# about(o::KV, key::Key) = about(ref(o), key) - -# function check(ref::T, key::Key) where {T} -# idx = findall(startswith(key), ref_keys(ref)) -# length(idx) > 0 || error("Key $key not found in reference $ref.") -# end - -# function check(ref::T, key::Key, val) where {T} -# check(ref, key) -# if !is_valid(ref, key, val) -# error("Reference::$T invalid value for key $key: $val. See `about(ref, key)`.") -# end -# end - - - - - - -#-----------------------------------------------------------------------------# Undefined -# struct Undefined{T} -# parent::T -# path::Vector{Symbol} -# end -# parent(o::Undefined) = getfield(o, :parent) -# path(o::Undefined) = getfield(o, :path) -# Base.show(io::IO, o::Undefined) = print(io, styled"{bright_yellow:Undefined($(typeof(parent(o)))): $(join(path(o), '.'))}") - -# Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [path(o)..., k]) - -# Base.propertynames(o::Undefined) = -# Base.getproperty(o::Undefined, k::Symbol) = getindex(o, k) -# Base.setproperty!(o::Undefined, k::Symbol, v) = setindex!(o, v, k) - - -#-----------------------------------------------------------------------------# Reference interface -# ref_keys(ref) = keys(ref) -# ref_value(ref, k) = ref[k] - -# iskey(ref, k) = k in ref_keys(ref) -# isval(ref, k, v) = ref_ - -# iskey(::Nothing, k) = true -# isval(::Nothing, k, v) = true - -# key_error(ref, k) = error("$ref does not have key $k.") -# val_error(ref, k, v) = error("$ref does not allow value $v for key $k.") - -# check(o, k) = iskey(ref(o), k) || key_error(ref(o), k) -# check(o, k, v) = check(o, k) && isval(ref(o), k, v) || val_error(ref(o), k, v) - -#-----------------------------------------------------------------------------# prune! -prune!(o::AbstractDict) = prune!(isempty, o) - -function prune!(f, o::AbstractDict) - for (k, v) in o - if v isa AbstractDict - f(v) ? delete!(o, k) : prune!(f, v) - end - end - return o -end - -# #-----------------------------------------------------------------------------# Config -# struct Config <: AbstractDict{Symbol, Any} -# dict::OrderedDict{Symbol, Any} -# end -# function Config(x::Pair...; kw...) -# Config(OrderedDict{Symbol,Any}((Symbol(x[1]) => _config(x[2]) for x in x)..., (k => _config(v) for (k,v) in kw)...)) -# end - -# Config(dict::Union{NamedTuple, AbstractDict}) = _config(dict) -# _config(x) = x -# _config(x::AbstractArray) = _config.(x) -# _config(x::AbstractDict) = Config(OrderedDict{Symbol, Any}((Symbol(k) => _config(v) for (k, v) in x)...)) -# _config(kw::NamedTuple) = Config(; kw...) -# _config(x::Pair{<:Union{AbstractString, Symbol}, T}) where {T} = Config(Symbol(x[1]) => _config(x[2])) - -# dict(o) = getfield(o, :dict) - -# for f in (:length, :keys, :values, :iterate, :setindex!, :get, :get!) -# @eval Base.$f(o::Config, x...) = $f(dict(o), x...) -# end -# Base.getindex(o::Config, k::Symbol) = get!(dict(o), k, Config()) -# for f in (:empty!, :delete!) -# @eval Base.$f(o::Config, x...) = ($f(dict(o), x...); o) -# end -# Base.merge!(a::Config, b::Config) = (merge!(dict(a), dict(b)); a) -# Base.merge(a::Config, b::Config) = merge!(copy(a), b) -# Base.delete!(o::Config, k::AbstractString) = delete!(o, Symbol(k)) -# Base.copy(o::Config) = Config(copy(dict(o))) -# Base.isempty(o::Config) = isempty(prune!(o)) - -# Base.getindex(o::Config, k::AbstractString) = getindex(o, Symbol(k)) -# Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) -# Base.get(o::Config, k::AbstractString, default) = get(o, Symbol(k), default) -# Base.get!(o::Config, k::AbstractString, default) = get!(o, Symbol(k), default) - -# Base.propertynames(o::Config) = Symbol.(keys(o)) -# Base.getproperty(o::Config, k::Symbol) = getindex(o, k) -# Base.getproperty(o::Config, k::AbstractString) = getindex(o, k) -# Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) -# Base.setproperty!(o::Config, k::AbstractString, v) = setindex!(o, v, k) - - -#-----------------------------------------------------------------------------# - - -# #-----------------------------------------------------------------------------# Key -# struct Key -# parts::Vector{Symbol} -# end -# parts(o::Key) = getfield(o, :parts) - -# Key(x::Symbol...) = Key(Symbol[x...]) -# Key(x::AbstractString) = Key(Symbol.(split(x, '.'))) -# Key(x::Key) = x -# Base.string(o::Key) = join(parts(o), '.') - -# Base.:(==)(a::Key, b::Key) = parts(a) == parts(b) -# Base.hash(o::Key, h::UInt) = hash(parts(o), h) - -# const KeyType = Union{Symbol, AbstractString, Key} - -# Base.:*(o::Key, k::KeyType) = Key([parts(o)..., parts(Key(k))...]) - -# Base.show(io::IO, o::Key) = print(io, styled"{bright_cyan:$(repr(string(o)))}") -# Base.getindex(o::Key, i::Integer) = parts(o)[i] -# Base.length(o::Key) = length(parts(o)) -# Base.iterate(o::Key, i::Int=1) = i > length(o) ? nothing : (o[i], i + 1) - -# Base.getproperty(o::Key, x::Symbol) = o * x - -# #-----------------------------------------------------------------------------# Undefined -# struct Undefined{T} -# parent::T -# path::Key -# end -# Base.show(io::IO, o::Undefined) = print(io, styled"{bright_red:Undefined($(string(o.path)))}") -# Base.getindex(o::Undefined, k::KeyType) = o.parent[k] - -# #-----------------------------------------------------------------------------# Config -# struct Config{R} <: AbstractDict{Symbol, Any} -# dict::OrderedDict{Key, Any} -# prefix::Key -# ref::R -# end - -# dict(o) = getfield(o, :dict) -# prefix(o) = getfield(o, :prefix) -# ref(o) = getfield(o, :ref) -# isdef(o) = getfield(o, :isdef) - -# function dict_subset(o::Config) -# filter(dict(o)) do (k, v) -# all(a == b for (a,b) in zip(k, prefix(o))) -# end -# end -# Base.iterate(o::Config, i=1) = iterate(dict_subset(o), i) -# Base.length(o::Config) = length(dict_subset(o)) -# Base.keys(o::Config) = keys(dict_subset(o)) -# Base.values(o::Config) = values(dict_subset(o)) - -# function Base.summary(io::IO, o::Config) -# isdef(o) ? -# print(io, styled"{bright_green:Config($(prefix(o)))} with $(length(o)) entries") : -# print(io, styled"{bright_yellow:Undefined Config($(prefix(o)))}") -# end - -# function Config(prefix = Key(), ref = nothing; kw...) -# out = Config(OrderedDict{Key, Any}(), prefix, ref) -# for (k, v) in kw -# out[k] = v -# end -# return out -# end - -# function Base.setindex!(o::Config, v, k::Key) -# iskey(ref(o), k) || return key_error(ref(o), k) -# isval(ref(o), k, v) || return val_error(ref(o), k, v) -# dict(o)[k] = v -# end -# Base.setindex!(o::Config, v, k::KeyType) = setindex!(o, v, prefix(o) * Key(k)) - - -# function Base.getindex(o::Config, k::Key) -# iskey(ref(o), k) || return key_error(ref(o), k) -# return get(dict(o), k, Undefined()) -# end -# Base.getindex(o::Config, k::KeyType) = getindex(o, prefix(o) * Key(k)) - -# indexing by Key: Always absolute path -# Indexing by Symbol: Relative to prefix -# Indexing by String: - - -# is_prefix(prefix, o::Key) = is_prefix(Key(prefix), o) -# is_prefix(prefix::Key, o::Key) = prefix.sep == o.sep && prefix.parts == o.parts[1:length(prefix.parts)] - -#-----------------------------------------------------------------------------# getters -# for f in (:dict, :prefix, :ref, :isd) - -# dict(o) = getfield(o, :dict) -# prefix(o) = getfield(o, :prefix) -# ref(o) = getfield(o, :ref) -# isdef(o) = getfield(o, :isdef) - -# #-----------------------------------------------------------------------------# Config -# struct Config{R} <: AbstractDict{Symbol, Any} -# dict::OrderedDict{Key, Any} -# prefix::Key -# ref::R -# isdef::Bool -# end -# for f in fieldnames(Config) -# @eval $f(o::Config) = getfield(o, $(QuoteNode(f))) -# end - -# function Config(prefix = Key(), ref = nothing; kw...) -# out = Config(OrderedDict{Key, Any}(), prefix, ref, true) -# for (k, v) in kw -# out[Key(k)] = v -# end -# return out -# end -# function (o::Config{T})(x::Pair...; kw...) where {T} -# for (k, v) in Iterators.flatten((x, kw)) -# o[k] = v -# end -# return o -# end - - -# _undef(o::Config, k) = Config(dict(o), prefix(o) * k, ref(o), false) - -# function Base.keys(o::Config) -# dict_keys = collect(keys(dict(o))) -# pre = prefix(o) -# out = filter(x -> isprefix(pre), dict_keys) -# map(out) do path -# path.parts[length(pre) + 1:end] -# end -# end -# Base.iterate(o::Config, i = 1) = i > length(o) ? nothing : (keys(o)[i], i + 1) - -# # for f in (:keys, :values, :iterate, :length) -# # @eval Base.$f(o::Config, x...) = $f(dict(o), x...) -# # end -# Base.merge!(a::Config, b::Config) = (merge!(dict(a), dict(b)); a) -# for f in (:empty!, :delete!) -# @eval Base.$f(o::Config{T}, x...) where {T} = ($f(dict(o), x...); o) -# end - -# # function Base.keys(o::Config) -# # out = collect(keys(dict(o))) -# # paths = filter!(x -> is_prefix(prefix(o), x), out) -# # [first(x) for x] -# # # end -# # Base.length(o::Config) = length(keys(o)) -# # function Base.iterate(o::Config, i::Int=1) -# # i > length(o) && return nothing -# # key = keys(o)[i] -# # return (key => o[key], i + 1) -# # end - -# # function Base.keys(o::Config) -# # out = collect(keys(dict(o))) -# # isnothing(prefix(o)) ? out : map(x -> replace(x, string(prefix(o), sep(o)) => ""), out) -# # end - -# # function Base.getindex(o::Config, k::Key) -# # iskey(ref(o), k) ? get(dict(o), k, _undef(o, k)) : key_error(ref(o), k) -# # end -# # Base.getindex(o::Config, k::KeyType) = getindex(o, prefix(o) * k) - - -# function Base.setindex!(o::Config, v, path::Key) -# k = prefix(o) * path -# iskey(ref(o), k) || return key_error(ref(o), k) -# isval(ref(o), k, v) || return val_error(ref(o), k, v) -# dict(o)[k] = v -# end -# # Base.setindex!(o::Config, v, k::KeyType) = setindex!(o, v, prefix(o) * k) - -# # function Base.getindex(o::Config, _k::KeyType) -# # k = Key(o, _k) -# # iskey(ref(o), k) || key_error(ref(o), k) -# # kys = filter(x -> is_prefix(k, x), keys(o)) -# # length(kys) == 0 && return Undefined(o, k) -# # length(kys) == 1 && k == only(kys) && return dict(o)[k] -# # return Config(filter(kv -> startswith(kv[1], k), dict(o)), k, sep(o), ref(o)) -# # end -# # Base.haskey(o::Config, k::KeyType) = any(startswith(key(o, k), keys(o))) - - - - - - -# struct Config{T, D <: AbstractDict{Symbol, T}, S} <: AbstractDict{Symbol, Union{T,S}} -# dict::OrderedDict{Symbol, T} -# default::S -# end -# dict(o) = getfield(o, :dict) - -# # Config(x::Pair...; kw...) = Config(OrderedDict{Symbol,Any}((Symbol(x[1]) => x[2] for x in x)..., pairs(kw)...)) - -# # Functions that map to the wrapped dict -# for f in (:keys, :values, :iterate, :length, :setindex!) -# @eval Base.$f(o::Config, x...) = $f(dict(o), x...) -# end - -# # Functions that map to the wrapped dict but return the Config -# for f in (:empty!, :merge!, :delete!) -# @eval Base.$f(o::Config, x...) = ($f(dict(o), x...); o) -# end - -# Base.propertynames(o::Config) = keys(o) -# Base.getproperty(o::Config, k::Symbol) = o[k] -# Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) - -# OrderedCollections.freeze(o::Config) = Config(OrderedCollections.freeze(dict(o))) - -# _config_value(x) = x -# _config_value(x::AbstractDict) = Config(x) -# _config_value(x::Pair) = Config(Symbol(x[1]) => x[2]) -# _config_value(x::AbstractArray) = map(_config_value, x) - -# OrderedCollections.isordered(::Type{Config}) = true - -# delete_empty!(x) = x -# function delete_empty!(o::Config) -# for (k,v) in pairs(o) -# v isa Config && isempty(v) ? delete!(o, k) : delete_empty!(v) -# end -# o -# end - -# dict(o::Config) = getfield(o, :d) - -# StructTypes.StructType(::Type{Config}) = StructTypes.DictType() - -# Base.getproperty(o::Config, k::Symbol) = get!(o, k, Config()) -# Base.getproperty(o::Config, k) = getproperty(o, Symbol(k)) - -# Base.setproperty!(o::Config, k::Symbol, v) = dict(o)[Symbol(k)] = v -# Base.setproperty!(o::Config, k, v) = setproperty!(o, Symbol(k), v) - -# Base.propertynames(o::Config) = collect(keys(dict(o))) - -# Base.getindex(o::Config, k) = getproperty(o, Symbol(k)) -# Base.setindex!(o::Config, v, k) = setproperty!(o, Symbol(k), v) - -# Base.iterate(o::Config, args...) = iterate(dict(o), args...) -# Base.keys(o::Config) = keys(dict(o)) -# Base.values(o::Config) = values(dict(o)) -# Base.length(o::Config) = length(dict(o)) -# Base.isempty(o::Config) = isempty(dict(delete_empty!(o))) -# Base.pairs(o::Config) = pairs(dict(o)) -# Base.empty!(o::Config) = (empty!(dict(o)); o) -# Base.get(o::Config, k, default) = get(dict(o), Symbol(k), default) -# Base.get!(o::Config, k, default) = get!(dict(o), Symbol(k), default) -# Base.haskey(o::Config, k) = haskey(dict(o), Symbol(k)) - -# Base.delete!(o::Config, k) = (delete!(dict(o), Symbol(k)); o) - -# Base.isequal(a::Config, b::Config) = dict(a) == dict(b) -# Base.copy(o::Config) = Config(copy(dict(o))) - -# Base.merge(a::Config, b::Config) = Config(merge(dict(a), dict(b))) - -# Base.merge!(a::Config, b::Config) = Config(merge!(dict(a), dict(b))) - # #-----------------------------------------------------------------------------# @config # """ # @config expr From 424f3f5576fc1066555a92638ac29bbd8c06d333 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 10:46:16 -0400 Subject: [PATCH 3/7] wip --- Project.toml | 7 +- src/EasyConfig.jl | 119 ++++++++++-------- test/runtests.jl | 307 +++++++++++++++++++++++----------------------- 3 files changed, 223 insertions(+), 210 deletions(-) diff --git a/Project.toml b/Project.toml index a1ac81f..6419cda 100644 --- a/Project.toml +++ b/Project.toml @@ -1,14 +1,9 @@ name = "EasyConfig" uuid = "acab07b0-f158-46d4-8913-50acef6d41fe" -authors = ["joshday "] version = "0.1.16" - -[deps] -OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +authors = ["joshday "] [compat] -StyledStrings = "1.11.0" julia = "1" [extras] diff --git a/src/EasyConfig.jl b/src/EasyConfig.jl index 16a8b7f..46dad61 100644 --- a/src/EasyConfig.jl +++ b/src/EasyConfig.jl @@ -1,6 +1,6 @@ module EasyConfig -export Config +export Config, @config #------------------------------------------------------------------------------# Undefined # Lazy path of nested dicts @@ -9,7 +9,7 @@ struct Undefined{T <: AbstractDict{Symbol, Any}} path::Vector{Symbol} end parent(o::Undefined) = getfield(o, :parent) -path(o::Undefined) = getfield(o, :keys) +path(o::Undefined) = getfield(o, :path) Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [path(o)..., k]) function Base.setindex!(o::Undefined{T}, v, key::Symbol) where {T} @@ -34,11 +34,18 @@ settings::Settings = Settings() #------------------------------------------------------------------------------# Config struct Config{D <: AbstractDict{Symbol, Any}} <: AbstractDict{Symbol, Any} dict::D + Config(dict::D) where {D <: AbstractDict{Symbol, Any}} = new{D}(dict) end Config() = Config(Dict{Symbol,Any}()) +Config(x::AbstractDict{Symbol}) = Config{Dict{Symbol,Any}}(x) Config{D}() where {D} = Config(D()) -Config{D}(x::AbstractDict{Symbol}) where {D} = Config(D((k => string(v) for (k,v) in x)...)) - +function Config{D}(x::AbstractDict{Symbol}) where {D} + out = Config{D}() + for (k, v) in x + out[k] = v + end + out +end function Config(x::Pair...; kw...) out = Config() for (k, v) in Iterators.flatten((x, kw)) @@ -53,6 +60,7 @@ dict(o::Config) = getfield(o, :dict) Base.getindex(o::Config, k::Symbol) = get(o, k, Undefined(o, [k])) Base.setindex!(o::Config, v, k::Symbol) = setindex!(dict(o), v, k) Base.setindex!(o::T, v::AbstractDict{Symbol}, k::Symbol) where {T <: Config} = setindex!(dict(o), T(v), k) +Base.setindex!(o::T, v::T, k::Symbol) where {T <: Config} = setindex!(dict(o), v, k) Base.getindex(o::Config, k::AbstractString) = getindex(o, Symbol(k)) Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) @@ -81,51 +89,60 @@ function Base.show(io::IO, o::Config) print(io, ')') end -# #-----------------------------------------------------------------------------# @config -# """ -# @config expr - -# Create a `Config` with a NamedTuple-like or block syntax. The following examples create equivalent `Config`s: - -# @config (x.one=1, x.two=2, z=3) - -# @config x.one=1 x.two=2 z=3 - -# @config begin -# x.one = 1 -# x.two = 2 -# z = 3 -# end - -# let -# c = Config() -# c.x.one = 1 -# c.x.two = 2 -# c.z = 3 -# end -# """ -# macro config(ex...) -# exprs = collect(ex) -# if length(exprs) == 1 -# ex = only(exprs) -# if ex.head == :block -# ex = Expr(:tuple, filter(x -> !(x isa LineNumberNode), ex.args)...) -# end -# else -# ex = Expr(:tuple, exprs...) -# end -# ex.head == :tuple || error("@config input must be a tuple") -# all(x -> x.head == :(=), ex.args) || error("Unexpected syntax in @config") -# x = gensym() -# out = Expr(:block, :(local $x = Config())) -# for val in ex.args -# lhs, rhs = val.args -# push!(out.args, :($(_prepend(x, lhs)) = $rhs)) -# end -# push!(out.args, x) -# esc(out) -# end - -# _prepend(val, e::Symbol) = :($val.$e) -# _prepend(val, e) = e.head == :. && (e.args[1] = _prepend(val, e.args[1]); e) +#------------------------------------------------------------------------------# deepmerge +# `merge`, but will update the keys of a nested dict rather than replacing the dict entirely +function deepmerge!(a::T, b::AbstractDict) where {T <: AbstractDict} + foreach(pairs(b)) do (k, v) + old = get(a, k, nothing) + a[k] = v isa AbstractDict ? deepmerge!(old isa AbstractDict ? old : T(), v) : v + end + return a +end + +deepmerge(a, b) = deepmerge!(deepmerge!(Config(), a), b) + + +#-----------------------------------------------------------------------------# @config +""" + @config expr + +Create a `Config` with a NamedTuple-like or block syntax. The following examples create equivalent `Config`s: + + @config (x.one=1, x.two=2, z=3) + + @config x.one=1 x.two=2 z=3 + + @config begin + x.one = 1 + x.two = 2 + z = 3 + end + + let + c = Config() + c.x.one = 1 + c.x.two = 2 + c.z = 3 + end +""" +macro config(ex...) + args = length(ex) == 1 ? _config_args(only(ex)) : collect(ex) + all(x -> Meta.isexpr(x, :(=), 2), args) || error("@config expects `key = value` expressions") + c = gensym("config") + body = map(x -> :($(_prepend(c, x.args[1])) = $(x.args[2])), args) + esc(Expr(:block, :(local $c = $Config()), body..., c)) +end + +function _config_args(ex) + Meta.isexpr(ex, :block) && return filter(x -> !(x isa LineNumberNode), ex.args) + Meta.isexpr(ex, :tuple) && return ex.args + return [ex] +end + +_prepend(c, ex::Symbol) = :($c.$ex) +function _prepend(c, ex) + Meta.isexpr(ex, :., 2) || error("@config expects keys like `x` or `x.y.z`, got: $ex") + Expr(:., _prepend(c, ex.args[1]), ex.args[2]) +end + end # module diff --git a/test/runtests.jl b/test/runtests.jl index 129cd1f..8bf3f9e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,160 +1,161 @@ using EasyConfig +using OrderedCollections using Test -@testset "Key" begin - @test isempty(Key()) - @test Key() == Key() - @test Key("x") == Key("x") - @test Key("a.b.c") == Key("a.b.c") - @test Key("a.b.c") != Key("a/b/c") - @test Key("a").b == Key("a.b") - @test Key("a") * Key("b") == Key("a.b") - @test EasyConfig.strip_prefix(Key("a.b.c"), Key("a.b")) == Key("c") - @test string(Key("a.b.c")) == "a.b.c" - @test collect(Key("a.b.c")) == collect("a.b.c") - @test Key("a").b.c.d.e == Key("a.b.c.d.e") - @test EasyConfig.parent(Key("a.b.c")) == Key("a.b") - @test Key("a.b.c") < Key("a.b.z") +@testset "Constructors" begin + @test Config() == Config() + @test isempty(Config()) + c = Config() + c.x = 1 + @test Config(x=1) == Config(:x => 1) == Config("x" => 1) == Config(Dict(:x => 1)) == c + @test Config(:x => 1; y=2) == Config(x=1, y=2) + + @testset "Wraps AbstractDict{Symbol, Any} without copying" begin + d = Dict{Symbol, Any}(:x => 1) + c = Config(d) + c.y = 2 + @test d[:y] == 2 + end + + @testset "AbstractDict values become Configs" begin + c = Config(Dict(:x => Dict(:y => Dict(:z => 1)))) + @test c.x isa Config + @test c.x.y isa Config + @test c.x.y.z == 1 + c.a = Dict(:b => 2) + @test c.a isa Config + @test c.a.b == 2 + end + + @testset "Config{D}" begin + D = OrderedDict{Symbol, Any} + @test Config{D}() isa Config{D} + c = Config{D}(Dict(:x => 1)) + @test c isa Config{D} + @test c.x == 1 + end end -@testset "KV" begin - @test KV() == KV() - @test KV("x" => 1)["x"] == 1 - # @test KV("x" => 1) == KV(:x => 1) +@testset "Get/set" begin + c = Config() + c.x = 1 + c[:y] = 2 + c["z"] = 3 + @test c.x == c[:x] == c["x"] == 1 + @test c.y == 2 + @test c.z == 3 + @test sort(collect(propertynames(c))) == [:x, :y, :z] + + @testset "Missing keys are Undefined" begin + c = Config() + @test c.x isa EasyConfig.Undefined + @test c.x.y.z isa EasyConfig.Undefined + @test isempty(c) + end + + @testset "Nested assignment" begin + c = Config() + c.a.b.c.d = 1 + @test c.a.b.c.d == 1 + c.a.b.e = 2 + @test c.a.b.e == 2 + @test c.a.b.c.d == 1 + c[:f][:g] = 3 + @test c.f.g == 3 + end + + @testset "Nested assignment preserves dict type" begin + c = Config(OrderedDict{Symbol, Any}()) + c.b.c = 1 + c.a = 2 + @test c.b isa Config{OrderedDict{Symbol, Any}} + @test collect(keys(c)) == [:b, :a] + end end -# @testset "Constructors" begin -# @test Config() == Config() -# c = Config() -# c.x = 1 -# @test Config(x=1) == Config(:x => 1) == Config(Dict(:x => 1)) == Config((; x=1)) == c - -# @testset "AbstractDict values" begin -# d = Dict(:x => Dict(:y => 1)) -# c = Config(d) -# @test c.x isa Config -# end - -# @testset "Vector{<:AbstractDict} values" begin -# d = Dict(:x => [Dict(:y => 1), Dict(:y=> 2)]) -# c = Config(d) -# @test c.x isa Vector{Config} -# end - -# @testset "Pair values" begin -# data = [Dict("x"=>1), "y" => 2] -# c = Config(; data) -# @test c.data[1].x == 1 -# @test c.data[2].y == 2 -# end -# end - -# @testset "Set/get property/field" begin -# c = Config() -# c.one."two"["three"][:four] = 5 -# @test c.one.two.three.four == 5 -# c."test" == Config() -# @test propertynames(c) == [:one, :test] -# end - -# @testset "Other base methods" begin -# c = Config() - -# @test get(c, "test", 1) == 1 -# @test c."test" == Config() - -# @test get!(c, "test2", 1) == 1 -# @test c."test2" == 1 -# delete!(c, "test") -# @test propertynames(c) == [:test2] - -# @test keys(Config(x = 1)) == keys(OrderedDict(:x => 1)) -# @test all(values(Config(x = 1)) .== values(OrderedDict(:x => 1))) -# @test pairs(Config(x = 1)) == pairs(OrderedDict(:x => 1)) -# @test empty!(Config(x=1)) == Config() -# @test haskey(Config(x=1), "x") == haskey(Config(x=1), :x) == true - -# @test Config(x=1) == Config(:x => 1) == Config("x" => 1) - -# a = Config(x=1) -# b = copy(a) -# @test a !== b - -# c = merge(a, Config(x=2,y=3)) -# @test c.x == 2 -# @test c.y == 3 - -# # https://github.com/JuliaComputing/EasyConfig.jl/issues/8 -# a = Config(x=1) -# b = Config(y=Config(z=2)) -# c = merge(a, b) -# @test a == Config(x=1) - -# merge!(a, b) -# @test a == Config(x=1, y=Config(z=2)) -# end - -# @testset "from NamedTuple" begin -# nt = (x=1, y=(x=1, y=(x=1, y=(x=1,y=2)))) -# c = Config(nt) -# @test c.y.y.y.y == 2 -# end - -# @testset "from Dict" begin -# d = Dict(:x => Dict(:x => Dict(:x => Dict(:x => 2)))) -# c = Config(d) -# @test c.x.x.x.x == 2 -# end - -# @testset "merge!" begin -# a = Config(x = 1, y = Config(x = 1)) -# b = Config(x = 5, y = Config(x=5, z="hi")) -# c = merge(a, b) -# merge!(a, b) -# @test a == c -# @test a.x == 5 -# @test a.y.x == 5 -# @test a.y.z == "hi" -# end - -# @testset "isempty/delete_empty!" begin -# c = Config() -# c.x.x.x -# @test isempty(c) -# c.x.x.x -# EasyConfig.delete_empty!(c) -# @test isempty(c) -# end - -# @testset "StructTypes" begin -# @test isempty(StructTypes.keyvaluepairs(Config())) -# c = Config(x=1,y=2) -# @test StructTypes.keyvaluepairs(c)[:x] == 1 -# @test StructTypes.keyvaluepairs(c)[:y] == 2 -# end - -# @testset "@config" begin -# val = 5 -# c = @config (x.a=1, x.b=2, x.c.d.e.f.g = 3, z = val) -# @test c.x.a == 1 -# @test c.x.b == 2 -# @test c.x.c.d.e.f.g == 3 -# @test c.z == val - -# c2 = @config x.a=1 x.b=2 x.c.d.e.f.g=3 z=val - -# c3 = @config begin -# x.a = 1 -# x.b = 2 -# x.c.d.e.f.g = 3 -# z = val -# end - -# c4 = Config() -# c4.x.a = 1 -# c4.x.b = 2 -# c4.x.c.d.e.f.g = 3 -# c4.z = val - -# @test c == c2 == c3 -# end +@testset "AbstractDict interface" begin + c = Config(x=1, y=2) + @test length(c) == 2 + @test get(c, :x, 0) == 1 + @test get(c, "x", 0) == 1 + @test get(c, :nope, 0) == 0 + @test haskey(c, :x) + @test !haskey(c, :nope) + @test Dict(c) == Dict(:x => 1, :y => 2) + + @test delete!(c, :x) === c + @test !haskey(c, :x) + delete!(c, "y") + @test isempty(c) + + c = Config(x=1) + @test empty!(c) === c + @test isempty(c) + + a = Config(x=1) + b = copy(a) + @test a == b + b.x = 2 + @test a.x == 1 +end + +@testset "merge/merge!" begin + a = Config(x=1) + b = Config(y=Config(z=2)) + c = merge(a, b) + @test c == Config(x=1, y=Config(z=2)) + @test a == Config(x=1) # https://github.com/JuliaComputing/EasyConfig.jl/issues/8 + + merge!(a, b) + @test a == Config(x=1, y=Config(z=2)) + + a = Config(x=1, y=Config(x=1)) + b = Config(x=5, y=Config(x=5, z="hi")) + merge!(a, b) + @test a.x == 5 + @test a.y.x == 5 + @test a.y.z == "hi" +end + +@testset "show" begin + @test repr(Config()) == "Config()" + @test repr(Config(x=1)) == "Config(:x => 1)" + @test repr(Config(x=Config(y="two"))) == "Config(:x => Config(:y => \"two\"))" +end + +@testset "@config" begin + val = 5 + c = @config (x.a=1, x.b=2, x.c.d.e.f.g=3, z=val) + @test c.x.a == 1 + @test c.x.b == 2 + @test c.x.c.d.e.f.g == 3 + @test c.z == val + + c2 = @config x.a=1 x.b=2 x.c.d.e.f.g=3 z=val + + c3 = @config begin + x.a = 1 + x.b = 2 + x.c.d.e.f.g = 3 + z = val + end + + c4 = Config() + c4.x.a = 1 + c4.x.b = 2 + c4.x.c.d.e.f.g = 3 + c4.z = val + + @test c == c2 == c3 == c4 + + @test @config(x=1) == Config(x=1) + @test @config((x=1,)) == Config(x=1) + @test @config(begin end) == Config() + + # hygiene: macro's internal variable doesn't leak or clash + config = 10 + @test @config(x=config).x == 10 + + @test_throws Exception @eval @config x + 1 + @test_throws Exception @eval @config x[1] = 1 +end From 54f9370e72d17fa041d03894f5800f290a3a767b Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 10:49:27 -0400 Subject: [PATCH 4/7] improve Undefined show --- src/EasyConfig.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EasyConfig.jl b/src/EasyConfig.jl index 46dad61..3e398f5 100644 --- a/src/EasyConfig.jl +++ b/src/EasyConfig.jl @@ -24,6 +24,7 @@ end Base.getproperty(o::Undefined, k::Symbol) = getindex(o, k) Base.setproperty!(o::Undefined, k::Symbol, v) = setindex!(o, v, k) +Base.show(io::IO, o::Undefined) = print(io, "Undefined(", join(path(o), " → "), ')') #------------------------------------------------------------------------------# Settings @kwdef struct Settings From fe3701fe7f4c1ebf53eceb10b8417f4028d01d39 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 11:16:52 -0400 Subject: [PATCH 5/7] fix some bugs --- src/EasyConfig.jl | 49 ++++++++++++++++++------- test/runtests.jl | 92 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/src/EasyConfig.jl b/src/EasyConfig.jl index 3e398f5..2094f9d 100644 --- a/src/EasyConfig.jl +++ b/src/EasyConfig.jl @@ -13,9 +13,10 @@ path(o::Undefined) = getfield(o, :path) Base.getindex(o::Undefined, k::Symbol) = Undefined(parent(o), [path(o)..., k]) function Base.setindex!(o::Undefined{T}, v, key::Symbol) where {T} + v = _value(T, v) # validate before creating intermediate levels c = parent(o) for k in path(o) - c[k] = T() + haskey(c, k) || (c[k] = T()) c = c[k] end c[key] = v @@ -24,20 +25,23 @@ end Base.getproperty(o::Undefined, k::Symbol) = getindex(o, k) Base.setproperty!(o::Undefined, k::Symbol, v) = setindex!(o, v, k) -Base.show(io::IO, o::Undefined) = print(io, "Undefined(", join(path(o), " → "), ')') +Base.getindex(o::Undefined, k::AbstractString) = getindex(o, Symbol(k)) +Base.setindex!(o::Undefined, v, k::AbstractString) = setindex!(o, v, Symbol(k)) +Base.getproperty(o::Undefined, k::AbstractString) = getindex(o, Symbol(k)) +Base.setproperty!(o::Undefined, k::AbstractString, v) = setindex!(o, v, Symbol(k)) -#------------------------------------------------------------------------------# Settings -@kwdef struct Settings - autoconvert::Vector{Union{UnionAll, DataType}} = [AbstractDict{Symbol}, NamedTuple] -end -settings::Settings = Settings() +Base.show(io::IO, o::Undefined) = print(io, "Undefined(", join(path(o), " → "), ')') #------------------------------------------------------------------------------# Config struct Config{D <: AbstractDict{Symbol, Any}} <: AbstractDict{Symbol, Any} dict::D - Config(dict::D) where {D <: AbstractDict{Symbol, Any}} = new{D}(dict) + function Config(dict::D) where {D <: AbstractDict{Symbol, Any}} + map!(v -> _value(Config{D}, v), values(dict)) + new{D}(dict) + end end Config() = Config(Dict{Symbol,Any}()) +Config(x::Config) = x Config(x::AbstractDict{Symbol}) = Config{Dict{Symbol,Any}}(x) Config{D}() where {D} = Config(D()) function Config{D}(x::AbstractDict{Symbol}) where {D} @@ -57,11 +61,15 @@ end dict(o::Config) = getfield(o, :dict) +# Dict values are stored as Configs of the same type as their parent +_value(::Type{T}, v) where {T <: Config} = v +_value(::Type{T}, v::AbstractDict{Symbol}) where {T <: Config} = T(v) +_value(::Type{T}, v::T) where {T <: Config} = v +_value(::Type{<:Config}, v::Undefined) = throw(ArgumentError("Cannot store an undefined key path: $v")) + ## Getting and Setting ## Base.getindex(o::Config, k::Symbol) = get(o, k, Undefined(o, [k])) -Base.setindex!(o::Config, v, k::Symbol) = setindex!(dict(o), v, k) -Base.setindex!(o::T, v::AbstractDict{Symbol}, k::Symbol) where {T <: Config} = setindex!(dict(o), T(v), k) -Base.setindex!(o::T, v::T, k::Symbol) where {T <: Config} = setindex!(dict(o), v, k) +Base.setindex!(o::T, v, k::Symbol) where {T <: Config} = setindex!(dict(o), _value(T, v), k) Base.getindex(o::Config, k::AbstractString) = getindex(o, Symbol(k)) Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) @@ -69,24 +77,38 @@ Base.setindex!(o::Config, v, k::AbstractString) = setindex!(o, v, Symbol(k)) Base.propertynames(o::Config) = keys(o) Base.getproperty(o::Config, k::Symbol) = getindex(o, k) Base.setproperty!(o::Config, k::Symbol, v) = setindex!(o, v, k) +Base.getproperty(o::Config, k::AbstractString) = getindex(o, Symbol(k)) +Base.setproperty!(o::Config, k::AbstractString, v) = setindex!(o, v, Symbol(k)) ## AbstractDict interface ## Base.get(o::Config, k::Symbol, default) = get(dict(o), k, default) Base.get(o::Config, k::AbstractString, default) = get(o, Symbol(k), default) +# Return the stored value, which may be a converted `default` +function Base.get!(f::Base.Callable, o::Config, k::Symbol) + haskey(dict(o), k) || (o[k] = f()) + return dict(o)[k] +end +Base.get!(f::Base.Callable, o::Config, k::AbstractString) = get!(f, o, Symbol(k)) +Base.get!(o::Config, k::Union{Symbol, AbstractString}, default) = get!(() -> default, o, k) Base.iterate(o::Config, s...) = iterate(dict(o), s...) Base.length(o::Config) = length(dict(o)) +Base.empty(o::Config) = Config(empty(dict(o))) Base.empty!(o::Config) = (empty!(dict(o)); o) Base.delete!(o::Config, k::Symbol) = (delete!(dict(o), k); o) Base.delete!(o::Config, k::AbstractString) = delete!(o, Symbol(k)) -Base.merge(a::Config, b::Config) = Config(merge(dict(a), dict(b))) +Base.merge(a::Config, bs::AbstractDict...) = merge!(copy(a), bs...) Base.merge!(a::Config, b::Config) = Config(merge!(dict(a), dict(b))) +Base.mergewith(f, a::Config, bs::AbstractDict...) = mergewith!(f, copy(a), bs...) Base.copy(o::Config) = Config(copy(dict(o))) #------------------------------------------------------------------------------# show # What gets used to show nested Configs by the AbstractDict show method: function Base.show(io::IO, o::Config) print(io, "Config(") - join(io, map(((k, v),) -> "$(repr(k)) => $(repr(v; context=io))", collect(o)), ", ") + if !Base.show_circular(io, o) + recur_io = IOContext(io, :SHOWN_SET => o) + join(io, map(((k, v),) -> "$(repr(k)) => $(repr(v; context=recur_io))", collect(o)), ", ") + end print(io, ')') end @@ -101,6 +123,7 @@ function deepmerge!(a::T, b::AbstractDict) where {T <: AbstractDict} end deepmerge(a, b) = deepmerge!(deepmerge!(Config(), a), b) +deepmerge(a::Config, b) = deepmerge!(deepmerge!(empty(a), a), b) #-----------------------------------------------------------------------------# @config diff --git a/test/runtests.jl b/test/runtests.jl index 8bf3f9e..c0ddda7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,4 @@ using EasyConfig -using OrderedCollections using Test @testset "Constructors" begin @@ -17,6 +16,11 @@ using Test @test d[:y] == 2 end + @testset "Config(::Config) doesn't rewrap" begin + c = Config(x=1) + @test Config(c) === c + end + @testset "AbstractDict values become Configs" begin c = Config(Dict(:x => Dict(:y => Dict(:z => 1)))) @test c.x isa Config @@ -25,10 +29,27 @@ using Test c.a = Dict(:b => 2) @test c.a isa Config @test c.a.b == 2 + + c = Config(Dict{Symbol, Any}(:x => Dict(:y => Dict(:z => 1)))) + @test c.x isa Config + @test c.x.y isa Config + @test c.x.y.z == 1 + + c = Config(IdDict{Symbol, Any}(:x => Dict(:y => 1))) + @test c.x isa Config{IdDict{Symbol, Any}} + end + + @testset "Config values are stored, not copied" begin + c = Config() + s = Config() + c.s = s + s.x = 1 + @test c.s === s + @test c.s.x == 1 end @testset "Config{D}" begin - D = OrderedDict{Symbol, Any} + D = IdDict{Symbol, Any} @test Config{D}() isa Config{D} c = Config{D}(Dict(:x => 1)) @test c isa Config{D} @@ -64,12 +85,37 @@ end @test c.f.g == 3 end + @testset "Reusing an Undefined doesn't overwrite earlier writes" begin + c = Config() + u = c.a.b + u.x = 1 + u.y = 2 + @test c.a.b.x == 1 + @test c.a.b.y == 2 + end + @testset "Nested assignment preserves dict type" begin - c = Config(OrderedDict{Symbol, Any}()) + c = Config(IdDict{Symbol, Any}()) c.b.c = 1 c.a = 2 - @test c.b isa Config{OrderedDict{Symbol, Any}} - @test collect(keys(c)) == [:b, :a] + @test c.b isa Config{IdDict{Symbol, Any}} + end + + @testset "Undefined can't be stored" begin + c = Config() + @test_throws ArgumentError c.b = c.a + @test_throws ArgumentError c.b.c = c.a + @test isempty(c) + end + + @testset "String keys" begin + c = Config() + c["x"]["y"] = 1 + @test c.x.y == 1 + c."a"."b" = 2 + @test c.a.b == 2 + @test c."a"."b" == 2 + @test c.z["w"] isa EasyConfig.Undefined end end @@ -97,6 +143,30 @@ end @test a == b b.x = 2 @test a.x == 1 + + @testset "get! returns the stored value" begin + c = Config() + r = get!(c, :x, Dict(:a => 1)) + @test r isa Config + r.b = 2 + @test c.x.b == 2 + @test get!(c, "x", 0) === r + @test get!(() -> 0, c, :y) == 0 + @test c.y == 0 + end + + @testset "Results keep the dict type" begin + D = IdDict{Symbol, Any} + c = Config(D(:x => 1, :y => 2)) + @test empty(c) isa Config{D} + @test isempty(empty(c)) + @test filter(p -> p[1] == :x, c) == Config(x=1) + @test filter(p -> p[1] == :x, c) isa Config{D} + @test merge(c, Config(z=3)) isa Config{D} + @test mergewith(+, c, Config(x=10)) isa Config{D} + @test mergewith(+, c, Config(x=10)).x == 11 + @test EasyConfig.deepmerge(c, Config(z=3)) isa Config{D} + end end @testset "merge/merge!" begin @@ -109,6 +179,13 @@ end merge!(a, b) @test a == Config(x=1, y=Config(z=2)) + @test merge(Config(x=1), Dict(:y => Dict(:z => 2))) == Config(x=1, y=Config(z=2)) + @test merge(Config(x=1), Config(y=2), Config(z=3)) == Config(x=1, y=2, z=3) + + a = Config(y=Config(x=1)) + @test EasyConfig.deepmerge(a, Config(y=Config(z=2))) == Config(y=Config(x=1, z=2)) + @test a == Config(y=Config(x=1)) + a = Config(x=1, y=Config(x=1)) b = Config(x=5, y=Config(x=5, z="hi")) merge!(a, b) @@ -121,6 +198,11 @@ end @test repr(Config()) == "Config()" @test repr(Config(x=1)) == "Config(:x => 1)" @test repr(Config(x=Config(y="two"))) == "Config(:x => Config(:y => \"two\"))" + + c = Config() + c.self = c + @test occursin("circular reference", repr(c)) + @test occursin("circular reference", repr(MIME"text/plain"(), c)) end @testset "@config" begin From 272c2215e86d42f9f042caf5e2906110a9372486 Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 11:17:54 -0400 Subject: [PATCH 6/7] bump version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6419cda..2765142 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "EasyConfig" uuid = "acab07b0-f158-46d4-8913-50acef6d41fe" -version = "0.1.16" +version = "0.2.0" authors = ["joshday "] [compat] From 4d242d73f200bd7a5e9bee0b1cf1795a8d45138e Mon Sep 17 00:00:00 2001 From: Josh Day Date: Tue, 29 Sep 2026 11:41:01 -0400 Subject: [PATCH 7/7] update workflows, remove unused docs --- .github/dependabot.yml | 21 +++++ .github/workflows/CI.yml | 62 ++++++++------- .github/workflows/CompatHelper.yml | 24 ------ .github/workflows/dependabot-automerge.yml | 10 +++ .gitignore | 2 - README.md | 18 +---- docs/Manifest.toml | 92 ---------------------- docs/Project.toml | 2 - docs/make.jl | 17 ---- docs/src/index.md | 8 -- 10 files changed, 67 insertions(+), 189 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/CompatHelper.yml create mode 100644 .github/workflows/dependabot-automerge.yml delete mode 100644 docs/Manifest.toml delete mode 100644 docs/Project.toml delete mode 100644 docs/make.jl delete mode 100644 docs/src/index.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0134ffc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + # Keep GitHub Actions up to date + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" + + # Keep Julia dependencies up to date + - package-ecosystem: "julia" + directory: "/" + schedule: + interval: "weekly" + groups: + julia-deps: + patterns: + - "*" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 77288dc..f351d87 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,49 +1,51 @@ name: CI on: push: - branches: [master] - tags: ["*"] + branches: + - main + - master + tags: ['*'] pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + name: Julia ${{ matrix.version }} / ${{ matrix.os }} runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + actions: write + contents: read strategy: fail-fast: false matrix: version: - - '1.7' - - '1' # automatically expands to the latest stable 1.x release of Julia - - 'nightly' + - '1' + - 'lts' os: - ubuntu-latest - - macOS-latest - windows-latest - arch: - - x64 - - x86 - exclude: - - os: macOS-latest - arch: x86 + - macOS-latest steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v7 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 - with: - file: lcov.info + + # Single aggregate check for branch protection to require + ci-success: + name: CI success + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Fail if any needed job did not succeed + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }} + run: exit 1 diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml deleted file mode 100644 index 9a88ca6..0000000 --- a/.github/workflows/CompatHelper.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: CompatHelper - -on: - schedule: - - cron: '00 00 * * *' - -jobs: - CompatHelper: - runs-on: ${{ matrix.os }} - strategy: - matrix: - julia-version: [1.2.0] - julia-arch: [x86] - os: [ubuntu-latest] - steps: - - uses: julia-actions/setup-julia@latest - with: - version: ${{ matrix.julia-version }} - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: julia -e 'using CompatHelper; CompatHelper.main()' \ No newline at end of file diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..2a28937 --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,10 @@ +name: Dependabot auto-merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + uses: RallypointOne/.github/.github/workflows/dependabot-automerge.yml@main diff --git a/.gitignore b/.gitignore index 99e2cea..1388e96 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ .DS_Store /Manifest.toml /dev/ -/docs/build/ -/docs/site/ diff --git a/README.md b/README.md index 5550854..7259472 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![Build status](https://github.com/JuliaComputing/EasyConfig.jl/workflows/CI/badge.svg)](https://github.com/JuliaComputing/EasyConfig.jl/actions?query=workflow%3ACI+branch%3Amaster) -[![Codecov](https://codecov.io/gh/JuliaComputing/EasyConfig.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaComputing/EasyConfig.jl) [![deps](https://juliahub.com/docs/EasyConfig/deps.svg)](https://juliahub.com/ui/Packages/EasyConfig/tMFix?t=2) [![version](https://juliahub.com/docs/EasyConfig/version.svg)](https://juliahub.com/ui/Packages/EasyConfig/tMFix) [![pkgeval](https://juliahub.com/docs/EasyConfig/pkgeval.svg)](https://juliahub.com/ui/Packages/EasyConfig/tMFix) @@ -76,18 +75,9 @@ let end ``` - # Note -- Accessing a property that doesn't exist will create an empty `Config()`. -- Clean up stranded empty `Config`s with `delete_empty!(::Config)`. - -```julia -c = Config() - -c.one.two.three.four.five.six == Config() - -# Internally we make the assumption that empty Config's shouldn't be there. -# Some functions will therefore call `delete_empty!` under the hood: -isempty(c) == true -``` +- Accessing a non-existent property creates an `Undefined`. +- This is necessary for assigning deeply nested properties (an eventual `setindex!` after nested `getindex`-es). + - `config.a == Undefined(config, [:a])`, `config.a.b == Undefined(config, [:a, :b])`, etc. + - `config.a.b.c = 1` == `Undefined(config, [:a, :b])[:c] = 1` diff --git a/docs/Manifest.toml b/docs/Manifest.toml deleted file mode 100644 index b748768..0000000 --- a/docs/Manifest.toml +++ /dev/null @@ -1,92 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[DocStringExtensions]] -deps = ["LibGit2", "Markdown", "Pkg", "Test"] -git-tree-sha1 = "88bb0edb352b16608036faadcc071adda068582a" -uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.8.1" - -[[Documenter]] -deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "646ebc3db49889ffeb4c36f89e5d82c6a26295ff" -uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.24.7" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[JSON]] -deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" -uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.0" - -[[LibGit2]] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" - -[[Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" - -[[Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" - -[[Parsers]] -deps = ["Dates", "Test"] -git-tree-sha1 = "75d07cb840c300084634b4991761886d0d762724" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "1.0.1" - -[[Pkg]] -deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" - -[[Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" - -[[Random]] -deps = ["Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/docs/Project.toml b/docs/Project.toml deleted file mode 100644 index dfa65cd..0000000 --- a/docs/Project.toml +++ /dev/null @@ -1,2 +0,0 @@ -[deps] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl deleted file mode 100644 index 2bc9122..0000000 --- a/docs/make.jl +++ /dev/null @@ -1,17 +0,0 @@ -using Documenter, EasyConfig - -makedocs(; - modules=[EasyConfig], - format=Documenter.HTML(), - pages=[ - "Home" => "index.md", - ], - repo="https://github.com/joshday/EasyConfig.jl/blob/{commit}{path}#L{line}", - sitename="EasyConfig.jl", - authors="joshday ", - assets=String[], -) - -deploydocs(; - repo="github.com/joshday/EasyConfig.jl", -) diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index fa47f43..0000000 --- a/docs/src/index.md +++ /dev/null @@ -1,8 +0,0 @@ -# EasyConfig.jl - -```@index -``` - -```@autodocs -Modules = [EasyConfig] -```