Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer setting preferences only within the currently active project #37

Merged
merged 2 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Preferences"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
authors = ["Elliot Saba <elliot.saba@juliacomputing.com>", "contributors"]
version = "1.2.5"
version = "1.3.0"
Copy link
Member

@DilumAluthge DilumAluthge Apr 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
version = "1.3.0"
version = "2.0.0-DEV"

Unless you know you don't plan on making any other breaking changes anytime soon, in which case you could just set it to 2.0.0 and cut a new release after this PR is merged.


[deps]
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
Expand Down
72 changes: 55 additions & 17 deletions src/Preferences.jl
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ function set_preferences!(target_toml::String, pkg_name::String, pairs::Pair{Str
end

"""
set_preferences!(uuid_or_module, prefs::Pair{String,Any}...; export_prefs=false, force=false)
set_preferences!(uuid_or_module, prefs::Pair{String,Any}...; export_prefs=false,
active_project_only=true, force=false)

Sets a series of preferences for the given UUID/Module, identified by the pairs passed in
as `prefs`. Preferences are loaded from `Project.toml` and `LocalPreferences.toml` files
Expand Down Expand Up @@ -176,7 +177,7 @@ special values that can be passed to `set_preferences!()`: `nothing` and `missin
preference key to a `__clear__` list in the `LocalPreferences.toml` file, that will
prevent any preferences from leaking through from higher environments.

Note that the behavior of `missing` and `nothing` is both similar (they both clear the
Note that the behaviors of `missing` and `nothing` are both similar (they both clear the
current settings) and diametrically opposed (one allows inheritance of preferences, the
other does not). They can also be composed with a normal `set_preferences!()` call:

Expand All @@ -192,24 +193,28 @@ up in the chain, we could do the same but passing `missing` first.

The `export_prefs` option determines whether the preferences being set should be stored
within `LocalPreferences.toml` or `Project.toml`.
"""
function set_preferences!(u::UUID, prefs::Pair{String,<:Any}...; export_prefs=false, kwargs...)
# Find the first `Project.toml` that has this UUID as a direct dependency
project_toml, pkg_name = find_first_project_with_uuid(u)
if project_toml === nothing && pkg_name === nothing
# If we couldn't find one, we're going to use `active_project()`
project_toml = Base.active_project()

# And we're going to need to add this UUID as an "extras" dependency:
# We're going to assume you want to name this this dependency in the
# same way as it's been loaded:
pkg_uuid_matches = filter(d -> d.uuid == u, keys(Base.loaded_modules))
if isempty(pkg_uuid_matches)
error("Cannot set preferences of an unknown package that is not loaded!")
The `active_project_only` flag ensures that the preference is set within the currently
active project (as determined by `Base.active_project()`), and if the target package is
not listed as a dependency, it is added under the `extras` section. Without this flag
set, if the target package is not found in the active project, `set_preferences!()` will
search up the load path for an environment that does contain that module, setting the
preference in the first one it finds. If none are found, it falls back to setting the
preference in the active project and adding it as an extra dependency.
"""
function set_preferences!(u::UUID, prefs::Pair{String,<:Any}...; export_prefs=false,
active_project_only::Bool=true, kwargs...)
# If we try to add preferences for a dependency, we need to make sure
# it is listed as a dependency, so if it's not, we'll add it in the
# "extras" section in the `Project.toml`.
function ensure_dep_added(project_toml, uuid, pkg_name)
# If this project already has a mapping for this UUID, early-exit
if Base.get_uuid_name(project_toml, uuid) !== nothing
return
end
pkg_name = first(pkg_uuid_matches).name

# Read in the project, add the deps, write it back out!
# Otherwise, insert it into `extras`, creating the section if
# it doesn't already exist.
project = Base.parsed_toml(project_toml)
if !haskey(project, "extras")
project["extras"] = Dict{String,Any}()
Expand All @@ -218,8 +223,41 @@ function set_preferences!(u::UUID, prefs::Pair{String,<:Any}...; export_prefs=fa
open(project_toml, "w") do io
TOML.print(io, project; sorted=true)
end
return project_toml, pkg_name
end

# Get the pkg name from the current environment if we can't find a
# mapping for it in any environment block. This assumes that the name
# mapping should be the same as what was used in when it was loaded.
function get_pkg_name_from_env()
pkg_uuid_matches = filter(d -> d.uuid == u, keys(Base.loaded_modules))
if isempty(pkg_uuid_matches)
return nothing
end
return first(pkg_uuid_matches).name
end


if active_project_only
project_toml = Base.active_project()
else
project_toml, pkg_name = find_first_project_with_uuid(u)
if project_toml === nothing && pkg_name === nothing
project_toml = Base.active_project()
end
end
pkg_name = something(
Base.get_uuid_name(project_toml, u),
get_pkg_name_from_env(),
Some(nothing),
)
# This only occurs if we couldn't find any hint of the given pkg
if pkg_name === nothing
error("Cannot set preferences of an unknown package that is not loaded!")
end

ensure_dep_added(project_toml, u, pkg_name)

# Finally, save the preferences out to either `Project.toml` or
# `(Julia)LocalPreferences.toml` keyed under that `pkg_name`:
target_toml = project_toml
Expand Down
37 changes: 35 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ up_path = joinpath(@__DIR__, "UsesPreferences")
end
end

# Load UsesPreferences, as we need it loaded for some set/get trickery below
# Load UsesPreferences, as we need it loaded to satisfy `set_preferences!()` below,
# otherwise it can't properly map from a UUID to a name when installing into a package
# that doesn't have UsesPreferences added yet.
activate(up_path) do
eval(:(using UsesPreferences))
end
Expand Down Expand Up @@ -194,7 +196,8 @@ end
@test load_preference(up_uuid, "location") == "outer_local"
end

# Ensure that we can load the preferences the same even if we exit the `activate()`
# Ensure that we can load the preferences even if we exit the `activate()`
# because `env_dir` is a part of `LOAD_PATH`.
@test load_preference(up_uuid, "location") == "outer_local"

# Next, we're going to create a lower environment, add some preferences there, and ensure
Expand Down Expand Up @@ -271,6 +274,36 @@ end
@test nested["nested2"]["b"] == 2
@test nested["leaf"] == "world"
end

# Test that setting a preference for UsesPreferences in a project that does
# not contain UsesPreferences adds the dependency if `active_project_only`
# is set, which is the default:
mktempdir() do empty_project_dir
touch(joinpath(empty_project_dir, "Project.toml"))
activate(empty_project_dir) do
# This will search up the environment stack for a project that contains
# the UsesPreferences UUID and insert the preference there.
set_preferences!(up_uuid, "location" => "overridden_outer_local"; active_project_only=false, force=true)
prefs = Base.parsed_toml(joinpath(env_dir, "LocalPreferences.toml"))
@test prefs["UsesPreferences"]["location"] == "overridden_outer_local"

# This will set it in the currently active project, and add `UsesPreferences`
# as a dependency under the `"extras"` section
set_preferences!(up_uuid, "location" => "empty_inner_local"; active_project_only=true)
prefs = Base.parsed_toml(joinpath(empty_project_dir, "LocalPreferences.toml"))
@test prefs["UsesPreferences"]["location"] == "empty_inner_local"
proj = Base.parsed_toml(joinpath(empty_project_dir, "Project.toml"))
@test haskey(proj, "extras")
@test haskey(proj["extras"], "UsesPreferences")
@test proj["extras"]["UsesPreferences"] == string(up_uuid)

# Now that UsesPreferences has been added to the empty project, this will
# set the preference in the local project since it is found in there first.
set_preferences!(up_uuid, "location" => "still_empty_inner_local"; active_project_only=false, force=true)
prefs = Base.parsed_toml(joinpath(empty_project_dir, "LocalPreferences.toml"))
@test prefs["UsesPreferences"]["location"] == "still_empty_inner_local"
end
end
end
end
end
Expand Down