-
-
Notifications
You must be signed in to change notification settings - Fork 270
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Preferences provides a simple package configuration store; packages can store arbitrary configurations into `Dict` objects that get serialized into TOML files and stored within the `prefs` folder of a Julia depot.
- Loading branch information
1 parent
6679131
commit 3357746
Showing
8 changed files
with
409 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
module Preferences | ||
import ...Pkg, ..TOML | ||
import ..API: get_uuid | ||
import ..Types: parse_toml | ||
import Base: UUID | ||
|
||
export load_preferences, @load_preferences, | ||
save_preferences!, @save_preferences!, | ||
modify_preferences!, @modify_preferences!, | ||
clear_preferences!, @clear_preferences! | ||
|
||
|
||
""" | ||
depot_preferences_paths(uuid::UUID) | ||
Return the possible paths of all preferences file for the given package `UUID` saved in | ||
depot-wide `prefs` locations. | ||
""" | ||
function depot_preferences_paths(uuid::UUID) | ||
depots = reverse(Pkg.depots()) | ||
return [joinpath(depot, "prefs", string(uuid, ".toml")) for depot in depots] | ||
end | ||
|
||
""" | ||
get_uuid_throw(m::Module) | ||
Convert a `Module` to a `UUID`, throwing an `ArgumentError` if the given module does not | ||
correspond to a loaded package. This is expected for modules such as `Base`, `Main`, | ||
anonymous modules, etc... | ||
""" | ||
function get_uuid_throw(m::Module) | ||
uuid = get_uuid(m) | ||
if uuid === nothing | ||
throw(ArgumentError("Module does not correspond to a loaded package!")) | ||
end | ||
return uuid | ||
end | ||
|
||
""" | ||
recursive_merge(base::Dict, overrides::Dict...) | ||
Helper function to merge preference dicts recursively, honoring overrides in nested | ||
dictionaries properly. | ||
""" | ||
function recursive_merge(base::Dict, overrides::Dict...) | ||
new_base = Base._typeddict(base, overrides...) | ||
for override in overrides | ||
for (k, v) in override | ||
if haskey(new_base, k) && isa(new_base[k], Dict) && isa(override[k], Dict) | ||
new_base[k] = recursive_merge(new_base[k], override[k]) | ||
else | ||
new_base[k] = override[k] | ||
end | ||
end | ||
end | ||
return new_base | ||
end | ||
|
||
""" | ||
load_preferences(uuid::UUID) | ||
load_preferences(m::Module) | ||
Load the preferences for the given package, returning them as a `Dict`. Most users | ||
should use the `@load_preferences()` macro which auto-determines the calling `Module`. | ||
""" | ||
function load_preferences(uuid::UUID) | ||
# First, load from depots, merging as we go: | ||
prefs = Dict{String,Any}() | ||
for path in depot_preferences_paths(uuid) | ||
if isfile(path) | ||
prefs = recursive_merge(prefs, parse_toml(path)) | ||
end | ||
end | ||
|
||
# Finally, load from the currently-active project: | ||
proj_path = Base.active_project() | ||
if isfile(proj_path) | ||
project = parse_toml(proj_path) | ||
if haskey(project, "preferences") && isa(project["preferences"], Dict) | ||
proj_prefs = get(project["preferences"], string(uuid), Dict()) | ||
prefs = recursive_merge(prefs, proj_prefs) | ||
end | ||
end | ||
return prefs | ||
end | ||
load_preferences(m::Module) = load_preferences(get_uuid_throw(m)) | ||
|
||
""" | ||
save_preferences!(uuid::UUID, prefs::Dict; depot::Union{String,Nothing} = nothing) | ||
save_preferences!(m::Module, prefs::Dict; depot::Union{String,Nothing} = nothing) | ||
Save the preferences for the given package. Most users should use the | ||
`@save_preferences!()` macro which auto-determines the calling `Module`. See also the | ||
`modify_preferences!()` function (and the associated `@modifiy_preferences!()` macro) for | ||
easy load/modify/save workflows. | ||
The `depot` keyword argument allows saving of depot-wide preferences, as opposed to the | ||
default of project-specific preferences. Simply set the `depot` keyword argument to the | ||
path of a depot (use `Pkg.depots1()` for the default depot) and the preferences will be | ||
saved to that location. | ||
""" | ||
function save_preferences!(uuid::UUID, prefs::Dict; | ||
depot::Union{AbstractString,Nothing} = nothing) | ||
if depot === nothing | ||
# Save to project | ||
proj_path = Base.active_project() | ||
project = Dict{String,Any}() | ||
if isfile(proj_path) | ||
project = parse_toml(proj_path) | ||
end | ||
if !haskey(project, "preferences") | ||
project["preferences"] = Dict{String,Any}() | ||
end | ||
if !isa(project["preferences"], Dict) | ||
error("$(proj_path) has conflicting `preferences` entry type: Not a Dict!") | ||
end | ||
project["preferences"][string(uuid)] = prefs | ||
mkpath(dirname(proj_path)) | ||
open(proj_path, "w") do io | ||
TOML.print(io, project, sorted=true) | ||
end | ||
else | ||
path = joinpath(depot, "prefs", string(uuid, ".toml")) | ||
mkpath(dirname(path)) | ||
open(path, "w") do io | ||
TOML.print(io, prefs, sorted=true) | ||
end | ||
end | ||
return nothing | ||
end | ||
function save_preferences!(m::Module, prefs::Dict; | ||
depot::Union{AbstractString,Nothing} = nothing) | ||
return save_preferences!(get_uuid_throw(m), prefs; depot=depot) | ||
end | ||
|
||
""" | ||
modify_preferences!(f::Function, uuid::UUID) | ||
modify_preferences!(f::Function, m::Module) | ||
Supports `do`-block modification of preferences. Loads the preferences, passes them to a | ||
user function, then writes the modified `Dict` back to the preferences file. Example: | ||
```julia | ||
modify_preferences!(@__MODULE__) do prefs | ||
prefs["key"] = "value" | ||
end | ||
``` | ||
This function returns the full preferences object. Most users should use the | ||
`@modify_preferences!()` macro which auto-determines the calling `Module`. | ||
Note that this method does not support modifying depot-wide preferences; modifications | ||
always are saved to the active project. | ||
""" | ||
function modify_preferences!(f::Function, uuid::UUID) | ||
prefs = load_preferences(uuid) | ||
f(prefs) | ||
save_preferences!(uuid, prefs) | ||
return prefs | ||
end | ||
modify_preferences!(f::Function, m::Module) = modify_preferences!(f, get_uuid_throw(m)) | ||
|
||
""" | ||
clear_preferences!(uuid::UUID) | ||
clear_preferences!(m::Module) | ||
Convenience method to remove all preferences for the given package. Most users should | ||
use the `@clear_preferences!()` macro, which auto-determines the calling `Module`. This | ||
method clears not only project-specific preferences, but also depot-wide preferences, if | ||
the current user has the permissions to do so. | ||
""" | ||
function clear_preferences!(uuid::UUID) | ||
for path in depot_preferences_paths(uuid) | ||
try | ||
rm(path; force=true) | ||
catch | ||
@warn("Unable to remove preference path $(path)") | ||
end | ||
end | ||
|
||
# Clear the project preferences key, if it exists | ||
proj_path = Base.active_project() | ||
if isfile(proj_path) | ||
project = parse_toml(proj_path) | ||
if haskey(project, "preferences") && isa(project["preferences"], Dict) | ||
delete!(project["preferences"], string(uuid)) | ||
open(proj_path, "w") do io | ||
TOML.print(io, project, sorted=true) | ||
end | ||
end | ||
end | ||
end | ||
|
||
""" | ||
@load_preferences() | ||
Convenience macro to call `load_preferences()` for the current package. | ||
""" | ||
macro load_preferences() | ||
return quote | ||
load_preferences($(esc(get_uuid_throw(__module__)))) | ||
end | ||
end | ||
|
||
""" | ||
@save_preferences!(prefs) | ||
Convenience macro to call `save_preferences!()` for the current package. Note that | ||
saving to a depot path is not supported in this macro, use `save_preferences!()` if you | ||
wish to do that. | ||
""" | ||
macro save_preferences!(prefs) | ||
return quote | ||
save_preferences!($(esc(get_uuid_throw(__module__))), $(esc(prefs))) | ||
end | ||
end | ||
|
||
""" | ||
@modify_preferences!(func) | ||
Convenience macro to call `modify_preferences!()` for the current package. | ||
""" | ||
macro modify_preferences!(func) | ||
return quote | ||
modify_preferences!($(esc(func)), $(esc(get_uuid_throw(__module__)))) | ||
end | ||
end | ||
|
||
""" | ||
@clear_preferences!() | ||
Convenience macro to call `clear_preferences!()` for the current package. | ||
""" | ||
macro clear_preferences!() | ||
return quote | ||
preferences!($(esc(get_uuid_throw(__module__)))) | ||
end | ||
end | ||
end # module Preferences |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
module PreferencesTests | ||
import ..Pkg | ||
using ..Utils, ..Pkg.TOML | ||
using Test, Pkg.Preferences | ||
|
||
@testset "Preferences" begin | ||
# Create a temporary package, store some preferences within it. | ||
with_temp_env() do project_dir | ||
uuid = Base.UUID(UInt128(0)) | ||
save_preferences!(uuid, Dict("foo" => "bar")) | ||
|
||
project_path = joinpath(project_dir, "Project.toml") | ||
@test isfile(project_path) | ||
proj = Pkg.Types.parse_toml(project_path) | ||
@test haskey(proj, "preferences") | ||
@test isa(proj["preferences"], Dict) | ||
@test haskey(proj["preferences"], string(uuid)) | ||
@test isa(proj["preferences"][string(uuid)], Dict) | ||
@test proj["preferences"][string(uuid)]["foo"] == "bar" | ||
|
||
prefs = modify_preferences!(uuid) do prefs | ||
prefs["foo"] = "baz" | ||
prefs["spoon"] = [Dict("qux" => "idk")] | ||
end | ||
@test prefs == load_preferences(uuid) | ||
|
||
clear_preferences!(uuid) | ||
proj = Pkg.Types.parse_toml(project_path) | ||
@test !haskey(proj, "preferences") | ||
end | ||
|
||
temp_pkg_dir() do project_dir | ||
# Test setting of depot-wide preferences | ||
uuid = Base.UUID(UInt128(0)) | ||
toml_path = last(Pkg.Preferences.depot_preferences_paths(uuid)) | ||
|
||
@test isempty(load_preferences(uuid)) | ||
@test !isfile(toml_path) | ||
|
||
# Now, save something | ||
save_preferences!(uuid, Dict("foo" => "bar"); depot=Pkg.depots1()) | ||
@test isfile(toml_path) | ||
prefs = load_preferences(uuid) | ||
@test load_preferences(uuid)["foo"] == "bar" | ||
|
||
prefs = modify_preferences!(uuid) do prefs | ||
prefs["foo"] = "baz" | ||
prefs["spoon"] = [Dict("qux" => "idk")] | ||
end | ||
|
||
# Test that we get the properly-merged prefs, but that the | ||
# depot-wide file stays the same: | ||
@test prefs == load_preferences(uuid) | ||
toml_prefs = Pkg.Types.parse_toml(toml_path) | ||
@test toml_prefs["foo"] != prefs["foo"] | ||
@test !haskey(toml_prefs, "spoon") | ||
|
||
clear_preferences!(uuid) | ||
@test !isfile(toml_path) | ||
end | ||
|
||
# Do a test within a package to ensure that we can use the macros | ||
temp_pkg_dir() do project_dir | ||
add_this_pkg() | ||
copy_test_package(project_dir, "UsesPreferences") | ||
Pkg.develop(path=joinpath(project_dir, "UsesPreferences")) | ||
Pkg.test("UsesPreferences") | ||
end | ||
end | ||
|
||
end # module PreferencesTests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
name = "UsesPreferences" | ||
uuid = "056c4eb5-4491-6b91-3d28-8fffe3ee2af9" | ||
version = "0.1.0" | ||
|
||
[deps] | ||
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" | ||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" |
Oops, something went wrong.