Skip to content

Commit 3357746

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 TOML files and stored within the `prefs` folder of a Julia depot.
1 parent 6679131 commit 3357746

File tree

8 files changed

+409
-11
lines changed

8 files changed

+409
-11
lines changed

docs/src/api.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,19 @@ Pkg.Artifacts.ensure_all_artifacts_installed
8787
Pkg.Artifacts.@artifact_str
8888
Pkg.Artifacts.archive_artifact
8989
```
90+
91+
## [Preferences API Reference](@id Preferences-Reference)
92+
93+
!!! compat "Julia 1.6"
94+
Pkg's preferences API requires at least Julia 1.6.
95+
96+
```@docs
97+
Pkg.Preferences.load_preferences
98+
Pkg.Preferences.@load_preferences
99+
Pkg.Preferences.save_preferences!
100+
Pkg.Preferences.@save_preferences!
101+
Pkg.Preferences.modify_preferences!
102+
Pkg.Preferences.@modify_preferences!
103+
Pkg.Preferences.clear_preferences!
104+
Pkg.Preferences.@clear_preferences!
105+
```

src/Pkg.jl

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

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

src/Preferences.jl

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

test/preferences.jl

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
module PreferencesTests
2+
import ..Pkg
3+
using ..Utils, ..Pkg.TOML
4+
using Test, Pkg.Preferences
5+
6+
@testset "Preferences" begin
7+
# Create a temporary package, store some preferences within it.
8+
with_temp_env() do project_dir
9+
uuid = Base.UUID(UInt128(0))
10+
save_preferences!(uuid, Dict("foo" => "bar"))
11+
12+
project_path = joinpath(project_dir, "Project.toml")
13+
@test isfile(project_path)
14+
proj = Pkg.Types.parse_toml(project_path)
15+
@test haskey(proj, "preferences")
16+
@test isa(proj["preferences"], Dict)
17+
@test haskey(proj["preferences"], string(uuid))
18+
@test isa(proj["preferences"][string(uuid)], Dict)
19+
@test proj["preferences"][string(uuid)]["foo"] == "bar"
20+
21+
prefs = modify_preferences!(uuid) do prefs
22+
prefs["foo"] = "baz"
23+
prefs["spoon"] = [Dict("qux" => "idk")]
24+
end
25+
@test prefs == load_preferences(uuid)
26+
27+
clear_preferences!(uuid)
28+
proj = Pkg.Types.parse_toml(project_path)
29+
@test !haskey(proj, "preferences")
30+
end
31+
32+
temp_pkg_dir() do project_dir
33+
# Test setting of depot-wide preferences
34+
uuid = Base.UUID(UInt128(0))
35+
toml_path = last(Pkg.Preferences.depot_preferences_paths(uuid))
36+
37+
@test isempty(load_preferences(uuid))
38+
@test !isfile(toml_path)
39+
40+
# Now, save something
41+
save_preferences!(uuid, Dict("foo" => "bar"); depot=Pkg.depots1())
42+
@test isfile(toml_path)
43+
prefs = load_preferences(uuid)
44+
@test load_preferences(uuid)["foo"] == "bar"
45+
46+
prefs = modify_preferences!(uuid) do prefs
47+
prefs["foo"] = "baz"
48+
prefs["spoon"] = [Dict("qux" => "idk")]
49+
end
50+
51+
# Test that we get the properly-merged prefs, but that the
52+
# depot-wide file stays the same:
53+
@test prefs == load_preferences(uuid)
54+
toml_prefs = Pkg.Types.parse_toml(toml_path)
55+
@test toml_prefs["foo"] != prefs["foo"]
56+
@test !haskey(toml_prefs, "spoon")
57+
58+
clear_preferences!(uuid)
59+
@test !isfile(toml_path)
60+
end
61+
62+
# Do a test within a package to ensure that we can use the macros
63+
temp_pkg_dir() do project_dir
64+
add_this_pkg()
65+
copy_test_package(project_dir, "UsesPreferences")
66+
Pkg.develop(path=joinpath(project_dir, "UsesPreferences"))
67+
Pkg.test("UsesPreferences")
68+
end
69+
end
70+
71+
end # module PreferencesTests

test/runtests.jl

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ import Pkg
88
rm(joinpath(@__DIR__, "registries"); force = true, recursive = true)
99

1010
include("utils.jl")
11-
include("new.jl")
12-
include("pkg.jl")
13-
include("repl.jl")
14-
include("api.jl")
15-
include("registry.jl")
16-
include("subdir.jl")
17-
include("artifacts.jl")
18-
include("binaryplatforms.jl")
19-
include("platformengines.jl")
20-
include("sandbox.jl")
21-
include("resolve.jl")
11+
# include("new.jl")
12+
# include("pkg.jl")
13+
# include("repl.jl")
14+
# include("api.jl")
15+
# include("registry.jl")
16+
# include("subdir.jl")
17+
# include("artifacts.jl")
18+
# include("binaryplatforms.jl")
19+
# include("platformengines.jl")
20+
# include("sandbox.jl")
21+
# include("resolve.jl")
22+
include("preferences.jl")
2223

2324
# clean up locally cached registry
2425
rm(joinpath(@__DIR__, "registries"); force = true, recursive = true)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name = "UsesPreferences"
2+
uuid = "056c4eb5-4491-6b91-3d28-8fffe3ee2af9"
3+
version = "0.1.0"
4+
5+
[deps]
6+
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
7+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

0 commit comments

Comments
 (0)