Skip to content

Commit

Permalink
Implement Pkg.Preferences
Browse files Browse the repository at this point in the history
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
staticfloat committed May 22, 2020
1 parent 6679131 commit a1e817b
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Pkg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ include("Operations.jl")
include("API.jl")
include("Registry.jl")
include("REPLMode/REPLMode.jl")
include("Preferences.jl")

import .REPLMode: @pkg_str
import .Types: UPLEVEL_MAJOR, UPLEVEL_MINOR, UPLEVEL_PATCH, UPLEVEL_FIXED
Expand Down
152 changes: 152 additions & 0 deletions src/Preferences.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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!


"""
preferences_path(uuid::UUID)
Return the path of the preferences file for the given package `UUID`.
"""
function preferences_path(uuid::UUID)
return joinpath(Pkg.depots1(), "prefs", string(uuid, ".toml"))
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

"""
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)
path = preferences_path(uuid)
if !isfile(path)
return Dict{String,Any}()
end
return parse_toml(path)
end
load_preferences(m::Module) = load_preferences(get_uuid_throw(m))

"""
save_preferences!(uuid::UUID, prefs::Dict)
save_preferences!(m::Module, prefs::Dict)
Save the preferences for the given package. Most users should use the
`@load_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.
"""
function save_preferences!(uuid::UUID, prefs::Dict)
path = preferences_path(uuid)
mkpath(dirname(path))
open(path, "w") do io
TOML.print(io, prefs, sorted=true)
end
return nothing
end
function save_preferences!(m::Module, prefs::Dict)
return save_preferences!(get_uuid_throw(m), prefs)
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`.
"""
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`.
"""
function clear_preferences!(uuid::UUID)
rm(preferences_path(uuid); force=true)
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.
"""
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
44 changes: 44 additions & 0 deletions test/preferences.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module PreferencesTests
import ..Pkg
using ..Utils, ..Pkg.TOML
using Test, Pkg.Preferences

@testset "Preferences" begin

temp_pkg_dir() do project_dir
# Test creation of preferences within this temporary depot
uuid = Base.UUID(UInt128(0))
toml_path = Pkg.Preferences.preferences_path(uuid)

@test isempty(load_preferences(uuid))
@test !isfile(toml_path)

# Now, save something
save_preferences!(uuid, Dict("foo" => "bar"))
@show toml_path
@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 prefs == load_preferences(uuid)
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")

up_uuid = Base.UUID("056c4eb5-4491-6b91-3d28-8fffe3ee2af9")
prefs = load_preferences(up_uuid)
@test haskey(prefs, "backend")
@test prefs["backend"] == "jlFPGA"
end
end

end # module PreferencesTests
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ include("binaryplatforms.jl")
include("platformengines.jl")
include("sandbox.jl")
include("resolve.jl")
include("preferences.jl")

# clean up locally cached registry
rm(joinpath(@__DIR__, "registries"); force = true, recursive = true)
Expand Down
7 changes: 7 additions & 0 deletions test/test_packages/UsesPreferences/Project.toml
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"
33 changes: 33 additions & 0 deletions test/test_packages/UsesPreferences/src/UsesPreferences.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module UsesPreferences
using Pkg.Preferences

# This will get initialized in __init__()
backend = Ref{String}()

function set_backend(new_backend::AbstractString)
if !(new_backend in ("OpenCL", "CUDA", "jlFPGA"))
throw(ArgumentError("Invalid backend: \"$(new_backend)\""))
end

# Set it in our runtime values, as well as saving it to disk
backend[] = new_backend
@modify_preferences!() do prefs
prefs["backend"] = new_backend
end
end

function get_backend()
return backend[]
end

function __init__()
@modify_preferences!() do prefs
prefs["initialized"] = "true"

# If it's never been set before, default it to OpenCL
prefs["backend"] = get(prefs, "backend", "OpenCL")
backend[] = prefs["backend"]
end
end

end # module UsesPreferences
32 changes: 32 additions & 0 deletions test/test_packages/UsesPreferences/test/runtests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using UsesPreferences, Test, Pkg, Pkg.Preferences

# Get the UUID for UsesPreferences
up_uuid = Pkg.API.get_uuid(UsesPreferences)

@show Pkg.Preferences.preferences_path(up_uuid)

prefs = load_preferences(up_uuid)
@test haskey(prefs, "backend")
@test prefs["backend"] == "OpenCL"
@test UsesPreferences.get_backend() == "OpenCL"

UsesPreferences.set_backend("CUDA")
prefs = load_preferences(up_uuid)
@test haskey(prefs, "backend")
@test prefs["backend"] == "CUDA"
@test UsesPreferences.get_backend() == "CUDA"

# sorry, AMD
@test_throws ArgumentError UsesPreferences.set_backend("ROCm")
prefs = load_preferences(up_uuid)
@test haskey(prefs, "backend")
@test prefs["backend"] == "CUDA"
@test UsesPreferences.get_backend() == "CUDA"

clear_preferences!(up_uuid)
prefs = load_preferences(up_uuid)
@test !haskey(prefs, "backend")
@test UsesPreferences.get_backend() == "CUDA"

# And finally, save something back so that the parent process can read it:
UsesPreferences.set_backend("jlFPGA")

0 comments on commit a1e817b

Please sign in to comment.