From 8405a93db51cc4fb9aee822d1ed144e923ec25ee Mon Sep 17 00:00:00 2001 From: Elliot Saba Date: Tue, 19 Apr 2022 21:42:53 +0000 Subject: [PATCH 1/2] Prefer setting preferences only within the currently active project Previous to this change, we would search up the environment stack looking for the project that contains the target package, and set the preference in there. This was intended to make it convenient to e.g. set a `Revise` preference while having an application project activated. It turns out that the more common problem is actually that a package wants to set a preference of a sub-package (e.g. `MPI` and `MPIPreferences`) that may not be a top-level dependent of the currently active project. Therefore, we change the behavior of `set_preferences!()` to prefer adding the target package as an `extras` dependency of the currently-active project, but maintain the old behavior behind a keyword argument to `set_preferences!()`. --- src/Preferences.jl | 72 +++++++++++++++++++++++++++++++++++----------- test/runtests.jl | 37 ++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/Preferences.jl b/src/Preferences.jl index f175e11..eff8941 100644 --- a/src/Preferences.jl +++ b/src/Preferences.jl @@ -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 @@ -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: @@ -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}() @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 8216ae6..d219d67 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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 @@ -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 @@ -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 From cdca17293abcc7f8d91cdea1f2c5b637fda6af34 Mon Sep 17 00:00:00 2001 From: Elliot Saba Date: Tue, 19 Apr 2022 21:46:54 +0000 Subject: [PATCH 2/2] Bump Preferences to v1.3.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 73996f0..5b32a12 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Preferences" uuid = "21216c6a-2e73-6563-6e65-726566657250" authors = ["Elliot Saba ", "contributors"] -version = "1.2.5" +version = "1.3.0" [deps] TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"