Skip to content

Commit 5e2343f

Browse files
committed
Implement Pkg.Preferences
Preferences provides a simple package configuration store; packages can store arbitrary configurations into `Dict` objects that get serialized into their active `Project.toml`. Depot-wide preferences can also be stored within the `prefs` folder of a Julia depot, allowing for default values to be passed down to new environments from the system admin.
1 parent 6acd87a commit 5e2343f

File tree

9 files changed

+479
-12
lines changed

9 files changed

+479
-12
lines changed

docs/src/api.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,18 @@ Pkg.Scratch.scratch_dir
103103
Pkg.Scratch.scratch_path
104104
Pkg.Scratch.track_scratch_access
105105
```
106+
## [Preferences API Reference](@id Preferences-Reference)
107+
108+
!!! compat "Julia 1.6"
109+
Pkg's preferences API requires at least Julia 1.6.
110+
111+
```@docs
112+
Pkg.Preferences.load_preferences
113+
Pkg.Preferences.@load_preferences
114+
Pkg.Preferences.save_preferences!
115+
Pkg.Preferences.@save_preferences!
116+
Pkg.Preferences.modify_preferences!
117+
Pkg.Preferences.@modify_preferences!
118+
Pkg.Preferences.clear_preferences!
119+
Pkg.Preferences.@clear_preferences!
120+
```

docs/src/preferences.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# [**8.** Preferences](@id Preferences)
2+
3+
!!! compat "Julia 1.6"
4+
Pkg's preferences API requires at least Julia 1.6.
5+
6+
`Pkg` Preferences support embedding a simple `Dict` of metadata for a package on a per-project or per-depot basis. These preferences allow for packages to set simple, persistent pieces of data that the user has selected, that can persist across multiple versions of a package.
7+
8+
## API Overview
9+
10+
Usage is performed primarily through the `@load_preferences`, `@save_preferences` and `@modify_preferences` macros. These macros will auto-detect the UUID of the calling package, (throwing an error if the calling module does not belong to a package) the function forms can be used to load, save or modify preferences belonging to another package.
11+
12+
Example usage:
13+
14+
```julia
15+
using Pkg.Preferences
16+
17+
function get_preferred_backend()
18+
prefs = @load_preferences()
19+
return get(prefs, "backend", "native")
20+
end
21+
22+
function set_backend(new_backend)
23+
@modify_preferences!() do prefs
24+
prefs["backend"] = new_backend
25+
end
26+
end
27+
```
28+
29+
By default, preferences are stored within the `Project.toml` file of the currently-active project, and as such all new projects will start from a blank state, with all preferences being un-set.
30+
Package authors that wish to have a default value set for their preferences should use the `get(prefs, key, default)` pattern as shown in the code example above.
31+
If a system administrator wishes to provide a default value for new environments on a machine, they may create a depot-wide default value by saving preferences for a particular UUID targeting a particular depot:
32+
33+
```julia
34+
using Pkg.Preferences, Foo
35+
# We want Foo to default to a certain library on this machine,
36+
# save that as a depot-wide preference to our `~/.julia` depot
37+
foo_uuid = Preferences.get_uuid_throw(Foo)
38+
prefs = Dict("libfoo_vendor" => "setec_astronomy")
39+
40+
save_preferences(pkg_uuid, prefs; depot=Pkg.depots1())
41+
```
42+
43+
Depot-wide preferences are overridden by preferences stored wtihin `Project.toml` files, and all preferences (including those inherited from depot-wide preferences) are stored concretely within `Project.toml` files.
44+
This means that depot-wide preferences will serve to provide default values for new projects/environments, but once a project has
45+
saved its preferences at all, they are effectively decoupled.
46+
This is an intentional design choice to maximize reproducibility and to continue to support the `Project.toml` as an independent archive.
47+
48+
For a full listing of docstrings and methods, see the [Preferences Reference](@ref) section.

src/Pkg.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ include("Operations.jl")
5454
include("API.jl")
5555
include("Registry.jl")
5656
include("REPLMode/REPLMode.jl")
57+
include("Preferences.jl")
5758

5859
import .REPLMode: @pkg_str
5960
import .Types: UPLEVEL_MAJOR, UPLEVEL_MINOR, UPLEVEL_PATCH, UPLEVEL_FIXED

src/Preferences.jl

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
module Preferences
2+
import ...Pkg, ..TOML
3+
import ..API: get_uuid
4+
import ..Types: parse_toml
5+
import ..Scratch: get_scratch!, delete_scratch!
6+
import Base: UUID
7+
8+
export load_preferences, @load_preferences,
9+
save_preferences!, @save_preferences!,
10+
modify_preferences!, @modify_preferences!,
11+
clear_preferences!, @clear_preferences!
12+
13+
14+
"""
15+
depot_preferences_paths(uuid::UUID)
16+
17+
Return the possible paths of all preferences file for the given package `UUID` saved in
18+
depot-wide `prefs` locations.
19+
"""
20+
function depot_preferences_paths(uuid::UUID)
21+
depots = reverse(Pkg.depots())
22+
return [joinpath(depot, "prefs", string(uuid, ".toml")) for depot in depots]
23+
end
24+
25+
"""
26+
get_uuid_throw(m::Module)
27+
28+
Convert a `Module` to a `UUID`, throwing an `ArgumentError` if the given module does not
29+
correspond to a loaded package. This is expected for modules such as `Base`, `Main`,
30+
anonymous modules, etc...
31+
"""
32+
function get_uuid_throw(m::Module)
33+
uuid = get_uuid(m)
34+
if uuid === nothing
35+
throw(ArgumentError("Module does not correspond to a loaded package!"))
36+
end
37+
return uuid
38+
end
39+
40+
"""
41+
recursive_merge(base::Dict, overrides::Dict...)
42+
43+
Helper function to merge preference dicts recursively, honoring overrides in nested
44+
dictionaries properly.
45+
"""
46+
function recursive_merge(base::Dict, overrides::Dict...)
47+
new_base = Base._typeddict(base, overrides...)
48+
for override in overrides
49+
for (k, v) in override
50+
if haskey(new_base, k) && isa(new_base[k], Dict) && isa(override[k], Dict)
51+
new_base[k] = recursive_merge(new_base[k], override[k])
52+
else
53+
new_base[k] = override[k]
54+
end
55+
end
56+
end
57+
return new_base
58+
end
59+
60+
"""
61+
load_preferences(uuid::UUID)
62+
load_preferences(m::Module)
63+
64+
Load the preferences for the given package, returning them as a `Dict`. Most users
65+
should use the `@load_preferences()` macro which auto-determines the calling `Module`.
66+
"""
67+
function load_preferences(uuid::UUID)
68+
# First, load from depots, merging as we go:
69+
prefs = Dict{String,Any}()
70+
for path in depot_preferences_paths(uuid)
71+
if isfile(path)
72+
prefs = recursive_merge(prefs, parse_toml(path))
73+
end
74+
end
75+
76+
# Finally, load from the currently-active project:
77+
proj_path = Base.active_project()
78+
if isfile(proj_path)
79+
project = parse_toml(proj_path)
80+
if haskey(project, "preferences") && isa(project["preferences"], Dict)
81+
proj_prefs = get(project["preferences"], string(uuid), Dict())
82+
prefs = recursive_merge(prefs, proj_prefs)
83+
end
84+
end
85+
return prefs
86+
end
87+
load_preferences(m::Module) = load_preferences(get_uuid_throw(m))
88+
89+
"""
90+
save_preferences!(uuid::UUID, prefs::Dict; depot::Union{String,Nothing} = nothing)
91+
save_preferences!(m::Module, prefs::Dict; depot::Union{String,Nothing} = nothing)
92+
93+
Save the preferences for the given package. Most users should use the
94+
`@save_preferences!()` macro which auto-determines the calling `Module`. See also the
95+
`modify_preferences!()` function (and the associated `@modifiy_preferences!()` macro) for
96+
easy load/modify/save workflows.
97+
98+
The `depot` keyword argument allows saving of depot-wide preferences, as opposed to the
99+
default of project-specific preferences. Simply set the `depot` keyword argument to the
100+
path of a depot (use `Pkg.depots1()` for the default depot) and the preferences will be
101+
saved to that location.
102+
103+
Depot-wide preferences are overridden by preferences stored wtihin `Project.toml` files,
104+
and all preferences (including those inherited from depot-wide preferences) are stored
105+
concretely within `Project.toml` files. This means that depot-wide preferences will
106+
serve to provide default values for new projects/environments, but once a project has
107+
saved its preferences at all, they are effectively decoupled. This is an intentional
108+
design choice to maximize reproducibility and to continue to support the `Project.toml`
109+
as an independent archive.
110+
"""
111+
function save_preferences!(uuid::UUID, prefs::Dict;
112+
depot::Union{AbstractString,Nothing} = nothing)
113+
if depot === nothing
114+
# Save to Project.toml
115+
proj_path = Base.active_project()
116+
mkpath(dirname(proj_path))
117+
project = Dict{String,Any}()
118+
if isfile(proj_path)
119+
project = parse_toml(proj_path)
120+
end
121+
if !haskey(project, "preferences")
122+
project["preferences"] = Dict{String,Any}()
123+
end
124+
if !isa(project["preferences"], Dict)
125+
error("$(proj_path) has conflicting `preferences` entry type: Not a Dict!")
126+
end
127+
project["preferences"][string(uuid)] = prefs
128+
open(proj_path, "w") do io
129+
TOML.print(io, project, sorted=true)
130+
end
131+
else
132+
path = joinpath(depot, "prefs", string(uuid, ".toml"))
133+
mkpath(dirname(path))
134+
open(path, "w") do io
135+
TOML.print(io, prefs, sorted=true)
136+
end
137+
end
138+
return nothing
139+
end
140+
function save_preferences!(m::Module, prefs::Dict;
141+
depot::Union{AbstractString,Nothing} = nothing)
142+
return save_preferences!(get_uuid_throw(m), prefs; depot=depot)
143+
end
144+
145+
"""
146+
modify_preferences!(f::Function, uuid::UUID)
147+
modify_preferences!(f::Function, m::Module)
148+
149+
Supports `do`-block modification of preferences. Loads the preferences, passes them to a
150+
user function, then writes the modified `Dict` back to the preferences file. Example:
151+
152+
```julia
153+
modify_preferences!(@__MODULE__) do prefs
154+
prefs["key"] = "value"
155+
end
156+
```
157+
158+
This function returns the full preferences object. Most users should use the
159+
`@modify_preferences!()` macro which auto-determines the calling `Module`.
160+
161+
Note that this method does not support modifying depot-wide preferences; modifications
162+
always are saved to the active project.
163+
"""
164+
function modify_preferences!(f::Function, uuid::UUID)
165+
prefs = load_preferences(uuid)
166+
f(prefs)
167+
save_preferences!(uuid, prefs)
168+
return prefs
169+
end
170+
modify_preferences!(f::Function, m::Module) = modify_preferences!(f, get_uuid_throw(m))
171+
172+
"""
173+
clear_preferences!(uuid::UUID)
174+
clear_preferences!(m::Module)
175+
176+
Convenience method to remove all preferences for the given package. Most users should
177+
use the `@clear_preferences!()` macro, which auto-determines the calling `Module`. This
178+
method clears not only project-specific preferences, but also depot-wide preferences, if
179+
the current user has the permissions to do so.
180+
"""
181+
function clear_preferences!(uuid::UUID)
182+
for path in depot_preferences_paths(uuid)
183+
try
184+
rm(path; force=true)
185+
catch
186+
@warn("Unable to remove preference path $(path)")
187+
end
188+
end
189+
190+
# Clear the project preferences key, if it exists
191+
proj_path = Base.active_project()
192+
if isfile(proj_path)
193+
project = parse_toml(proj_path)
194+
if haskey(project, "preferences") && isa(project["preferences"], Dict)
195+
delete!(project["preferences"], string(uuid))
196+
open(proj_path, "w") do io
197+
TOML.print(io, project, sorted=true)
198+
end
199+
end
200+
end
201+
end
202+
203+
"""
204+
@load_preferences()
205+
206+
Convenience macro to call `load_preferences()` for the current package.
207+
"""
208+
macro load_preferences()
209+
return quote
210+
load_preferences($(esc(get_uuid_throw(__module__))))
211+
end
212+
end
213+
214+
"""
215+
@save_preferences!(prefs)
216+
217+
Convenience macro to call `save_preferences!()` for the current package. Note that
218+
saving to a depot path is not supported in this macro, use `save_preferences!()` if you
219+
wish to do that.
220+
"""
221+
macro save_preferences!(prefs)
222+
return quote
223+
save_preferences!($(esc(get_uuid_throw(__module__))), $(esc(prefs)))
224+
end
225+
end
226+
227+
"""
228+
@modify_preferences!(func)
229+
230+
Convenience macro to call `modify_preferences!()` for the current package.
231+
"""
232+
macro modify_preferences!(func)
233+
return quote
234+
modify_preferences!($(esc(func)), $(esc(get_uuid_throw(__module__))))
235+
end
236+
end
237+
238+
"""
239+
@clear_preferences!()
240+
241+
Convenience macro to call `clear_preferences!()` for the current package.
242+
"""
243+
macro clear_preferences!()
244+
return quote
245+
preferences!($(esc(get_uuid_throw(__module__))))
246+
end
247+
end
248+
249+
end # module Preferences

0 commit comments

Comments
 (0)