Skip to content

Commit e3796e5

Browse files
authored
Allow for OrderedDicts to be passed to dict_list and other similar functions (#283)
* Allow for OrderedDicts to be passed to dict_list and other similar functions. Anywhere where functions required the input to be a ::Dict{K,T} were changed to ::AbstractDict{K,T} to allow for OrderedDicts. The only place this was not changed was in the project_setup where it gets the pkg["name"] because that returns a Dict. Added an optional kwarg `order` argument to struct2dict to return an OrderedDict (default is false), where order refers to insertion order. If a set of OrderedDicts is used with dict_list an OrderedDict should be returned. checktagtype! will also return a regular Dict. OrderedDict from DataStructures is also exported from DrWatson. * Removed `DataStructures` dependency. struct2dict now allows the user to specify the type of dictionary to be returned. If one is not specified, then a `Dict` is returned. * Added tests for example usage of OrderedDicts to `stool_tests.jl` `tostringdict` and `tosymboldict` also allow for a specific type of `dict` to be returned. Still have the `DataStructures` dependency. Unsure why and not sure how to fix. Have checked the files and the only place DataStructures is used is in `stool_tests.jl` * `DataStructures` dependency is actually removed this time. * Fixed minor typos and comments in `naming.jl` and `saving_tools.jl`.
1 parent f4c22d1 commit e3796e5

File tree

7 files changed

+102
-40
lines changed

7 files changed

+102
-40
lines changed

Project.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@ julia = "1.0"
2323
[extras]
2424
BSON = "fbb218c0-5317-5bc6-957e-2ee96dd4b1f0"
2525
CSVFiles = "5d742f6a-9f54-50ce-8119-2520741973ca"
26+
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
2627
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
28+
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
2729
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
2830
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
2931
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
3032
Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
3133
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
3234
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
33-
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
3435

3536
[targets]
36-
test = [
37-
"Test", "BSON", "FileIO", "Parameters", "DataFrames",
38-
"JLD2", "Statistics", "Dates", "CSVFiles", "CodecZlib"
39-
]
37+
test = ["Test", "BSON", "FileIO", "Parameters", "DataFrames", "JLD2", "Statistics", "Dates", "CSVFiles", "CodecZlib", "DataStructures"]

src/DrWatson.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"The perfect sidekick to your scientific inquiries"
22
module DrWatson
33
import Pkg, LibGit2
4-
54
const PATH_SEPARATOR = joinpath("_", "_")[2]
65

76
# Misc functions for kw-macros

src/dict_list.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
dict_list(c::Dict)
2+
dict_list(c::AbstractDict)
33
Expand the dictionary `c` into a vector of dictionaries.
44
Each entry has a unique combination from the product of the `Vector`
55
values of the dictionary while the non-`Vector` values are kept constant
@@ -47,7 +47,7 @@ julia> dict_list(c)
4747
Dict(:a=>2,:b=>4,:run=>"tri",:e=>[3, 5],:model=>"linear")
4848
```
4949
"""
50-
function dict_list(c::Dict)
50+
function dict_list(c::AbstractDict)
5151
if contains_partially_restricted(c)
5252
# The method for generating the restricted parameter set is as follows:
5353
# 1. Remove any nested parameter restrictions (#209)
@@ -94,7 +94,7 @@ function is_solution_subset_of_existing(trial, trial_solutions)
9494
return false
9595
end
9696

97-
function _dict_list(c::Dict)
97+
function _dict_list(c::AbstractDict)
9898
iterable_fields = filter(k -> typeof(c[k]) <: Vector, keys(c))
9999
non_iterables = setdiff(keys(c), iterable_fields)
100100

@@ -152,7 +152,7 @@ function DependentParameter(value::DependentParameter, condition::Function)
152152
DependentParameter(value.value, new_condition)
153153
end
154154

155-
contains_partially_restricted(d::Dict) = any(contains_partially_restricted,values(d))
155+
contains_partially_restricted(d::AbstractDict) = any(contains_partially_restricted,values(d))
156156
contains_partially_restricted(d::Vector) = any(contains_partially_restricted,d)
157157
contains_partially_restricted(::DependentParameter) = true
158158
contains_partially_restricted(::Any) = false
@@ -170,7 +170,7 @@ In a case like this:
170170
171171
Broadcasting is obviously not wanted as `:b` should retain it's type of `Vector{Int}`.
172172
"""
173-
function unexpand_restricted(c::Dict{T}) where T
173+
function unexpand_restricted(c::AbstractDict{T}) where T
174174
_c = Dict{T,Any}() # There are hardly any cases where this will not be any.
175175
for k in keys(c)
176176
if c[k] isa AbstractVector && any(el->eltype(el) <: DependentParameter, c[k])

src/naming.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ See also [`parse_savename`](@ref) and [`@savename`](@ref).
6060
called with its default arguments (so customization here is possible only
6161
by rolling your own container type). Containers leading to empty `savename`
6262
are skipped.
63-
* `equals = "="` : Connector between name and value. Can be useful to modify for
63+
* `equals = "="` : Connector between name and value. Can be useful to modify for
6464
adding space `" = "`.
6565
6666
## Examples
@@ -271,7 +271,7 @@ end
271271

272272
"""
273273
esc_dict_expr_from_vars(vars)
274-
Transform a `Tuple` of `Symbol` and assignments (`a=b`)
274+
Transform a `Tuple` of `Symbol` and assignments (`a=b`)
275275
into a dictionary where each `Symbol` in `vars`
276276
defines a key-value pair. The value is obtained by evaluating the `Symbol` in
277277
the macro calling environment.
@@ -356,24 +356,26 @@ ntuple2dict(nt::NamedTuple) = Dict(k => nt[k] for k in keys(nt))
356356
Convert a dictionary (with `Symbol` or `String` as key type) to
357357
a `NamedTuple`.
358358
"""
359-
function dict2ntuple(dict::Dict{String, T}) where T
359+
function dict2ntuple(dict::AbstractDict{String, T}) where T
360360
NamedTuple{Tuple(Symbol.(keys(dict)))}(values(dict))
361361
end
362-
function dict2ntuple(dict::Dict{Symbol, T}) where T
362+
function dict2ntuple(dict::AbstractDict{Symbol, T}) where T
363363
NamedTuple{Tuple(keys(dict))}(values(dict))
364364
end
365365

366366
"""
367367
tostringdict(d)
368368
Change a dictionary with key type `Symbol` to have key type `String`.
369369
"""
370-
tostringdict(d) = Dict(zip(String.(keys(d)), values(d)))
370+
tostringdict(::Type{DT},d) where {DT<:AbstractDict} = DT(zip(String.(keys(d)), values(d)))
371+
tostringdict(d) = tostringdict(Dict,d)
371372

372373
"""
373374
tosymboldict(d)
374375
Change a dictionary with key type `String` to have key type `Symbol`.
375376
"""
376-
tosymboldict(d) = Dict(zip(Symbol.(keys(d)), values(d)))
377+
tosymboldict(::Type{DT},d) where {DT<:AbstractDict} = DT(zip(Symbol.(keys(d)), values(d)))
378+
tosymboldict(d) = tosymboldict(Dict,d)
377379

378380
"""
379381
parse_savename(filename::AbstractString; kwargs...)

src/saving_files.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ with the global path that it is saved at (`s`).
1010
If the file does not exist then call `file = f(config)`, with `f` your function
1111
that produces your data. Then save the `file` as `s` and then return `file, s`.
1212
13-
The function `f` should return a dictionary if the data are saved in the default
13+
The function `f` should return a dictionary if the data are saved in the default
1414
format of JLD2.jl., the macro [`@strdict`](@ref) can help with that.
1515
1616
You can use a [do-block]
@@ -118,7 +118,7 @@ end
118118
# tag saving #
119119
################################################################################
120120
"""
121-
tagsave(file::String, d::Dict; safe = false, gitpath = projectdir(), storepatch = true, force = false, kwargs...)
121+
tagsave(file::String, d::AbstractDict; safe = false, gitpath = projectdir(), storepatch = true, force = false, kwargs...)
122122
First [`tag!`](@ref) dictionary `d` and then save `d` in `file`.
123123
If `safe = true` save the file using [`safesave`](@ref).
124124
@@ -144,7 +144,7 @@ end
144144

145145

146146
"""
147-
@tagsave(file::String, d::Dict; kwargs...)
147+
@tagsave(file::String, d::AbstractDict; kwargs...)
148148
Same as [`tagsave`](@ref) but one more field `:script` is added that records
149149
the local path of the script and line number that called `@tagsave`, see [`@tag!`](@ref).
150150
"""

src/saving_tools.jl

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ end
9696
"""
9797
read_stdout_stderr(cmd::Cmd)
9898
99-
Run `cmd` synchronously and capture stdout, stdin and a possible error exception.
99+
Run `cmd` synchronously and capture stdout, stdin and a possible error exception.
100100
Return a `NamedTuple` with the fields `exception`, `out` and `err`.
101101
"""
102102
function read_stdout_stderr(cmd::Cmd)
@@ -121,7 +121,7 @@ compared to its last commit; i.e. what `git diff HEAD` produces.
121121
The `gitpath` needs to point to a directory within a git repository,
122122
otherwise `nothing` is returned.
123123
124-
Be aware that `gitpatch` needs a working installation of Git, that
124+
Be aware that `gitpatch` needs a working installation of Git, that
125125
can be found in the current PATH.
126126
"""
127127
function gitpatch(path = projectdir(); try_submodule_diff=true)
@@ -160,7 +160,7 @@ end
160160
# Tagging
161161
########################################################################################
162162
"""
163-
tag!(d::Dict; gitpath = projectdir(), storepatch = true, force = false) -> d
163+
tag!(d::AbstractDict; gitpath = projectdir(), storepatch = true, force = false) -> d
164164
Tag `d` by adding an extra field `gitcommit` which will have as value
165165
the [`gitdescribe`](@ref) of the repository at `gitpath` (by default
166166
the project's gitpath). Do nothing if a key `gitcommit` already exists
@@ -192,7 +192,7 @@ Dict{Symbol,Any} with 3 entries:
192192
:x => 3
193193
```
194194
"""
195-
function tag!(d::Dict{K,T}; gitpath = projectdir(), storepatch = true, force = false, source = nothing) where {K,T}
195+
function tag!(d::AbstractDict{K,T}; gitpath = projectdir(), storepatch = true, force = false, source = nothing) where {K,T}
196196
@assert (K <: Union{Symbol,String}) "We only know how to tag dictionaries that have keys that are strings or symbols"
197197
c = gitdescribe(gitpath)
198198
c === nothing && return d # gitpath is not a git repo
@@ -205,7 +205,7 @@ function tag!(d::Dict{K,T}; gitpath = projectdir(), storepatch = true, force = f
205205
@warn "The dictionary already has a key named `gitcommit`. We won't "*
206206
"add any Git information."
207207
else
208-
d = checktagtype!(d)
208+
d = checktagtype!(d)
209209
d[commitname] = c
210210
# Only include patch info if `storepatch` is true and if we can get the info.
211211
if storepatch
@@ -225,37 +225,46 @@ function tag!(d::Dict{K,T}; gitpath = projectdir(), storepatch = true, force = f
225225
end
226226

227227
"""
228-
keyname(d::Dict{K,T}, key) where {K<:Union{Symbol,String},T}
228+
keyname(d::AbstractDict{K,T}, key) where {K<:Union{Symbol,String},T}
229229
230230
Check the key type of `d` and convert `key` to the appropriate type.
231231
"""
232-
function keyname(d::Dict{K,T}, key) where {K<:Union{Symbol,String},T}
232+
function keyname(d::AbstractDict{K,T}, key) where {K<:Union{Symbol,String},T}
233233
if K == Symbol
234234
return Symbol(key)
235235
end
236236
return String(key)
237237
end
238238

239239
"""
240-
checktagtype!(d::Dict{K,T}) where {K<:Union{Symbol,String},T}
240+
checktagtype!(d::AbstractDict{K,T}) where {K<:Union{Symbol,String},T}
241241
242242
Check if the value type of `d` allows `String` and promote it to do so if not.
243243
"""
244-
function checktagtype!(d::Dict{K,T}) where {K<:Union{Symbol,String},T}
244+
function checktagtype!(d::AbstractDict{K,T}) where {K<:Union{Symbol,String},T}
245+
DT = get_rawtype(typeof(d)) #concrete type of dictionary
245246
if !(String <: T)
246-
d = Dict{K, promote_type(T, String)}(d)
247+
d = DT{K, promote_type(T, String)}(d)
247248
end
248249
d
249250
end
250251

251252
"""
252-
scripttag!(d::Dict{K,T}, source::LineNumberNode; gitpath = projectdir(), force = false) where {K<:Union{Symbol,String},T}
253+
get_rawtype(D::DataType) = getproperty(parentmodule(D), nameof(D))
254+
255+
Return Concrete DataType from an `AbstractDict` `D`. Found online at:
256+
https://discourse.julialang.org/t/retrieve-the-type-of-abstractdict-without-parameters-from-a-concrete-dictionary-type/67567/3
257+
"""
258+
get_rawtype(D::DataType) = getproperty(parentmodule(D), nameof(D))
259+
260+
"""
261+
scripttag!(d::AbstractDict{K,T}, source::LineNumberNode; gitpath = projectdir(), force = false) where {K<:Union{Symbol,String},T}
253262
254263
Include a `script` field in `d`, containing the source file and line number in
255264
`source`. Do nothing if the field is already present unless `force = true`. Uses
256265
`gitpath` to make the source file path relative.
257266
"""
258-
function scripttag!(d::Dict{K,T}, source; gitpath = projectdir(), force = false) where {K,T}
267+
function scripttag!(d::AbstractDict{K,T}, source; gitpath = projectdir(), force = false) where {K,T}
259268
# We want this functionality to be separate from `tag!` to allow
260269
# inclusion of this information without the git tagging
261270
# functionality.
@@ -329,16 +338,18 @@ istaggable(x) = x isa AbstractDict
329338

330339

331340
"""
332-
struct2dict(s) -> d
341+
struct2dict([type = Dict,] s) -> d
333342
Convert a Julia composite type `s` to a dictionary `d` with key type `Symbol`
334-
that maps each field of `s` to its value. This can be useful in e.g. saving:
343+
that maps each field of `s` to its value. Simply passing `s` will return a regular dictionary.
344+
This can be useful in e.g. saving:
335345
```
336346
tagsave(savename(s), struct2dict(s))
337347
```
338348
"""
339-
function struct2dict(s)
340-
Dict(x => getfield(s, x) for x in fieldnames(typeof(s)))
349+
function struct2dict(::Type{DT},s) where {DT<:AbstractDict}
350+
DT(x => getfield(s, x) for x in fieldnames(typeof(s)))
341351
end
352+
struct2dict(s) = struct2dict(Dict,s)
342353

343354
"""
344355
struct2ntuple(s) -> n

test/stools_tests.jl

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using DrWatson, Test
2+
using DataStructures
3+
using JLD2
24

35
# Test commit function
46
com = gitdescribe(@__DIR__)
@@ -239,8 +241,8 @@ p = Dict(
239241

240242
# Testing nested @onlyif calls
241243
@test Set(dict_list(Dict(
242-
:a=>[1,2],
243-
:b => [3,4],
244+
:a=>[1,2],
245+
:b => [3,4],
244246
:c => @onlyif( :a == 2, [5, @onlyif(:b == 4, 6)])
245247
))) == Set([Dict(:a => 1,:b => 3),
246248
Dict(:a => 2,:b => 3,:c => 5),
@@ -270,9 +272,59 @@ for r in ret
270272
end
271273
rm(tmpdir, force = true, recursive = true)
272274
@test !isdir(tmpdir)
273-
274-
## is taggable
275+
## is taggable
275276
@test DrWatson.istaggable("test.jld2")
276277
@test !DrWatson.istaggable("test.csv")
277278
@test !DrWatson.istaggable(0.5)
278279
@test DrWatson.istaggable(Dict(:a => 0.5))
280+
281+
## Testing OrderedDict usage
282+
@testset "OrderedDict Tests" begin
283+
cd(@__DIR__)
284+
struct TestStruct
285+
z::Float64
286+
y::Int
287+
x::String
288+
end
289+
290+
struct TestStruct2 #this structure allows for the if statement to be run in checktagtype!, (will promote the valuetype to Any)
291+
z::Int64
292+
y::Int64
293+
x::Int64
294+
end
295+
296+
#test struct2dict
297+
t = TestStruct(2.0,1,"3") #this tests the case where struct2dict will by default not work
298+
d1 = struct2dict(t)
299+
d2 = struct2dict(OrderedDict,t)
300+
@test !all(collect(fieldnames(typeof(t))).==keys(d1)) #the example struct given does not have the keys in the same order when converted to a dict
301+
@test all(collect(fieldnames(typeof(t))).==keys(d2)) #OrderedDict should have the key in the same order as the struct
302+
303+
#test struct2dict
304+
t2 = TestStruct2(1,3,4)
305+
d3 = struct2dict(t2)
306+
d4 = struct2dict(OrderedDict,t2)
307+
@test isa(d3,Dict)
308+
@test isa(d4,OrderedDict)
309+
310+
#test tostringdict and tosymboldict
311+
d10 = tostringdict(OrderedDict,d4)
312+
@test isa(d10,OrderedDict)
313+
d11 = tosymboldict(OrderedDict,d10)
314+
@test isa(d11,OrderedDict)
315+
316+
#test checktagtype!
317+
@test isa(DrWatson.checktagtype!(d3),Dict)
318+
@test isa(DrWatson.checktagtype!(d11),OrderedDict)
319+
320+
#check tagsave
321+
sn = savename(d10,"jld2")
322+
tagsave(sn,d10,gitpath=findproject())
323+
324+
file = load(sn)
325+
display(file)
326+
@test "gitcommit" in keys(file)
327+
@test file["gitcommit"] |>typeof ==String
328+
rm(sn)
329+
330+
end

0 commit comments

Comments
 (0)