Weave.jl/src/run.jl

512 lines
16 KiB
Julia
Raw Normal View History

2018-07-26 10:35:17 +02:00
using Base64
2017-03-14 20:06:47 +01:00
2020-05-16 12:52:56 +02:00
const PROGRESS_ID = "weave_progress"
"""
2020-05-08 19:31:45 +02:00
run_doc(doc::WeaveDoc; kwargs...)
2020-03-27 10:45:04 +01:00
Run code chunks and capture output from the parsed document.
## Keyword options
- `doctype::Union{Nothing,AbstractString} = nothing`: Output document format. By default (i.e. given `nothing`), Weave will set it automatically based on file extension. You can also manually specify it; see [`list_out_formats()`](@ref) for the supported formats
2020-03-27 10:45:04 +01:00
- `out_path::Union{Symbol,AbstractString} = :doc`: Path where the output is generated can be either of:
* `:doc`: Path of the source document (default)
* `:pwd`: Julia working directory
* `"somepath"`: `String` of output directory e.g. `"~/outdir"`, or of filename e.g. `"~/outdir/outfile.tex"`
- `args::Dict = Dict()`: Arguments to be passed to the weaved document; will be available as `WEAVE_ARGS` in the document
2020-05-09 09:09:11 +02:00
- `mod::Union{Module,Nothing} = nothing`: Module where Weave `eval`s code. You can pass a `Module` object, otherwise create an new sandbox module.
2020-03-27 10:45:04 +01:00
- `fig_path::AbstractString = "figures"`: Where figures will be generated, relative to `out_path`
- `fig_ext::Union{Nothing,AbstractString} = nothing`: Extension for saved figures e.g. `".pdf"`, `".png"`. Default setting depends on `doctype`
- `cache_path::AbstractString = "cache"`: Where of cached output will be saved
- `cache::Symbol = :off`: Controls caching of code:
* `:off` means no caching (default)
* `:all` caches everything
* `:user` caches based on chunk options
* `:refresh` runs all code chunks and save new cache
- `throw_errors::Bool = false`: If `false` errors are included in output document and the whole document is executed. If `true` errors are thrown when they occur
- `latex_keep_unicode::Bool = false`: If `true`, do not convert unicode characters to their respective latex representation. This is especially useful if a font and tex-engine with support for unicode characters are used
!!! note
Run Weave from terminal and try to avoid weaving from IJulia or ESS; they tend to mess with capturing output.
"""
2020-05-08 19:31:45 +02:00
function run_doc(
2020-03-27 10:45:04 +01:00
doc::WeaveDoc;
doctype::Union{Nothing,AbstractString} = nothing,
2020-03-27 10:45:04 +01:00
out_path::Union{Symbol,AbstractString} = :doc,
args::Dict = Dict(),
2020-05-09 09:09:11 +02:00
mod::Union{Module,Nothing} = nothing,
2020-03-27 10:45:04 +01:00
fig_path::AbstractString = "figures",
fig_ext::Union{Nothing,AbstractString} = nothing,
cache_path::AbstractString = "cache",
cache::Symbol = :off,
throw_errors::Bool = false,
2020-05-08 16:39:17 +02:00
latex_keep_unicode::Bool = false,
2020-03-27 10:45:04 +01:00
)
2020-05-08 16:39:17 +02:00
# cache :all, :user, :off, :refresh
2020-05-16 16:10:44 +02:00
doc.doctype = isnothing(doctype) ? (doctype = detect_doctype(doc.source)) : doctype
doc.format = formats[doctype]
if haskey(doc.format.formatdict, :keep_unicode)
doc.format.formatdict[:keep_unicode] = latex_keep_unicode
end
2020-03-18 00:53:25 +01:00
doc.cwd = get_cwd(doc, out_path)
isdir(doc.cwd) || mkpath(doc.cwd)
if (occursin("2pdf", doctype) && cache == :off) || occursin("2html", doctype)
fig_path = mktempdir(abspath(doc.cwd))
2016-12-23 07:34:54 +01:00
end
2020-05-09 09:09:11 +02:00
cache === :off || @eval import Serialization # XXX: evaluate in a more sensible module
2020-05-08 16:39:17 +02:00
# This is needed for latex and should work on all output formats
2020-05-09 16:55:11 +02:00
@static Sys.iswindows() && (fig_path = replace(fig_path, "\\" => "/"))
2016-12-23 07:34:54 +01:00
doc.fig_path = fig_path
set_rc_params(doc, fig_path, fig_ext)
2020-05-08 16:39:17 +02:00
# New sandbox for each document with args exposed
isnothing(mod) && (mod = sandbox = Core.eval(Main, :(module $(gensym(:WeaveSandBox)) end))::Module)
2020-05-09 09:09:11 +02:00
@eval mod WEAVE_ARGS = $args
mimetypes = get(doc.format.formatdict, :mimetypes, default_mime_types)
report = Report(doc.cwd, doc.basename, doc.format.formatdict, mimetypes, throw_errors)
pushdisplay(report)
try
if cache !== :off && cache !== :refresh
cached = read_cache(doc, cache_path)
2020-05-08 20:17:57 +02:00
isnothing(cached) && @info "No cached results found, running code"
else
cached = nothing
end
executed = []
2020-05-16 12:52:56 +02:00
n = length(filter(chunk->isa(chunk,CodeChunk), doc.chunks))
i = 0
for chunk in doc.chunks
2020-05-16 12:52:56 +02:00
if chunk isa CodeChunk
options = merge(doc.chunk_defaults, chunk.options)
merge!(chunk.options, options)
2020-05-16 12:52:56 +02:00
@info "Weaving chunk $(chunk.number) from line $(chunk.start_line)" progress=(i)/n _id=PROGRESS_ID
i+=1
end
2020-05-16 12:52:56 +02:00
restore = (cache === :user && chunk isa CodeChunk && chunk.options[:cache])
result_chunks = if cached nothing && (cache === :all || restore)
restore_chunk(chunk, cached)
else
run_chunk(chunk, doc, report, mod)
end
executed = [executed; result_chunks]
end
2020-05-15 16:51:52 +02:00
replace_header_inline!(doc, report, mod) # evaluate and replace inline code in header
doc.header_script = report.header_script
doc.chunks = executed
cache !== :off && write_cache(doc, cache_path)
2020-05-09 09:09:11 +02:00
@isdefined(sandbox) && clear_module!(sandbox)
catch err
rethrow(err)
finally
2020-05-16 12:52:56 +02:00
@info "Weaved all chunks" progress=1 _id=PROGRESS_ID
popdisplay(report) # ensure display pops out even if internal error occurs
end
return doc
end
2020-05-16 16:10:44 +02:00
run_doc(doc::WeaveDoc, doctype::Union{Nothing,AbstractString}; kwargs...) =
run_doc(doc; doctype = doctype, kwargs...)
"""
2020-05-16 16:10:44 +02:00
detect_doctype(path)
Detect the output format based on file extension.
"""
2020-05-16 16:10:44 +02:00
function detect_doctype(path)
_, ext = lowercase.(splitext(path))
match(r"^\.(jl|.?md|ipynb)", ext) !== nothing && return "md2html"
ext == ".rst" && return "rst"
ext == ".tex" && return "texminted"
2020-05-08 16:39:17 +02:00
ext == ".txt" && return "asciidoc"
return "pandoc"
2016-04-22 15:16:12 +02:00
end
2020-05-16 12:52:56 +02:00
function run_chunk(chunk::CodeChunk, doc, report, mod)
result = eval_chunk(chunk, report, mod)
occursin("2html", report.formatdict[:doctype]) && (embed_figures!(result, report.cwd))
return result
end
function embed_figures!(chunk::CodeChunk, cwd)
for (i, fig) in enumerate(chunk.figures)
chunk.figures[i] = img2base64(fig, cwd)
end
2016-12-23 11:27:10 +01:00
end
function embed_figures!(chunks::Vector{CodeChunk}, cwd)
for chunk in chunks
embed_figures!(chunk, cwd)
end
end
function img2base64(fig, cwd)
2020-05-08 16:39:17 +02:00
ext = splitext(fig)[2]
f = open(joinpath(cwd, fig), "r")
raw = read(f)
2020-05-08 16:39:17 +02:00
close(f)
if ext == ".png"
return "data:image/png;base64," * stringmime(MIME("image/png"), raw)
elseif ext == ".svg"
return "data:image/svg+xml;base64," * stringmime(MIME("image/svg"), raw)
elseif ext == ".gif"
return "data:image/gif;base64," * stringmime(MIME("image/gif"), raw)
else
return (fig)
end
end
2020-05-16 12:52:56 +02:00
function run_chunk(chunk::DocChunk, doc, report, mod)
chunk.content = [run_inline(c, doc, report, mod) for c in chunk.content]
return chunk
end
run_inline(inline::InlineText, doc::WeaveDoc, report::Report, SandBox::Module) = inline
2016-12-26 20:21:55 +01:00
function run_inline(inline::InlineCode, doc::WeaveDoc, report::Report, SandBox::Module)
2020-05-08 16:39:17 +02:00
# Make a temporary CodeChunk for running code. Collect results and don't wrap
2017-03-09 21:09:36 +01:00
chunk = CodeChunk(inline.content, 0, 0, "", Dict(:hold => true, :wrap => false))
options = merge(doc.chunk_defaults, chunk.options)
2016-12-26 20:21:55 +01:00
merge!(chunk.options, options)
2017-03-09 21:09:36 +01:00
2016-12-26 20:28:57 +01:00
chunks = eval_chunk(chunk, report, SandBox)
occursin("2html", report.formatdict[:doctype]) && (embed_figures!(chunks, report.cwd))
2016-12-26 20:28:57 +01:00
2016-12-26 20:21:55 +01:00
output = chunks[1].output
2017-04-02 01:07:15 +02:00
endswith(output, "\n") && (output = output[1:end-1])
2016-12-26 20:21:55 +01:00
inline.output = output
inline.rich_output = chunks[1].rich_output
inline.figures = chunks[1].figures
return inline
end
function reset_report(report::Report)
report.cur_result = ""
2016-04-11 17:40:18 +02:00
report.figures = AbstractString[]
report.term_state = :text
end
function run_code(chunk::CodeChunk, report::Report, SandBox::Module)
expressions = parse_input(chunk.content)
N = length(expressions)
2020-05-08 16:39:17 +02:00
# @show expressions
result_no = 1
2020-05-08 16:39:17 +02:00
results = ChunkOutput[]
2020-05-08 16:39:17 +02:00
for (str_expr, expr) in expressions
reset_report(report)
lastline = (result_no == N)
2020-05-08 16:39:17 +02:00
(obj, out) = capture_output(
expr,
SandBox,
chunk.options[:term],
chunk.options[:display],
lastline,
report.throw_errors,
)
figures = report.figures # Captured figures
result = ChunkOutput(str_expr, out, report.cur_result, report.rich_output, figures)
report.rich_output = ""
push!(results, result)
result_no += 1
end
return results
end
2018-07-23 19:29:07 +02:00
getstdout() = stdout
2016-12-15 18:54:50 +01:00
2020-05-08 16:39:17 +02:00
function capture_output(expr, SandBox::Module, term, disp, lastline, throw_errors = false)
# oldSTDOUT = STDOUT
2016-12-15 18:54:50 +01:00
oldSTDOUT = getstdout()
out = nothing
obj = nothing
rw, wr = redirect_stdout()
2018-07-23 19:29:07 +02:00
reader = @async read(rw, String)
try
2018-07-23 19:29:07 +02:00
obj = Core.eval(SandBox, expr)
2020-05-15 16:51:52 +02:00
!isnothing(obj) && ((term || disp) || lastline) && display(obj)
catch err
throw_errors && throw(err)
display(err)
@warn "ERROR: $(typeof(err)) occurred, including output in Weaved document"
finally
redirect_stdout(oldSTDOUT)
close(wr)
2018-07-23 19:29:07 +02:00
out = fetch(reader)
close(rw)
end
2020-05-08 16:39:17 +02:00
out = replace(out, r"\u001b\[.*?m" => "") # Remove ANSI color codes
return (obj, out)
end
2020-05-08 16:39:17 +02:00
# Parse chunk input to array of expressions
function parse_input(s)
res = []
s = lstrip(s)
n = sizeof(s)
2020-05-08 16:39:17 +02:00
pos = 1 # The first character is extra line end
while (oldpos = pos) n
ex, pos = Meta.parse(s, pos)
push!(res, (s[oldpos:pos-1], ex))
end
return res
end
function eval_chunk(chunk::CodeChunk, report::Report, SandBox::Module)
if !chunk.options[:eval]
chunk.output = ""
chunk.options[:fig] = false
return chunk
end
2020-05-08 16:39:17 +02:00
# Run preexecute_hooks
for hook in preexecute_hooks
2020-05-08 16:39:17 +02:00
chunk = Base.invokelatest(hook, chunk)
end
report.fignum = 1
report.cur_chunk = chunk
2020-05-08 20:17:57 +02:00
if haskey(report.formatdict, :out_width) && isnothing(chunk.options[:out_width])
chunk.options[:out_width] = report.formatdict[:out_width]
end
chunk.result = run_code(chunk, report, SandBox)
2020-05-08 16:39:17 +02:00
# Run post_execute chunks
for hook in postexecute_hooks
2020-05-08 16:39:17 +02:00
chunk = Base.invokelatest(hook, chunk)
end
if chunk.options[:term]
chunks = collect_results(chunk, TermResult())
2016-04-19 15:38:03 +02:00
elseif chunk.options[:hold]
chunks = collect_results(chunk, CollectResult())
else
chunks = collect_results(chunk, ScriptResult())
end
2020-05-08 16:39:17 +02:00
# else
# chunk.options[:fig] && (chunk.figures = copy(report.figures))
# end
return chunks
end
2020-05-09 09:09:11 +02:00
"""
clear_module!(mod::Module)
Recursively sets variables in `mod` to `nothing` so that they're GCed.
!!! warning
`const` variables can't be reassigned, as such they can't be cleared.
"""
function clear_module!(mod::Module)
for name in names(mod; all = true)
name === :eval && continue
try
v = getfield(mod, name)
if v isa Module && v != mod
clear_module!(v)
continue
2020-05-08 16:39:17 +02:00
end
2020-05-09 09:09:11 +02:00
isconst(mod, name) && continue # can't clear constant
Core.eval(mod, :($name = nothing))
catch err
@debug err
end
end
end
function get_figname(report::Report, chunk; fignum = nothing, ext = nothing)
figpath = joinpath(report.cwd, chunk.options[:fig_path])
isdir(figpath) || mkpath(figpath)
2020-05-08 20:17:57 +02:00
isnothing(ext) && (ext = chunk.options[:fig_ext])
isnothing(fignum) && (fignum = report.fignum)
2020-05-08 20:17:57 +02:00
chunkid = isnothing(chunk.options[:label]) ? chunk.number : chunk.options[:label]
2020-05-08 16:39:17 +02:00
full_name = joinpath(
report.cwd,
chunk.options[:fig_path],
"$(report.basename)_$(chunkid)_$(fignum)$ext",
)
rel_name = "$(chunk.options[:fig_path])/$(report.basename)_$(chunkid)_$(fignum)$ext" # Relative path is used in output
return full_name, rel_name
end
function get_cwd(doc::WeaveDoc, out_path)
2020-05-08 16:39:17 +02:00
# Set the output directory
if out_path == :doc
cwd = doc.path
elseif out_path == :pwd
cwd = pwd()
else
2020-05-08 16:39:17 +02:00
# If there is no extension, use as path
2016-04-24 14:02:03 +02:00
splitted = splitext(out_path)
if splitted[2] == ""
cwd = expanduser(out_path)
else
cwd = splitdir(expanduser(out_path))[1]
end
end
return cwd
end
2016-04-24 14:02:03 +02:00
"""Get output file name based on out_path"""
function get_outname(out_path::Symbol, doc::WeaveDoc; ext = nothing)
2020-05-08 20:17:57 +02:00
isnothing(ext) && (ext = doc.format.formatdict[:extension])
2016-04-24 14:02:03 +02:00
outname = "$(doc.cwd)/$(doc.basename).$ext"
end
"""Get output file name based on out_path"""
function get_outname(out_path::AbstractString, doc::WeaveDoc; ext = nothing)
2020-05-08 20:17:57 +02:00
isnothing(ext) && (ext = doc.format.formatdict[:extension])
2016-04-24 14:02:03 +02:00
splitted = splitext(out_path)
if (splitted[2]) == ""
outname = "$(doc.cwd)/$(doc.basename).$ext"
else
outname = expanduser(out_path)
end
end
function set_rc_params(doc::WeaveDoc, fig_path, fig_ext)
formatdict = doc.format.formatdict
2020-05-08 20:17:57 +02:00
if isnothing(fig_ext)
doc.chunk_defaults[:fig_ext] = formatdict[:fig_ext]
else
doc.chunk_defaults[:fig_ext] = fig_ext
end
2020-05-08 16:39:17 +02:00
doc.chunk_defaults[:fig_path] = fig_path
return nothing
end
function collect_results(chunk::CodeChunk, fmt::ScriptResult)
content = ""
result_no = 1
2020-05-08 16:39:17 +02:00
result_chunks = CodeChunk[]
for r in chunk.result
# Check if there is any output from chunk
if strip(r.stdout) == "" && isempty(r.figures) && strip(r.rich_output) == ""
content *= r.code
else
content = "\n" * content * r.code
2020-05-08 16:39:17 +02:00
rchunk = CodeChunk(
content,
chunk.number,
chunk.start_line,
chunk.optionstring,
copy(chunk.options),
)
content = ""
rchunk.result_no = result_no
2020-05-08 16:39:17 +02:00
result_no *= 1
rchunk.figures = r.figures
rchunk.output = r.stdout * r.displayed
rchunk.rich_output = r.rich_output
push!(result_chunks, rchunk)
end
end
if content != ""
startswith(content, "\n") || (content = "\n" * content)
2020-05-08 16:39:17 +02:00
rchunk = CodeChunk(
content,
chunk.number,
chunk.start_line,
chunk.optionstring,
copy(chunk.options),
)
push!(result_chunks, rchunk)
end
return result_chunks
end
function collect_results(chunk::CodeChunk, fmt::TermResult)
output = ""
2016-05-01 00:21:14 +02:00
prompt = chunk.options[:prompt]
result_no = 1
2020-05-08 16:39:17 +02:00
result_chunks = CodeChunk[]
for r in chunk.result
output *= prompt * r.code
2016-05-01 00:21:14 +02:00
output *= r.displayed * r.stdout
if !isempty(r.figures)
2020-05-08 16:39:17 +02:00
rchunk = CodeChunk(
"",
chunk.number,
chunk.start_line,
chunk.optionstring,
copy(chunk.options),
)
rchunk.output = output
output = ""
rchunk.figures = r.figures
push!(result_chunks, rchunk)
end
end
if output != ""
2020-05-08 16:39:17 +02:00
rchunk = CodeChunk(
"",
chunk.number,
chunk.start_line,
chunk.optionstring,
copy(chunk.options),
)
rchunk.output = output
push!(result_chunks, rchunk)
end
return result_chunks
end
function collect_results(chunk::CodeChunk, fmt::CollectResult)
result_no = 1
2020-05-08 16:39:17 +02:00
for r in chunk.result
chunk.output *= r.stdout
chunk.rich_output *= r.rich_output
chunk.figures = [chunk.figures; r.figures]
end
return [chunk]
end
2020-05-15 16:51:52 +02:00
const HEADER_INLINE = Regex("$(HEADER_INLINE_START)(?<code>.+)$(HEADER_INLINE_END)")
replace_header_inline!(doc, report, mod) = _replace_header_inline!(doc, doc.header, report, mod)
function _replace_header_inline!(doc, header, report, mod)
replace!(header) do (k,v)
return k =>
v isa Dict ? _replace_header_inline!(doc, v, report, mod) :
!isa(v, AbstractString) ? v :
2020-05-15 16:51:52 +02:00
replace(v, HEADER_INLINE => s -> begin
m = match(HEADER_INLINE, s)
run_inline_code(m[:code], doc, report, mod)
end)
end
return header
end
function run_inline_code(s, doc, report, mod)
inline = InlineCode(s, 1, 1, 1, :inline)
inline = run_inline(inline, doc, report, mod)
return strip(inline.output, '"')
end