From e2dd2fa1d146967e0636292070caefa596a0dcbf Mon Sep 17 00:00:00 2001 From: Phillip Alday Date: Wed, 24 Jul 2024 02:21:53 +0000 Subject: [PATCH] IJulia docstrings + tests (#542) * IJulia docstrings * macro docstrings * remove some shadowing * tests for IJulia * get coverage * more coverage * linux only * headless * set up jupyter kernel --- Project.toml | 6 ++- src/RCall.jl | 2 +- src/ijulia.jl | 99 +++++++++++++++++++++++++++++++++--------------- src/macros.jl | 36 ++++++++---------- test/.gitignore | 5 +++ test/ijulia.jl | 40 +++++++++++++++++++ test/ijulia.jmd | 10 +++++ test/runtests.jl | 9 +++++ 8 files changed, 154 insertions(+), 53 deletions(-) create mode 100644 test/.gitignore create mode 100644 test/ijulia.jl create mode 100644 test/ijulia.jmd diff --git a/Project.toml b/Project.toml index 95a7fafd..ef5309ca 100644 --- a/Project.toml +++ b/Project.toml @@ -23,10 +23,12 @@ CategoricalArrays = "0.8, 0.9, 0.10" Conda = "1.4" DataFrames = "0.21, 0.22, 1.0" DataStructures = "0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18" +IJulia = "1.25" Logging = "0, 1" Preferences = "1" Requires = "0.5.2, 1" StatsModels = "0.6, 0.7" +Weave = "0.10" WinReg = "0.2, 0.3, 1" julia = "1.6" @@ -34,11 +36,13 @@ julia = "1.6" AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Weave = "44d3d7a6-8a23-5bf8-98c5-b353f8df5ec9" [targets] -test = ["Dates", "AxisArrays", "REPL", "Test", "Random", "CondaPkg", "Pkg", "Logging"] +test = ["Dates", "AxisArrays", "REPL", "Test", "Random", "CondaPkg", "Pkg", "Logging", "IJulia", "Weave"] diff --git a/src/RCall.jl b/src/RCall.jl index 4cb9e3d9..d0f45385 100644 --- a/src/RCall.jl +++ b/src/RCall.jl @@ -24,7 +24,7 @@ export RObject, globalEnv, isnull, isna, anyna, robject, rcopy, rparse, rprint, reval, rcall, rlang, - rimport, @rimport, @rlibrary, @rput, @rget, @var_str, @R_str + rimport, @rimport, @rlibrary, @rput, @rget, @R_str # These two preference get marked as compile-time preferences by being accessed # here diff --git a/src/ijulia.jl b/src/ijulia.jl index 1554f234..55bba278 100644 --- a/src/ijulia.jl +++ b/src/ijulia.jl @@ -1,39 +1,64 @@ # IJulia hooks for displaying plots with RCall -# TODO: create a special graphics device. This would allow us to not accidentally close devices opened by users, and display plots immediately as they appear. +# TODO: create a special graphics device. +# This would allow us to not accidentally close devices opened by users, +# and display plots immediately as they appear. -ijulia_mime = nothing +const IJULIA_MIME = Ref{Union{Nothing,MIME}}(nothing) +const IJULIA_FILE_DIR = Ref{String}("") """ + ijulia_setdevice(m::MIME; kwargs...) + ijulia_setdevice(m::MIME"image/png"; width=6*72, height=5*72) + ijulia_setdevice(m::MIME"image/svg+xml"; width=6, height=5) + Set options for R plotting with IJulia. The first argument should be a MIME object: currently supported are * `MIME("image/png")` [default] * `MIME("image/svg+xml")` -The remaining arguments (keyword only) are passed to the appropriate R graphics +The keyword arguments are forwarded to the appropriate R graphics device: see the relevant R help for details. """ -function ijulia_setdevice(m::MIME;kwargs...) - global ijulia_mime - rcall_p(:options, rcalljl_device=rdevicename(m)) - rcall_p(:options, rcalljl_options=Dict(kwargs)) - ijulia_mime = m - nothing +function ijulia_setdevice(m::MIME; kwargs...) + rcall_p(:options; rcalljl_device=rdevicename(m)) + rcall_p(:options; rcalljl_options=Dict(kwargs)) + IJULIA_MIME[] = m + return nothing end -ijulia_setdevice(m::MIME"image/png") = ijulia_setdevice(m, width=6*72, height=5*72) -ijulia_setdevice(m::MIME"image/svg+xml") = ijulia_setdevice(m, width=6, height=5) +ijulia_setdevice(m::MIME"image/png") = ijulia_setdevice(m; width=6*72, height=5*72) +ijulia_setdevice(m::MIME"image/svg+xml") = ijulia_setdevice(m; width=6, height=5) + +""" + rdevicename(::MIME"image/png") + rdevicename(::MIME"image/svg+xml") + +Return the name of the associated R device as a symbol. + +See also [`ijulia_setdevice`](@ref). +""" +rdevicename(::MIME"image/png") = :png +rdevicename(::MIME"image/svg+xml") = :svg +rdevicename(m::MIME) = throw(ArgumentError(string("Unsupported MIME type: ", m))) + +""" + ijulia_displayfile(m::MIME, f) -rdevicename(m::MIME"image/png") = :png -rdevicename(m::MIME"image/svg+xml") = :svg +Display a graphics file in IJulia. +This function generally should not be called by the user, but instead by +the appropriate display hook. +See also [`ijulia_setdevice`](@ref). +""" function ijulia_displayfile(m::MIME"image/png", f) open(f) do f d = read(f) - display(m,d) + display(m, d) end end + function ijulia_displayfile(m::MIME"image/svg+xml", f) # R svg images use named defs, which cause problem when used inline, see # https://github.com/jupyter/notebook/issues/333 @@ -41,46 +66,56 @@ function ijulia_displayfile(m::MIME"image/svg+xml", f) open(f) do f r = randstring() d = read(f, String) - d = replace(d, "id=\"glyph" => "id=\"glyph"*r) - d = replace(d, "href=\"#glyph" => "href=\"#glyph"*r) - display(m,d) + d = replace(d, "id=\"glyph" => "id=\"glyph" * r) + d = replace(d, "href=\"#glyph" => "href=\"#glyph" * r) + display(m, d) end end """ -Called after cell evaluation. + ijulia_displayplots() + Closes graphics device and displays files in notebook. + +This is a postexecution hook called by IJulia after cell evaluation +and should generally not be called by the user. """ function ijulia_displayplots() if rcopy(Int,rcall_p(Symbol("dev.cur"))) != 1 rcall_p(Symbol("dev.off")) - for fn in sort(readdir(ijulia_file_dir)) - ffn = joinpath(ijulia_file_dir,fn) - ijulia_displayfile(ijulia_mime,ffn) + for fn in sort(readdir(IJULIA_FILE_DIR[])) + ffn = joinpath(IJULIA_FILE_DIR[],fn) + ijulia_displayfile(IJULIA_MIME[],ffn) rm(ffn) end end end -# cleanup after error +""" + ijulia_cleanup() + +Clean up R display device and temporary files after error. +""" function ijulia_cleanup() - if rcopy(Int,rcall_p(Symbol("dev.cur"))) != 1 + if rcopy(Int, rcall_p(Symbol("dev.cur"))) != 1 rcall_p(Symbol("dev.off")) end - for fn in readdir(ijulia_file_dir) - ffn = joinpath(ijulia_file_dir,fn) + for fn in readdir(IJULIA_FILE_DIR[]) + ffn = joinpath(IJULIA_FILE_DIR[], fn) rm(ffn) end end +""" + ijulia_init() -ijulia_file_dir = "" - +Initialize RCall's IJulia support. +""" function ijulia_init() - global ijulia_file_dir - ijulia_file_dir = mktempdir() - ijulia_file_fmt = joinpath(ijulia_file_dir,"rij_%03d") - rcall_p(:options,rcalljl_filename=ijulia_file_fmt) + # TODO: use scratchspace? + IJULIA_FILE_DIR[] = mktempdir() + ijulia_file_fmt = joinpath(IJULIA_FILE_DIR[],"rij_%03d") + rcall_p(:options; rcalljl_filename=ijulia_file_fmt) reval_p(rparse_p(""" options(device = function(filename=getOption('rcalljl_filename'), ...) { @@ -89,6 +124,8 @@ function ijulia_init() }) """)) + # TODO: remove the implicit dependency on IJulia + # and be explicit via package extensions Main.IJulia.push_postexecute_hook(ijulia_displayplots) Main.IJulia.push_posterror_hook(ijulia_cleanup) ijulia_setdevice(MIME"image/png"()) diff --git a/src/macros.jl b/src/macros.jl index 7c2bde51..5ecbe514 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -1,16 +1,18 @@ """ -Copies variables from Julia to R using the same name. + @rput(args...) + +Copy variable(s) from Julia to R using the same name. """ macro rput(args...) blk = Expr(:block) for a in args - if isa(a,Symbol) + if isa(a, Symbol) v = a - push!(blk.args,:(Const.GlobalEnv[$(QuoteNode(v))] = $(esc(v)))) - elseif isa(a,Expr) && a.head == :(::) + push!(blk.args, :(Const.GlobalEnv[$(QuoteNode(v))] = $(esc(v)))) + elseif isa(a, Expr) && a.head == :(::) v = a.args[1] S = a.args[2] - push!(blk.args,:(Const.GlobalEnv[$(QuoteNode(v))] = robject($S, $(esc(v))))) + push!(blk.args, :(Const.GlobalEnv[$(QuoteNode(v))] = robject($S, $(esc(v))))) else error("Incorrect usage of @rput") end @@ -19,18 +21,20 @@ macro rput(args...) end """ -Copies variables from R to Julia using the same name. + @rget(args...) + +Copy variable(s) from R to Julia using the same name. """ macro rget(args...) blk = Expr(:block) for a in args - if isa(a,Symbol) + if isa(a, Symbol) v = a - push!(blk.args,:($(esc(v)) = rcopy(Const.GlobalEnv[$(QuoteNode(v))]))) - elseif isa(a,Expr) && a.head == :(::) + push!(blk.args, :($(esc(v)) = rcopy(Const.GlobalEnv[$(QuoteNode(v))]))) + elseif isa(a, Expr) && a.head == :(::) v = a.args[1] T = a.args[2] - push!(blk.args,:($(esc(v)) = rcopy($(esc(T)),Const.GlobalEnv[$(QuoteNode(v))]))) + push!(blk.args, :($(esc(v)) = rcopy($(esc(T)),Const.GlobalEnv[$(QuoteNode(v))]))) else error("Incorrect usage of @rget") end @@ -54,9 +58,8 @@ It is also possible to pass Julia expressions: All such Julia expressions are evaluated once, before the R expression is evaluated. -The expression does not support assigning to Julia variables, so the only way retrieve -values from R via the return value. - +The expression does not support assigning to Julia variables, so the only way to retrieve +values from R is via the return value. """ macro R_str(script) script, symdict = render(script) @@ -72,10 +75,3 @@ macro R_str(script) end end end - -""" -Returns a variable named "str". Useful for passing keyword arguments containing dots. -""" -macro var_str(str) - esc(Symbol(str)) -end diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..f0d719a4 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,5 @@ +ijulia.html +ijulia.ipynb +ijulia-checkpoint*.ipynb +ijulia_files/ +ijulia.md diff --git a/test/ijulia.jl b/test/ijulia.jl new file mode 100644 index 00000000..7624efd5 --- /dev/null +++ b/test/ijulia.jl @@ -0,0 +1,40 @@ +using Conda +using IJulia +using RCall +using Weave +using Test + +# $(abspath(dirname(@__DIR__))) +IJulia.installkernel("julia", "--project=@.") +jupyter_path = joinpath(Conda.BINDIR, "jupyter") +if !isfile(jupyter_path) + Conda.add("jupyter") +end +testpath = Base.Fix1(joinpath, @__DIR__) +Weave.notebook(testpath("ijulia.jmd"); out_path=@__DIR__, jupyter_path=jupyter_path) + +run(`$(jupyter_path) nbconvert $(testpath("ijulia.ipynb")) --to html --embed-images`) +const PNG = """