Skip to content

Commit

Permalink
Expand functionality for savename (#50)
Browse files Browse the repository at this point in the history
* expand functionality for savename

* re-organize source

* tests for expand

* documentation

* changelog
  • Loading branch information
Datseris authored May 19, 2019
1 parent 3ed699e commit d54c664
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# 0.4.0
* Add expand functionality to `savename`, which handles better containers with nested containers (#50)
* `produce_or_load` now allows the possibility of not loading the file
* New function `struct2dict` that converts a struct to a dictionary (for saving)
# 0.3.0
Expand Down
14 changes: 8 additions & 6 deletions docs/src/name.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,31 @@ This is what the function [`savename`](@ref) does. Of course, you don't have to

```@docs
savename
@dict
@strdict
@ntuple
```

Notice that this naming scheme integrates perfectly with Parameters.jl.

Two convenience functions are also provided to easily switch between named tuples and dictionaries:
## Convenience functions
Convenience functions are provided to easily create named tuples, dictionaries as well as switch between them:
```@docs
@dict
@strdict
@ntuple
ntuple2dict
dict2ntuple
```

## Customizing `savename`
You can customize [`savename`](@ref) for your own Types. For example you could make it so that it only uses some specific keys instead of all of them, only specific types, or you could make it access data in a different way (maybe even loading files!). You can even make it have
a custom `prefix`!
You can customize [`savename`](@ref) for your own Types. For example you could make it so that it only uses some specific keys instead of all of them, only specific types, or you could make it access data in a different way (maybe even loading files!). You can even make it have a custom `prefix`!

To do that you may extend the following functions:
```@docs
DrWatson.allaccess
DrWatson.access
DrWatson.default_allowed
DrWatson.default_prefix
DrWatson.default_expand
```

See [Real World Examples](@ref) for an example of customizing `savename`.
Specifically, have a look at [`savename` and nested containers](@ref) for a way to
50 changes: 50 additions & 0 deletions docs/src/real_world.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,56 @@ println( savename(e1) )
println( savename(e2) )
```

## `savename` and nested containers
In the case of user-defined structs and projects of significant complexity, it is often necessary that your "main" container has other containers as subfields.
`savename` can adapt to these situations as well.
Consider the following example, where I need a core struct that represents a spatio temporal system, and its simulation:
```@example customizing
struct SpatioTemporalSystem
model::String # system codeword
N # Integer or Tuple of integers: spatial extent
Δt::Real # sampling time in real time units
p # parameters. nothing or Dict{Symbol}
end
const STS = SpatioTemporalSystem
struct SpatioTemporalTimeseries
sts::STS
T::Int # total frame amount
ic # initial condition (matrix, string, seed)
fields::Dict # resulting timeseries, dictionary of string to vector
end
const STT = SpatioTemporalTimeseries
```
For my use case, `p` can be `nothing` or it can be a dictionary itself, containing the possible parameters the spatiotemporal systems can have.
To adapt `savename` to situations like this, we use the functionality surrounding [`DrWatson.default_expand`](@ref).

Expanding the necessary methods allows me to do:
```@example customizing
DrWatson.allaccess(c::STS) = (:N, :Δt, :p)
DrWatson.default_prefix(c::STS) = c.model
DrWatson.default_allowed(c::STS) = (Real, Tuple, Dict, String)
DrWatson.default_expand(c::STS) = ["p"]
bk = STS("barkley", 60, 0.1, nothing)
savename(bk)
```
and when I do want to use different parameters than the default:
```@example customizing
a = 0.3; b = 0.5
bk = STS("barkley", 60, 0.1, @dict a b)
savename(bk)
```

Expanding to the second struct is also fine:
```@example customizing
DrWatson.default_prefix(c::STT) = savename(c.sts)
stt = STT(bk, 1000, nothing, Dict("U"=>rand(100), "V"=>rand(100)))
savename(stt)
```



## Stopping "Did I run this?"
It can become very tedious to have a piece of code that you may or may not have run and may or may not have saved the produced data. You then constantly ask yourself "Did I run this?". Typically one uses `isfile` and an `if` clause to either load a file or run some code. Especially in the cases where the code takes only a couple of minutes to finish you are left in a dilemma "Is it even worth it to save?".

Expand Down
117 changes: 65 additions & 52 deletions src/naming.jl
Original file line number Diff line number Diff line change
@@ -1,51 +1,6 @@
export savename, @dict, @ntuple, @strdict
export ntuple2dict, dict2ntuple

"""
allaccess(c)
Return all the keys `c` can be accessed using [`access`](@ref).
For dictionaries/named tuples this is `keys(c)`,
for everything else it is `fieldnames(typeof(c))`.
"""
allaccess(c::AbstractDict) = collect(keys(c))
allaccess(c::NamedTuple) = keys(c)
allaccess(c::Any) = fieldnames(typeof(c))
allaccess(c::DataType) = fieldnames(c)
allaccess(c::String) = error("`c` must be a container, not a string!")

"""
access(c, key)
Access `c` with given key. For `AbstractDict` this is `getindex`,
for anything else it is `getproperty`.
access(c, keys...)
When given multiple keys, `access` is called recursively, i.e.
`access(c, key1, key2) = access(access(c, key1), key2)` and so on.
For example, if `c, c.k1` are `NamedTuple`s then
`access(c, k1, k2) == c.k1.k2`.
!!! note
Please only extend the single key method when customizing `access`
for your own Types.
"""
access(c, keys...) = access(access(c, keys[1]), Base.tail(keys)...)
access(c::AbstractDict, key) = getindex(c, key)
access(c, key) = getproperty(c, key)

"""
default_allowed(c) = (Real, String, Symbol)
Return the (super-)Types that will be used as `allowedtypes`
in [`savename`](@ref) or other similar functions.
"""
default_allowed(c) = (Real, String, Symbol)

"""
default_prefix(c) = ""
Return the `prefix` that will be used by default
in [`savename`](@ref) or other similar functions.
"""
default_prefix(c) = ""

"""
savename([prefix,], c [, suffix]; kwargs...)
Create a shorthand name, commonly used for saving a file, based on the
Expand Down Expand Up @@ -84,6 +39,10 @@ it ends as a path (`/` or `\\`) then the `connector` is ommited.
```
then the integer value is used in the name instead.
* `connector = "_"` : string used to connect the various entries.
* `expand::Vector{String} = default_expand` : keys that will be expanded
to the `savename` of their contents, to allow for nested containers.
By default is empty. Notice that the type of the container must also be
allowed for `expand` to take effect!
## Examples
```julia
Expand All @@ -107,7 +66,7 @@ savename(prefix::String, c::Any; kwargs...) = savename(prefix, c, ""; kwargs...)
function savename(prefix::String, c, suffix::String;
allowedtypes = default_allowed(c),
accesses = allaccess(c), digits = 3,
connector = "_")
connector = "_", expand::Vector{String} = default_expand(c))

labels = vecstring(accesses) # make it vector of strings
p = sortperm(labels)
Expand All @@ -120,20 +79,74 @@ function savename(prefix::String, c, suffix::String;
if any(x -> (t <: x), allowedtypes)
!first && (s *= connector)
if t <: AbstractFloat
if round(val; digits = digits) == round(Int, val)
val = round(Int, val)
else
val = round(val; digits = digits)
end
x = round(val; digits = digits); y = round(Int, val)
val = x == y ? y : x
end
if label expand
s *= label*"="*'('*savename(val;connector=",")*')'
else
s *= label*"="*string(val)
end
s *= label*"="*string(val);
first = false
end
end
suffix != "" && (s *= "."*suffix)
return s
end

"""
allaccess(c)
Return all the keys `c` can be accessed using [`access`](@ref).
For dictionaries/named tuples this is `keys(c)`,
for everything else it is `fieldnames(typeof(c))`.
"""
allaccess(c::AbstractDict) = collect(keys(c))
allaccess(c::NamedTuple) = keys(c)
allaccess(c::Any) = fieldnames(typeof(c))
allaccess(c::DataType) = fieldnames(c)
allaccess(c::String) = error("`c` must be a container, not a string!")

"""
access(c, key)
Access `c` with given key. For `AbstractDict` this is `getindex`,
for anything else it is `getproperty`.
access(c, keys...)
When given multiple keys, `access` is called recursively, i.e.
`access(c, key1, key2) = access(access(c, key1), key2)` and so on.
For example, if `c, c.k1` are `NamedTuple`s then
`access(c, k1, k2) == c.k1.k2`.
!!! note
Please only extend the single key method when customizing `access`
for your own Types.
"""
access(c, keys...) = access(access(c, keys[1]), Base.tail(keys)...)
access(c::AbstractDict, key) = getindex(c, key)
access(c, key) = getproperty(c, key)

"""
default_allowed(c) = (Real, String, Symbol)
Return the (super-)Types that will be used as `allowedtypes`
in [`savename`](@ref) or other similar functions.
"""
default_allowed(c) = (Real, String, Symbol)

"""
default_prefix(c) = ""
Return the `prefix` that will be used by default
in [`savename`](@ref) or other similar functions.
"""
default_prefix(c) = ""

"""
default_expand(c) = String[]
Keys that should be expanded in their `savename` within [`savename`](@ref).
Must be `Vector{String}` (as all keys are first translated into strings inside
`savename`).
"""
default_expand(c) = String[]

"""
@dict vars...
Create a dictionary out of the given variables that has as keys the variable
Expand Down
13 changes: 12 additions & 1 deletion test/naming_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,15 @@ d = 5; e = @dict c d
@test DrWatson.access(e, :c, :a) == a
ff = dict2ntuple(e)
@test DrWatson.access(ff, :c, :a) == a
@test ff.c.a == a
@test ff.c.a == a

# Expand tests:
a = 3; b = 4
c = @dict a b
d = 5; e = @dict c d

s = savename(e; allowedtypes = (Any,), expand = ["c"])
@test '(' s
@test ')' s
@test occursin("a=3", s)
@test occursin("b=4", s)

2 comments on commit d54c664

@Datseris
Copy link
Member Author

Choose a reason for hiding this comment

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

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/867

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

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

git tag -a v0.4.0 -m "<description of version>" d54c664b2384848a0691aa51b89b13aa6433cb91
git push origin v0.4.0

Please sign in to comment.