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

Enable REPL to offer to install missing packages if install hooks are provided #39026

Merged
merged 5 commits into from
Apr 27, 2021
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
37 changes: 37 additions & 0 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ const softscope! = softscope

const repl_ast_transforms = Any[softscope] # defaults for new REPL backends

# Allows an external package to add hooks into the code loading.
# The hook should take a Vector{Symbol} of package names and
# return true if all packages could be installed, false if not
# to e.g. install packages on demand
const install_packages_hooks = Any[]

function eval_user_input(@nospecialize(ast), backend::REPLBackend)
lasterr = nothing
Base.sigatomic_begin()
Expand All @@ -133,6 +139,9 @@ function eval_user_input(@nospecialize(ast), backend::REPLBackend)
put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
else
backend.in_eval = true
if !isempty(install_packages_hooks)
check_for_missing_packages_and_run_hooks(ast)
end
for xf in backend.ast_transforms
ast = Base.invokelatest(xf, ast)
end
Expand All @@ -155,6 +164,34 @@ function eval_user_input(@nospecialize(ast), backend::REPLBackend)
nothing
end

function check_for_missing_packages_and_run_hooks(ast)
mods = modules_to_be_loaded(ast)
filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
if !isempty(mods)
for f in install_packages_hooks
Base.invokelatest(f, mods) && return
end
end
end

function modules_to_be_loaded(ast, mods = Symbol[])
if ast.head in [:using, :import]
for arg in ast.args
if first(arg.args) isa Symbol # i.e. `Foo`
if first(arg.args) != :. # don't include local imports
push!(mods, first(arg.args))
end
else # i.e. `Foo: bar`
push!(mods, first(first(arg.args).args))
end
end
end
for arg in ast.args
arg isa Expr && modules_to_be_loaded(arg, mods)
end
return mods
end

"""
start_repl_backend(repl_channel::Channel, response_channel::Channel)

Expand Down
27 changes: 27 additions & 0 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1292,3 +1292,30 @@ Base.wait(frontend_task)
macro throw_with_linenumbernode(err)
Expr(:block, LineNumberNode(42, Symbol("test.jl")), :(() -> throw($err)))
end

@testset "Install missing packages via hooks" begin
@testset "Parse AST for packages" begin
mods = REPL.modules_to_be_loaded(Meta.parse("using Foo"))
@test mods == [:Foo]
mods = REPL.modules_to_be_loaded(Meta.parse("import Foo"))
@test mods == [:Foo]
mods = REPL.modules_to_be_loaded(Meta.parse("using Foo, Bar"))
@test mods == [:Foo, :Bar]
mods = REPL.modules_to_be_loaded(Meta.parse("import Foo, Bar"))
@test mods == [:Foo, :Bar]

mods = REPL.modules_to_be_loaded(Meta.parse("if false using Foo end"))
@test mods == [:Foo]
mods = REPL.modules_to_be_loaded(Meta.parse("if false if false using Foo end end"))
@test mods == [:Foo]
mods = REPL.modules_to_be_loaded(Meta.parse("if false using Foo, Bar end"))
@test mods == [:Foo, :Bar]
mods = REPL.modules_to_be_loaded(Meta.parse("if false using Foo: bar end"))
@test mods == [:Foo]

mods = REPL.modules_to_be_loaded(Meta.parse("import Foo.bar as baz"))
@test mods == [:Foo]
mods = REPL.modules_to_be_loaded(Meta.parse("using .Foo"))
@test mods == []
end
end