Skip to content

Commit

Permalink
IJulia docstrings + tests (#542)
Browse files Browse the repository at this point in the history
* IJulia docstrings

* macro docstrings

* remove some shadowing

* tests for IJulia

* get coverage

* more coverage

* linux only

* headless

* set up jupyter kernel
  • Loading branch information
palday authored Jul 24, 2024
1 parent 12fdd5b commit e2dd2fa
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 53 deletions.
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,26 @@ 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"

[extras]
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"]
2 changes: 1 addition & 1 deletion src/RCall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 68 additions & 31 deletions src/ijulia.jl
Original file line number Diff line number Diff line change
@@ -1,86 +1,121 @@
# 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
# we get around this by renaming the elements.
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'), ...) {
Expand All @@ -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"())
Expand Down
36 changes: 16 additions & 20 deletions src/macros.jl
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
5 changes: 5 additions & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ijulia.html
ijulia.ipynb
ijulia-checkpoint*.ipynb
ijulia_files/
ijulia.md
40 changes: 40 additions & 0 deletions test/ijulia.jl
Original file line number Diff line number Diff line change
@@ -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 = """<img alt="No description has been provided for this image" class="" src="data:image/png;base64"""
const SVG = """<img alt="No description has been provided for this image" src="data:image/svg+xml;base64"""
html = read(testpath("ijulia.html"), String)

# these are the tests to show that things actually work
@test occursin(PNG, html)
@test occursin(SVG, html)

@test_throws ArgumentError("Unsupported MIME type: lulz") RCall.rdevicename(MIME("lulz"))

# create a folder ijulia_files with the exported images -- could be useful if we ever set up percy
# run(`$(jupyter_path) nbconvert $(testpath("ijulia.ipynb")) --to markdown`)

# these are the tests to get code coverage
# nothing like a little piracy when testing in a headless setup
RCall.ijulia_init()
R"plot(1:10, 1:10)"
# throws a method error when running headless !
@test_throws(MethodError, RCall.ijulia_displayplots())
RCall.ijulia_setdevice(MIME("image/svg+xml"))
R"plot(-1 * 1:10, -1 * 1:10)"
# throws a method error when running headless !
@test_throws(MethodError, RCall.ijulia_displayplots())
RCall.ijulia_cleanup()
10 changes: 10 additions & 0 deletions test/ijulia.jmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
```julia
using RCall
RCall.ijulia_init()
R"plot(1:10, 1:10)"
```

```julia
RCall.ijulia_setdevice(MIME("image/svg+xml"))
R"plot(-1 * 1:10, -1 * 1:10)"
```
9 changes: 9 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ println(R"l10n_info()")
end
end

if Sys.islinux()
# the IJulia tests depend on the R graphics device being set up correctly,
# which is non trivial on non-linux headless devices (e.g. CI)
# it also uses the assumed path to Jupyter on unix
@testset "IJulia" begin
include("ijulia.jl")
end
end

@info "" RCall.conda_provided_r

# make sure we're back where we started
Expand Down

0 comments on commit e2dd2fa

Please sign in to comment.