From 28a412688ef792854a10858f4b6d4f36c2766ae6 Mon Sep 17 00:00:00 2001 From: "Tamas K. Papp" Date: Wed, 16 Oct 2024 12:38:06 +0200 Subject: [PATCH] Add new configuration option `continue_on_error`, closes #201. (#257) --- CHANGELOG.md | 8 ++++++++ src/Literate.jl | 27 ++++++++++++++++++++------- test/runtests.jl | 16 ++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8346c8..50802541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased +### Added + - A new keyword argument configuration `continue_on_error::Bool = false` has been added + which controls the behavior of code execution errors. By default (`continue_on_error = + false`) execution errors are re-thrown by Literate (as before). If `continue_on_error = + true` is set the error is used as the block result and execution continues with following + blocks. ([#201], [#257]) + ## [v2.19.1] - 2024-09-13 ### Fixed - Set `:SOURCE_PATH` in the task local storage to the output file when executing code so diff --git a/src/Literate.jl b/src/Literate.jl index 9ce052ee..e8612380 100644 --- a/src/Literate.jl +++ b/src/Literate.jl @@ -346,6 +346,7 @@ function create_configuration(inputfile; user_config, user_kwargs, type=nothing) cfg["softscope"] = type === (:nb) ? true : false # on for Jupyter notebooks cfg["keep_comments"] = false cfg["execute"] = type === :md ? false : true + cfg["continue_on_error"] = false cfg["codefence"] = pick_codefence( get(user_config, "flavor", cfg["flavor"]), get(user_config, "execute", cfg["execute"]), @@ -433,6 +434,10 @@ Available options: output script. Only applicable for `Literate.script`. - `execute` (default: `true` for notebook, `false` for markdown): Whether to execute and capture the output. Only applicable for `Literate.notebook` and `Literate.markdown`. +- `continue_on_error` (default: `false`): Whether to continue code execution of remaining + blocks after encountering an error. By default execution errors are re-thrown. If + `continue_on_error = true` the error will be used as the output of the block instead and + execution will continue. This option is only applicable when `execute = true`. - `codefence` (default: `````"````@example \$(name)" => "````"````` for `DocumenterFlavor()` and `````"````julia" => "````"````` otherwise): Pair containing opening and closing code fence for wrapping code blocks. @@ -621,6 +626,7 @@ function markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg image_formats=config["image_formats"], file_prefix="$(config["name"])-$(chunknum)", softscope=config["softscope"], + continue_on_error=config["continue_on_error"], ) end end @@ -639,9 +645,10 @@ end function execute_markdown!(io::IO, sb::Module, block::String, outputdir; inputfile::String, fake_source::String, flavor::AbstractFlavor, image_formats::Vector, file_prefix::String, - softscope::Bool) + softscope::Bool, continue_on_error::Bool) # TODO: Deal with explicit display(...) calls - r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source, softscope=softscope) + r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source, + softscope=softscope, continue_on_error=continue_on_error) # issue #101: consecutive codefenced blocks need newline # issue #144: quadruple backticks allow for triple backticks in the output plain_fence = "\n````\n" => "\n````" @@ -777,7 +784,9 @@ function jupyter_notebook(chunks, config) cd(config["literate_outputdir"]) do nb = execute_notebook(nb; inputfile=config["literate_inputfile"], fake_source=config["literate_outputfile"], - softscope=config["softscope"]) + softscope=config["softscope"], + continue_on_error=config["continue_on_error"], +) end catch err @error "error when executing notebook based on input file: " * @@ -788,7 +797,8 @@ function jupyter_notebook(chunks, config) return nb end -function execute_notebook(nb; inputfile::String, fake_source::String, softscope::Bool) +function execute_notebook(nb; inputfile::String, fake_source::String, softscope::Bool, + continue_on_error=continue_on_error) sb = sandbox() execution_count = 0 for cell in nb["cells"] @@ -796,7 +806,9 @@ function execute_notebook(nb; inputfile::String, fake_source::String, softscope: execution_count += 1 cell["execution_count"] = execution_count block = join(cell["source"]) - r, str, display_dicts = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source, softscope=softscope) + r, str, display_dicts = execute_block(sb, block; inputfile=inputfile, + fake_source=fake_source, softscope=softscope, + continue_on_error=continue_on_error) # str should go into stream if !isempty(str) @@ -878,7 +890,8 @@ function Base.display(ld::LiterateDisplay, mime::MIME, x) end # Execute a code-block in a module and capture stdout/stderr and the result -function execute_block(sb::Module, block::String; inputfile::String, fake_source::String, softscope::Bool) +function execute_block(sb::Module, block::String; inputfile::String, fake_source::String, + softscope::Bool, continue_on_error::Bool) @debug """execute_block($sb, block) ``` $(block) @@ -904,7 +917,7 @@ function execute_block(sb::Module, block::String; inputfile::String, fake_source end end popdisplay(disp) # IOCapture.capture has a try-catch so should always end up here - if c.error + if c.error && !continue_on_error error(""" $(sprint(showerror, c.value)) when executing the following code block from inputfile `$(Base.contractuser(inputfile))` diff --git a/test/runtests.jl b/test/runtests.jl index 529231d4..971501b8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1459,6 +1459,22 @@ end end end # mktempdir end end +@testset "continue_on_error=true" begin + input_with_error = + """ + # The following will error + + sqrt(-1.0) + """ + mktempdir(@__DIR__) do sandbox + inputfile = joinpath(sandbox, "input.jl") + write(inputfile, input_with_error) + Literate.markdown(inputfile, sandbox; continue_on_error = true, execute = true) + output_md = read(joinpath(sandbox, "input.md"), String) + @test occursin("DomainError(-1.0", output_md) + end +end + @testset "Configuration" begin; Base.CoreLogging.with_logger(Base.CoreLogging.NullLogger()) do mktempdir(@__DIR__) do sandbox cd(sandbox) do