From 81877fe73ed3534ebfbf5c63c78e0c917d9ea556 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Wed, 8 Nov 2023 02:30:51 +0100 Subject: [PATCH] Add soft scoping rule capabilities This patch enables "soft" scoping rules (see e.g. https://github.com/JuliaLang/SoftGlobalScope.jl) for code execution (markdown and notebook output). This is enabled by default for Jupyter notebook output (to mimic how the IJulia kernel works), and disabled otherwise. Soft scope rules can be enabled/disabled with the `softscope :: Bool` configuration variable. Fixes #227. --- CHANGELOG.md | 7 +++++++ src/Literate.jl | 24 ++++++++++++++++------- test/runtests.jl | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3261c3..680b47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ 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 +- "Soft" scoping rules (see e.g. https://github.com/JuliaLang/SoftGlobalScope.jl) are now + available for code execution (markdown and notebook output). This is enabled by default + for Jupyter notebook output (to mimic how the IJulia kernel works), and disabled + otherwise. Soft scope rules can be enabled/disabled with the `softscope :: Bool` + configuration variable. ([#227][github-227], [#230][github-230]) ### Changed - The minimum Julia version requirement for Literate >= 2.16.0 is now 1.6.0 (from 1.0.0). ([#230][github-230]) @@ -269,6 +275,7 @@ https://discourse.julialang.org/t/ann-literate-jl/10651 for release announcement [github-221]: https://github.com/fredrikekre/Literate.jl/pull/221 [github-222]: https://github.com/fredrikekre/Literate.jl/issues/222 [github-223]: https://github.com/fredrikekre/Literate.jl/pull/223 +[github-227]: https://github.com/fredrikekre/Literate.jl/issues/227 [github-228]: https://github.com/fredrikekre/Literate.jl/issues/228 [github-229]: https://github.com/fredrikekre/Literate.jl/pull/229 [github-230]: https://github.com/fredrikekre/Literate.jl/pull/230 diff --git a/src/Literate.jl b/src/Literate.jl index f12440c..071fa17 100644 --- a/src/Literate.jl +++ b/src/Literate.jl @@ -312,6 +312,7 @@ function create_configuration(inputfile; user_config, user_kwargs, type=nothing) cfg["flavor"] = type === (:md) ? DocumenterFlavor() : DefaultFlavor() cfg["credit"] = true cfg["mdstrings"] = false + cfg["softscope"] = type === (:nb) ? true : false # on for Jupyter notebooks cfg["keep_comments"] = false cfg["execute"] = type === :md ? false : true cfg["codefence"] = get(user_config, "flavor", cfg["flavor"]) isa DocumenterFlavor && @@ -408,6 +409,8 @@ Available options: - `devurl` (default: `"dev"`): URL for "in-development" docs, see [Documenter docs] (https://juliadocs.github.io/Documenter.jl/). Unused if `repo_root_url`/ `nbviewer_root_url`/`binder_root_url` are set. +- `softscope` (default: `true` for Jupyter notebooks, `false` otherwise): enable/disable + "soft" scoping rules when executing, see e.g. https://github.com/JuliaLang/SoftGlobalScope.jl. - `repo_root_url`: URL to the root of the repository. Determined automatically on Travis CI, GitHub Actions and GitLab CI. Used for `@__REPO_ROOT_URL__`. - `nbviewer_root_url`: URL to the root of the repository as seen on nbviewer. Determined @@ -580,6 +583,7 @@ function markdown(inputfile, outputdir=pwd(); config::AbstractDict=Dict(), kwarg flavor=config["flavor"], image_formats=config["image_formats"], file_prefix="$(config["name"])-$(chunknum)", + softscope=config["softscope"], ) end end @@ -597,9 +601,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) + flavor::AbstractFlavor, image_formats::Vector, file_prefix::String, + softscope::Bool) # TODO: Deal with explicit display(...) calls - r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source) + r, str, _ = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source, softscope=softscope) # issue #101: consecutive codefenced blocks need newline # issue #144: quadruple backticks allow for triple backticks in the output plain_fence = "\n````\n" => "\n````" @@ -734,7 +739,8 @@ function jupyter_notebook(chunks, config) try cd(config["literate_outputdir"]) do nb = execute_notebook(nb; inputfile=config["literate_inputfile"], - fake_source=config["literate_outputfile"]) + fake_source=config["literate_outputfile"], + softscope=config["softscope"]) end catch err @error "error when executing notebook based on input file: " * @@ -745,7 +751,7 @@ function jupyter_notebook(chunks, config) return nb end -function execute_notebook(nb; inputfile::String, fake_source::String) +function execute_notebook(nb; inputfile::String, fake_source::String, softscope::Bool) sb = sandbox() execution_count = 0 for cell in nb["cells"] @@ -753,7 +759,7 @@ function execute_notebook(nb; inputfile::String, fake_source::String) 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) + r, str, display_dicts = execute_block(sb, block; inputfile=inputfile, fake_source=fake_source, softscope=softscope) # str should go into stream if !isempty(str) @@ -835,7 +841,7 @@ 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) +function execute_block(sb::Module, block::String; inputfile::String, fake_source::String, softscope::Bool) @debug """execute_block($sb, block) ``` $(block) @@ -851,7 +857,11 @@ function execute_block(sb::Module, block::String; inputfile::String, fake_source # `rethrow = Union{}` means that we try-catch all the exceptions thrown in the do-block # and return them via the return value (they get handled below). c = IOCapture.capture(rethrow = Union{}) do - include_string(sb, block, fake_source) + if softscope + include_string(REPL.softscope, sb, block, fake_source) + else + include_string(sb, block, fake_source) + end end popdisplay(disp) # IOCapture.capture has a try-catch so should always end up here if c.error diff --git a/test/runtests.jl b/test/runtests.jl index d56f304..41a61e2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -905,6 +905,27 @@ end end Literate.markdown(inputfile, relpath(outdir); execute=true, flavor=Literate.CommonMarkFlavor()) @test read(joinpath(outdir, "inputfile-1.svg"), String) == "issue228" + + # Softscope + write( + inputfile, + """ + ret = 0 + for k = 1:10 + ret += k + end + println("ret = ", ret) + """ + ) + Literate.markdown(inputfile, outdir; execute=true, softscope=true) + @test occursin("ret = 55", read(joinpath(outdir, "inputfile.md"), String)) + ## Disabled softscope + try + Literate.markdown(inputfile, outdir; execute=true, softscope=false) + error("unreachable") + catch err + @test occursin(r"`?ret`? not defined", sprint(Base.showerror, err)) + end end # cd(sandbox) end # mktemp end end @@ -1319,8 +1340,33 @@ end end @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 + + # Softscope + # TODO: Windows CI says here, but no longer: The input file that have been used + # above multiple times without problem now gives + # "SystemError: opening file: Invalid argument" for some reason... + new_inputfile = "inputfile2.jl" + write( + new_inputfile, + """ + ret = 0 + for k = 1:10 + ret += k + end + println("ret = ", ret) + """ + ) + Literate.notebook(new_inputfile, outdir) + @test occursin("ret = 55", read(joinpath(outdir, "inputfile2.ipynb"), String)) + ## Disabled softscope + try + Literate.notebook(new_inputfile, outdir; softscope=false) + error("unreachable") + catch err + @test occursin(r"`?ret`? not defined", sprint(Base.showerror, err)) + end + end # cd(sandbox) + end # mktempdir end end @testset "Configuration" begin; Base.CoreLogging.with_logger(Base.CoreLogging.NullLogger()) do