|
1 | 1 | module Preferences
|
2 | 2 |
|
| 3 | +using TOML |
| 4 | +using Base: UUID, TOMLCache |
| 5 | + |
| 6 | +export load_preference, @load_preference, |
| 7 | + set_preferences!, @set_preferences! |
| 8 | + |
| 9 | +include("utils.jl") |
| 10 | + |
| 11 | +""" |
| 12 | + load_preference(uuid_or_module, key, default = nothing) |
| 13 | +
|
| 14 | +Load a particular preference from the `Preferences.toml` file, shallowly merging keys |
| 15 | +as it walks the hierarchy of load paths, loading preferences from all environments that |
| 16 | +list the given UUID as a direct dependency. |
| 17 | +
|
| 18 | +Most users should use the `@load_preference` convenience macro which auto-determines the |
| 19 | +calling `Module`. |
| 20 | +""" |
| 21 | +function load_preference(uuid::UUID, key::String, default = nothing) |
| 22 | + # Re-use definition in `base/loading.jl` so as to not repeat code. |
| 23 | + d = Base.get_preferences(uuid) |
| 24 | + if currently_compiling() |
| 25 | + Base.record_compiletime_preference(uuid, key) |
| 26 | + end |
| 27 | + # Drop any nested `__clear__` keys: |
| 28 | + function drop_clears(data::Dict) |
| 29 | + delete!(data, "__clear__") |
| 30 | + for (k, v) in data |
| 31 | + if isa(v, Dict) |
| 32 | + drop_clears(v) |
| 33 | + end |
| 34 | + end |
| 35 | + return data |
| 36 | + end |
| 37 | + drop_clears(x) = x |
| 38 | + |
| 39 | + return drop_clears(get(d, key, default)) |
| 40 | +end |
| 41 | +function load_preference(m::Module, key::String, default = nothing) |
| 42 | + return load_preference(get_uuid(m), key, default) |
| 43 | +end |
| 44 | + |
3 | 45 | """
|
4 |
| - CompileTime |
| 46 | + @load_preference(key) |
5 | 47 |
|
6 |
| -This module provides bindings for setting/getting preferences that can be used at compile |
7 |
| -time and will cause your `.ji` file to be invalidated when they are changed. |
| 48 | +Convenience macro to call `load_preference()` for the current package. |
8 | 49 | """
|
9 |
| -module CompileTime |
10 |
| -const PREFS_KEY = "compile-preferences" |
11 |
| -include("common.jl") |
12 |
| -end # module CompileTime |
| 50 | +macro load_preference(key, default = nothing) |
| 51 | + return quote |
| 52 | + load_preference($(esc(get_uuid(__module__))), $(esc(key)), $(esc(default))) |
| 53 | + end |
| 54 | +end |
13 | 55 |
|
14 |
| -# Export `CompileTime` but don't `using` it |
15 |
| -export CompileTime |
16 | 56 |
|
17 |
| -# Second copy of code for non-compiletime preferences |
18 |
| -const PREFS_KEY = "preferences" |
19 |
| -include("common.jl") |
| 57 | +""" |
| 58 | + process_sentinel_values!(prefs::Dict) |
| 59 | +
|
| 60 | +Recursively search for preference values that end in `nothing` or `missing` leaves, |
| 61 | +which we handle specially, see the `set_preferences!()` docstring for more detail. |
| 62 | +""" |
| 63 | +function process_sentinel_values!(prefs::Dict) |
| 64 | + # Need to widen `prefs` so that when we try to assign to `__clear__` below, |
| 65 | + # we don't error due to a too-narrow type on `prefs` |
| 66 | + prefs = Base._typeddict(prefs, Dict{String,Vector{String}}()) |
| 67 | + |
| 68 | + clear_keys = get(prefs, "__clear__", String[]) |
| 69 | + for k in collect(keys(prefs)) |
| 70 | + if prefs[k] === nothing |
| 71 | + # If this should add `k` to the `__clear__` list, do so, then remove `k` |
| 72 | + push!(clear_keys, k) |
| 73 | + delete!(prefs, k) |
| 74 | + elseif prefs[k] === missing |
| 75 | + # If this should clear out the mapping for `k`, do so, and drop it from `clear_keys` |
| 76 | + delete!(prefs, k) |
| 77 | + filter!(x -> x != k, clear_keys) |
| 78 | + elseif isa(prefs[k], Dict) |
| 79 | + # Recurse for nested dictionaries |
| 80 | + prefs[k] = process_sentinel_values!(prefs[k]) |
| 81 | + end |
| 82 | + end |
| 83 | + # Store the updated list of clear_keys |
| 84 | + if !isempty(clear_keys) |
| 85 | + prefs["__clear__"] = collect(Set(clear_keys)) |
| 86 | + else |
| 87 | + delete!(prefs, "__clear__") |
| 88 | + end |
| 89 | + return prefs |
| 90 | +end |
| 91 | + |
| 92 | +# See the `set_preferences!()` docstring below for more details |
| 93 | +function set_preferences!(target_toml::String, pkg_name::String, pairs::Pair{String,<:Any}...; force::Bool = false) |
| 94 | + # Load the old preferences in first, as we'll merge ours into whatever currently exists |
| 95 | + d = Dict{String,Any}() |
| 96 | + if isfile(target_toml) |
| 97 | + d = Base.parsed_toml(target_toml) |
| 98 | + end |
| 99 | + prefs = d |
| 100 | + if endswith(target_toml, "Project.toml") |
| 101 | + if !haskey(prefs, "preferences") |
| 102 | + prefs["preferences"] = Dict{String,Any}() |
| 103 | + end |
| 104 | + # If this is a `(Julia)Project.toml` file, we squirrel everything away under the |
| 105 | + # "preferences" key, while for a `Preferences.toml` file it sits at top-level. |
| 106 | + prefs = prefs["preferences"] |
| 107 | + end |
| 108 | + # Index into our package name |
| 109 | + if !haskey(prefs, pkg_name) |
| 110 | + prefs[pkg_name] = Dict{String,Any}() |
| 111 | + end |
| 112 | + # Set each preference, erroring unless `force` is set to `true` |
| 113 | + for (k, v) in pairs |
| 114 | + if !force && haskey(prefs[pkg_name], k) && (v === missing || prefs[pkg_name][k] != v) |
| 115 | + throw(ArgumentError("Cannot set preference '$(k)' to '$(v)' for $(pkg_name) in $(target_toml): preference already set to '$(prefs[pkg_name][k])'!")) |
| 116 | + end |
| 117 | + prefs[pkg_name][k] = v |
| 118 | + |
| 119 | + # Recursively scan for `nothing` and `missing` values that we need to handle specially |
| 120 | + prefs[pkg_name] = process_sentinel_values!(prefs[pkg_name]) |
| 121 | + end |
| 122 | + open(target_toml, "w") do io |
| 123 | + TOML.print(io, d, sorted=true) |
| 124 | + end |
| 125 | + return nothing |
| 126 | +end |
| 127 | + |
| 128 | +""" |
| 129 | + set_preferences!(uuid_or_module, prefs::Pair{String,Any}...; export_prefs=false, force=false) |
| 130 | +
|
| 131 | +Sets a series of preferences for the given UUID/Module, identified by the pairs passed in |
| 132 | +as `prefs`. Preferences are loaded from `Project.toml` and `LocalPreferences.toml` files |
| 133 | +on the load path, merging values together into a cohesive view, with preferences taking |
| 134 | +precedence in `LOAD_PATH` order, just as package resolution does. Preferences stored in |
| 135 | +`Project.toml` files are considered "exported", as they are easily shared across package |
| 136 | +installs, whereas the `LocalPreferences.toml` file is meant to represent local |
| 137 | +preferences that are not typically shared. `LocalPreferences.toml` settings override |
| 138 | +`Project.toml` settings where appropriate. |
| 139 | +
|
| 140 | +After running `set_preferences!(uuid, "key" => value)`, a future invocation of |
| 141 | +`load_preference(uuid, "key")` will generally result in `value`, with the exception of |
| 142 | +the merging performed by `load_preference()` due to inheritance of preferences from |
| 143 | +elements higher up in the `load_path()`. To control this inheritance, there are two |
| 144 | +special values that can be passed to `set_preferences!()`: `nothing` and `missing`. |
| 145 | +
|
| 146 | +* Passing `missing` as the value causes all mappings of the associated key to be removed |
| 147 | + from the current level of `LocalPreferences.toml` settings, allowing preferences set |
| 148 | + higher in the chain of preferences to pass through. Use this value when you want to |
| 149 | + clear your settings but still inherit any higher settings for this key. |
| 150 | +
|
| 151 | +* Passing `nothing` as the value causes all mappings of the associated key to be removed |
| 152 | + from the current level of `LocalPreferences.toml` settings and blocks preferences set |
| 153 | + higher in the chain of preferences from passing through. Internally, this adds the |
| 154 | + preference key to a `__clear__` list in the `LocalPreferences.toml` file, that will |
| 155 | + prevent any preferences from leaking through from higher environments. |
| 156 | +
|
| 157 | +Note that the behavior of `missing` and `nothing` is both similar (they both clear the |
| 158 | +current settings) and diametrically opposed (one allows inheritance of preferences, the |
| 159 | +other does not). They can also be composed with a normal `set_preferences!()` call: |
| 160 | +
|
| 161 | +```julia |
| 162 | +@set_preferences!("compiler_options" => nothing) |
| 163 | +@set_preferences!("compiler_options" => Dict("CXXFLAGS" => "-g", LDFLAGS => "-ljulia")) |
| 164 | +``` |
| 165 | +
|
| 166 | +The above snippet first clears the `"compiler_options"` key of any inheriting influence, |
| 167 | +then sets a preference option, which guarantees that future loading of that preference |
| 168 | +will be exactly what was saved here. If we wanted to re-enable inheritance from higher |
| 169 | +up in the chain, we could do the same but passing `missing` first. |
| 170 | +
|
| 171 | +The `export_prefs` option determines whether the preferences being set should be stored |
| 172 | +within `LocalPreferences.toml` or `Project.toml`. |
| 173 | +""" |
| 174 | +function set_preferences!(u::UUID, prefs::Pair{String,<:Any}...; export_prefs=false, kwargs...) |
| 175 | + # Find the first `Project.toml` that has this UUID as a direct dependency |
| 176 | + project_toml, pkg_name = find_first_project_with_uuid(u) |
| 177 | + if project_toml === nothing && pkg_name === nothing |
| 178 | + # If we couldn't find one, we're going to use `active_project()` |
| 179 | + project_toml = Base.active_project() |
| 180 | + |
| 181 | + # And we're going to need to add this UUID as an "extras" dependency: |
| 182 | + # We're going to assume you want to name this this dependency in the |
| 183 | + # same way as it's been loaded: |
| 184 | + pkg_uuid_matches = filter(d -> d.uuid == u, keys(Base.loaded_modules)) |
| 185 | + if isempty(pkg_uuid_matches) |
| 186 | + error("Cannot set preferences of an unknown package that is not loaded!") |
| 187 | + end |
| 188 | + pkg_name = first(pkg_uuid_matches).name |
| 189 | + |
| 190 | + # Read in the project, add the deps, write it back out! |
| 191 | + project = Base.parsed_toml(project_toml) |
| 192 | + if !haskey(project, "extras") |
| 193 | + project["extras"] = Dict{String,Any}() |
| 194 | + end |
| 195 | + project["extras"][pkg_name] = string(u) |
| 196 | + open(project_toml, "w") do io |
| 197 | + TOML.print(io, project; sorted=true) |
| 198 | + end |
| 199 | + end |
| 200 | + |
| 201 | + # Finally, save the preferences out to either `Project.toml` or `Preferences.toml` |
| 202 | + # keyed under that `pkg_name`: |
| 203 | + target_toml = project_toml |
| 204 | + if !export_prefs |
| 205 | + target_toml = joinpath(dirname(project_toml), "LocalPreferences.toml") |
| 206 | + end |
| 207 | + return set_preferences!(target_toml, pkg_name, prefs...; kwargs...) |
| 208 | +end |
| 209 | +function set_preferences!(m::Module, prefs::Pair{String,<:Any}...; kwargs...) |
| 210 | + return set_preferences!(get_uuid(m), prefs...; kwargs...) |
| 211 | +end |
| 212 | + |
| 213 | +""" |
| 214 | + @set_preferences!(prefs...) |
| 215 | +
|
| 216 | +Convenience macro to call `set_preferences!()` for the current package. Defaults to |
| 217 | +setting `force=true`, since a package should have full control over itself, but not |
| 218 | +so for setting the preferences in other packages, pending private dependencies. |
| 219 | +""" |
| 220 | +macro set_preferences!(prefs...) |
| 221 | + return quote |
| 222 | + set_preferences!($(esc(get_uuid(__module__))), $(esc(prefs...)), force=true) |
| 223 | + end |
| 224 | +end |
20 | 225 |
|
21 | 226 | end # module Preferences
|
0 commit comments