Skip to content

Commit

Permalink
Capture output from display(x) and display(mime, x)
Browse files Browse the repository at this point in the history
This implement a simple LiterateDisplay and pushes that
to the display stack when executing code to capture manual
calls to display(x) and display(mime, x), fixes #128.
  • Loading branch information
fredrikekre committed Jan 19, 2021
1 parent 5db8c22 commit 9562f1d
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 14 deletions.
1 change: 1 addition & 0 deletions src/IJulia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function display_dict(x)
elseif showable(image_jpeg, x) # don't send jpeg if we have png
data[string(image_jpeg)] = limitstringmime(image_jpeg, x)
end
# TODO: IJulia logic has changed? Seems to send both html and latex for DataFrame for example
if showable(text_markdown, x)
data[string(text_markdown)] = limitstringmime(text_markdown, x)
elseif showable(text_html, x)
Expand Down
65 changes: 52 additions & 13 deletions src/Literate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,8 @@ function markdown(inputfile, outputdir=pwd(); config::Dict=Dict(), kwargs...)
end

function execute_markdown!(io::IO, sb::Module, block::String, outputdir)
r, str = execute_block(sb, block)
# TODO: Deal with explicit display(...) calls
r, str, _ = execute_block(sb, block)
plain_fence = "\n```\n" => "\n```" # issue #101: consecutive codefenced blocks need newline
if r !== nothing && !REPL.ends_with_semicolon(block)
for (mime, ext) in [(MIME("image/png"), ".png"), (MIME("image/jpeg"), ".jpeg")]
Expand Down Expand Up @@ -621,7 +622,7 @@ function execute_notebook(nb)
execution_count += 1
cell["execution_count"] = execution_count
block = join(cell["source"])
r, str = execute_block(sb, block)
r, str, display_dicts = execute_block(sb, block)

# str should go into stream
if !isempty(str)
Expand All @@ -632,6 +633,27 @@ function execute_notebook(nb)
push!(cell["outputs"], stream)
end

# Some mimes need to be split into vectors of lines instead of a single string
# TODO: Seems like text/plain and text/latex are also split now, but not doing
# it seems to work fine. Leave for now.
function split_mime(dict)
for mime in ("image/svg+xml", "text/html")
if haskey(dict, mime)
dict[mime] = collect(Any, eachline(IOBuffer(dict[mime]), keep = true))
end
end
return dict
end

# Any explicit calls to display(...)
for dict in display_dicts
display_data = Dict{String,Any}()
display_data["output_type"] = "display_data"
display_data["metadata"] = Dict()
display_data["data"] = split_mime(dict)
push!(cell["outputs"], display_data)
end

# check if ; is used to suppress output
r = REPL.ends_with_semicolon(block) ? nothing : r

Expand All @@ -641,18 +663,10 @@ function execute_notebook(nb)
execute_result["output_type"] = "execute_result"
execute_result["metadata"] = Dict()
execute_result["execution_count"] = execution_count
dd = Base.invokelatest(IJulia.display_dict, r)
# we need to split some mime types into vectors of lines instead of a single string
for mime in ("image/svg+xml", "text/html")
if haskey(dd, mime)
dd[mime] = collect(Any, eachline(IOBuffer(dd[mime]), keep = true))
end
end
execute_result["data"] = dd

dict = Base.invokelatest(IJulia.display_dict, r)
execute_result["data"] = split_mime(dict)
push!(cell["outputs"], execute_result)
end

end
return nb
end
Expand All @@ -668,15 +682,40 @@ function sandbox()
return m
end

# Capture display for notebooks
struct LiterateDisplay <: AbstractDisplay
data::Vector
LiterateDisplay() = new([])
end
function Base.display(ld::LiterateDisplay, x)
push!(ld.data, Base.invokelatest(IJulia.display_dict, x))
return nothing
end
# TODO: Problematic to accept mime::MIME here?
function Base.display(ld::LiterateDisplay, mime::MIME, x)
r = Base.invokelatest(IJulia.limitstringmime, mime, x)
display_dicts = Dict{String,Any}(string(mime) => r)
# TODO: IJulia does this part below for unknown mimes
# if istextmime(mime)
# display_dicts["text/plain"] = r
# end
push!(ld.data, display_dicts)
return nothing
end

# Execute a code-block in a module and capture stdout/stderr and the result
function execute_block(sb::Module, block::String)
# Push a capturing display on the displaystack
disp = LiterateDisplay()
pushdisplay(disp)
# r is the result
# status = (true|false)
# _: backtrace
# str combined stdout, stderr output
r, status, _, str = Documenter.withoutput() do
include_string(sb, block)
end
popdisplay(disp) # Documenter.withoutput has a try-catch so should always end up here
if !status
error("""
$(sprint(showerror, r))
Expand All @@ -687,7 +726,7 @@ function execute_block(sb::Module, block::String)
```
""")
end
return r, str
return r, str, disp.data
end

end # module
38 changes: 37 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Literate
import Literate, JSON
import Literate: Chunk, MDChunk, CodeChunk
using Test

Expand Down Expand Up @@ -1110,6 +1110,42 @@ end end
end
write(inputfile, script)
Literate.notebook(inputfile, outdir)

# Calls to display(x) and display(mime, x)
script = """
struct DF x end
Base.show(io::IO, ::MIME"text/plain", df::DF) = print(io, "DF(\$(df.x)) as text/plain")
Base.show(io::IO, ::MIME"text/html", df::DF) = print(io, "DF(\$(df.x)) as text/html")
Base.show(io::IO, ::MIME"text/latex", df::DF) = print(io, "DF(\$(df.x)) as text/latex")
#-
foreach(display, [DF(1), DF(2)])
DF(3)
#-
display(MIME("text/latex"), DF(4))
"""
write(inputfile, script)
Literate.notebook(inputfile, outdir)
json = JSON.parsefile(joinpath(outdir, "inputfile.ipynb"))
cells = json["cells"]
@test length(cells) == 4
# Cell 2 has 3 outputs: 2 display and one execute result
cellout = cells[2]["outputs"]
@test length(cellout) == 3
for i in 1:3
exe_res = i == 3
@test cellout[i]["output_type"] == (exe_res ? "execute_result" : "display_data")
@test keys(cellout[i]["data"]) == Set(("text/plain", "text/html"))
@test cellout[i]["data"]["text/plain"] == "DF($i) as text/plain"
@test cellout[i]["data"]["text/html"] == Any["DF($i) as text/html"]
@test haskey(cellout[i], "execution_count") == exe_res
end
# Cell 3 has one output from a single display call
cellout = cells[3]["outputs"]
@test length(cellout) == 1
@test cellout[1]["output_type"] == "display_data"
@test keys(cellout[1]["data"]) == Set(("text/latex",))
@test cellout[1]["data"]["text/latex"] == "DF(4) as text/latex"
@test !haskey(cellout[1], "execution_count")
end
end
end end
Expand Down

0 comments on commit 9562f1d

Please sign in to comment.