From 3e77205540b1019a0a03236ca86c27282a6a6c12 Mon Sep 17 00:00:00 2001 From: Kevin Bonham Date: Thu, 13 Jun 2019 17:27:33 -0400 Subject: [PATCH] Add option to change working directory for evaluation of example code (#1025) --- .gitignore | 1 + CHANGELOG.md | 6 +++++ docs/src/man/syntax.md | 6 +++-- src/Builder.jl | 15 ++++++++++++- src/Documenter.jl | 14 ++++++++++++ src/Documents.jl | 38 ++++++++++++++++++-------------- src/Expanders.jl | 8 +++---- test/runtests.jl | 3 +++ test/utilities.jl | 2 +- test/workdir/make.jl | 19 ++++++++++++++++ test/workdir/src/file.md | 9 ++++++++ test/workdir/src/index.md | 9 ++++++++ test/workdir/src/subdir/file.md | 9 ++++++++ test/workdir/src/subdir/index.md | 9 ++++++++ test/workdir/tests.jl | 36 ++++++++++++++++++++++++++++++ 15 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 test/workdir/make.jl create mode 100644 test/workdir/src/file.md create mode 100644 test/workdir/src/index.md create mode 100644 test/workdir/src/subdir/file.md create mode 100644 test/workdir/src/subdir/index.md create mode 100644 test/workdir/tests.jl diff --git a/.gitignore b/.gitignore index bb383ce631..1b98cd498b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ test/formats/builds/ test/missingdocs/build/ test/nongit/build/ test/errors/build/ +test/workdir/builds/ docs/build/ docs/pdf/build/ docs/site/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 59be18979e..380637c264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ * ![Enhancement][badge-enhancement] Minor changes to how doctesting errors are printed. ([#1028][github-1028]) +* ![Experimental][badge-experimental] ![Feature][badge-feature] The current working directory when evaluating `@repl` and `@example` blocks can now be set to a fixed directory by passing the `workdir` keyword to `makedocs`. _The new keyword and its behaviour are experimental and not part of the public API._ ([#1013][github-1013], [#1025][github-1025]) + ## Version `v0.22.4` * ![Bugfix][badge-bugfix] Documenter no longer crashes if the build includes doctests from docstrings that are defined in files that do not exist on the file system (e.g. if a Julia Base docstring is included when running a non-source Julia build). ([#1002][github-1002]) @@ -327,8 +329,10 @@ [github-1003]: https://github.com/JuliaDocs/Documenter.jl/issues/1003 [github-1004]: https://github.com/JuliaDocs/Documenter.jl/pull/1004 [github-1009]: https://github.com/JuliaDocs/Documenter.jl/pull/1009 +[github-1013]: https://github.com/JuliaDocs/Documenter.jl/issues/1013 [github-1014]: https://github.com/JuliaDocs/Documenter.jl/pull/1014 [github-1015]: https://github.com/JuliaDocs/Documenter.jl/pull/1015 +[github-1025]: https://github.com/JuliaDocs/Documenter.jl/pull/1025 [github-1027]: https://github.com/JuliaDocs/Documenter.jl/issues/1027 [github-1028]: https://github.com/JuliaDocs/Documenter.jl/pull/1028 [github-1029]: https://github.com/JuliaDocs/Documenter.jl/pull/1029 @@ -344,6 +348,7 @@ [badge-enhancement]: https://img.shields.io/badge/enhancement-blue.svg [badge-bugfix]: https://img.shields.io/badge/bugfix-purple.svg [badge-security]: https://img.shields.io/badge/security-black.svg +[badge-experimental]: https://img.shields.io/badge/experimental-lightgrey.svg diff --git a/docs/src/man/syntax.md b/docs/src/man/syntax.md index e8c54b6141..c3d864d162 100644 --- a/docs/src/man/syntax.md +++ b/docs/src/man/syntax.md @@ -334,7 +334,8 @@ on each line is also removed. !!! note The working directory, `pwd`, is set to the directory in `build` where the file - will be written to, and the paths in `include` calls are interpreted to be relative to `pwd`. + will be written to, and the paths in `include` calls are interpreted to be relative to + `pwd`. This can be customized with the `workdir` keyword of [`makedocs`](@ref). **Hiding Source Code** @@ -493,7 +494,8 @@ Named `@repl ` blocks behave in the same way as named `@example ` bl !!! note The working directory, `pwd`, is set to the directory in `build` where the file - will be written to, and the paths in `include` calls are interpreted to be relative to `pwd`. + will be written to, and the paths in `include` calls are interpreted to be relative to + `pwd`. This can be customized with the `workdir` keyword of [`makedocs`](@ref). ## `@setup ` block diff --git a/src/Builder.jl b/src/Builder.jl index 69b63bc576..f85bbe32e1 100644 --- a/src/Builder.jl +++ b/src/Builder.jl @@ -81,6 +81,8 @@ function Selectors.runner(::Type{SetupBuildDirectory}, doc::Documents.Document) # Frequently used fields. build = doc.user.build source = doc.user.source + workdir = doc.user.workdir + # The .user.source directory must exist. isdir(source) || error("source directory '$(abspath(source))' is missing.") @@ -105,9 +107,20 @@ function Selectors.runner(::Type{SetupBuildDirectory}, doc::Documents.Document) for file in files src = normpath(joinpath(root, file)) dst = normpath(joinpath(build, relpath(root, source), file)) + + if workdir == :build + # set working directory to be the same as `build` + wd = normpath(joinpath(build, relpath(root, source))) + elseif workdir isa Symbol + # Maybe allow `:src` and `:root` as well? + throw(ArgumentError("Unrecognized working directory option '$workdir'")) + else + wd = normpath(joinpath(doc.user.root, workdir)) + end + if endswith(file, ".md") push!(mdpages, Utilities.srcpath(source, root, file)) - Documents.addpage!(doc, src, dst) + Documents.addpage!(doc, src, dst, wd) else cp(src, dst; force = true) end diff --git a/src/Documenter.jl b/src/Documenter.jl index 7f47360851..82a22c3862 100644 --- a/src/Documenter.jl +++ b/src/Documenter.jl @@ -195,6 +195,20 @@ ignored. **`strict`** -- [`makedocs`](@ref) fails the build right before rendering if it encountered any errors with the document in the previous build phases. +**`workdir`** determines the working directory where `@example` and `@repl` code blocks are +executed. It can be either a path or the special value `:build` (default). + +If the `workdir` is set to a path, the working directory is reset to that path for each code +block being evaluated. Relative paths are taken to be relative to `root`, but using absolute +paths is recommended (e.g. `workdir = joinpath(@__DIR__, "..")` for executing in the package +root for the usual `docs/make.jl` setup). + +With the default `:build` option, the working directory is set to a subdirectory of `build`, +determined from the source file path. E.g. for `src/foo.md` it is set to `build/`, for +`src/foo/bar.md` it is set to `build/foo` etc. + +Note that `workdir` does not affect doctests. + ## Output formats **`format`** allows the output format to be specified. The default format is [`Documenter.HTML`](@ref) which creates a set of HTML files. diff --git a/src/Documents.jl b/src/Documents.jl index 85a6a3c5dd..dfcde60999 100644 --- a/src/Documents.jl +++ b/src/Documents.jl @@ -35,8 +35,9 @@ Globals() = Globals(Main, Dict()) Represents a single markdown file. """ struct Page - source :: String - build :: String + source :: String + build :: String + workdir :: Union{Symbol,String} """ Ordered list of raw toplevel markdown nodes from the parsed page contents. This vector should be considered immutable. @@ -50,9 +51,9 @@ struct Page mapping :: IdDict{Any,Any} globals :: Globals end -function Page(source::AbstractString, build::AbstractString) +function Page(source::AbstractString, build::AbstractString, workdir::AbstractString) elements = Markdown.parse(read(source, String)).content - Page(source, build, elements, IdDict{Any,Any}(), Globals()) + Page(source, build, workdir, elements, IdDict{Any,Any}(), Globals()) end # Document Nodes. @@ -61,12 +62,12 @@ end ## IndexNode. struct IndexNode - pages :: Vector{String} # Which pages to include in the index? Set by user. - modules :: Vector{Module} # Which modules to include? Set by user. - order :: Vector{Symbol} # What order should docs be listed in? Set by user. - build :: String # Path to the file where this index will appear. - source :: String # Path to the file where this index was written. - elements :: Vector # (object, doc, page, mod, cat)-tuple for constructing links. + pages :: Vector{String} # Which pages to include in the index? Set by user. + modules :: Vector{Module} # Which modules to include? Set by user. + order :: Vector{Symbol} # What order should docs be listed in? Set by user. + build :: String # Path to the file where this index will appear. + source :: String # Path to the file where this index was written. + elements :: Vector # (object, doc, page, mod, cat)-tuple for constructing links. function IndexNode(; # TODO: Fix difference between uppercase and lowercase naming of keys. @@ -85,11 +86,11 @@ end ## ContentsNode. struct ContentsNode - pages :: Vector{String} # Which pages should be included in contents? Set by user. - depth :: Int # Down to which level should headers be displayed? Set by user. - build :: String # Same as for `IndexNode`s. - source :: String # Same as for `IndexNode`s. - elements :: Vector # (order, page, anchor)-tuple for constructing links. + pages :: Vector{String} # Which pages should be included in contents? Set by user. + depth :: Int # Down to which level should headers be displayed? Set by user. + build :: String # Same as for `IndexNode`s. + source :: String # Same as for `IndexNode`s. + elements :: Vector # (order, page, anchor)-tuple for constructing links. function ContentsNode(; Pages = [], @@ -187,6 +188,7 @@ struct User root :: String # An absolute path to the root directory of the document. source :: String # Parent directory is `.root`. Where files are read from. build :: String # Parent directory is also `.root`. Where files are written to. + workdir :: Union{Symbol,String} # Parent directory is also `.root`. Where code is executed from. format :: Vector{Plugin} # What format to render the final document with? clean :: Bool # Empty the `build` directory before starting a new build? doctest :: Union{Bool,Symbol} # Run doctests? @@ -240,6 +242,7 @@ function Document(plugins = nothing; root :: AbstractString = Utilities.currentdir(), source :: AbstractString = "src", build :: AbstractString = "build", + workdir :: Union{Symbol, AbstractString} = :build, format :: Any = Documenter.HTML(), clean :: Bool = true, doctest :: Union{Bool,Symbol} = true, @@ -272,6 +275,7 @@ function Document(plugins = nothing; root, source, build, + workdir, format, clean, doctest, @@ -336,8 +340,8 @@ end ## Methods -function addpage!(doc::Document, src::AbstractString, dst::AbstractString) - page = Page(src, dst) +function addpage!(doc::Document, src::AbstractString, dst::AbstractString, wd::AbstractString) + page = Page(src, dst, wd) # page's identifier is the path relative to the `doc.user.source` directory name = normpath(relpath(src, doc.user.source)) doc.internal.pages[name] = page diff --git a/src/Expanders.jl b/src/Expanders.jl index cce19c18e9..9896a79bee 100644 --- a/src/Expanders.jl +++ b/src/Expanders.jl @@ -491,7 +491,7 @@ end function Selectors.runner(::Type{EvalBlocks}, x, page, doc) sandbox = Module(:EvalBlockSandbox) lines = Utilities.find_block_in_file(x.code, page.source) - cd(dirname(page.build)) do + cd(page.workdir) do result = nothing for (ex, str) in Utilities.parseblock(x.code, doc, page; keywords = false) try @@ -553,7 +553,7 @@ function Selectors.runner(::Type{ExampleBlocks}, x, page, doc) end for (ex, str) in Utilities.parseblock(code, doc, page; keywords = false) (value, success, backtrace, text) = Utilities.withoutput() do - cd(dirname(page.build)) do + cd(page.workdir) do Core.eval(mod, ex) end end @@ -610,7 +610,7 @@ function Selectors.runner(::Type{REPLBlocks}, x, page, doc) buffer = IOBuffer() input = droplines(str) (value, success, backtrace, text) = Utilities.withoutput() do - cd(dirname(page.build)) do + cd(page.workdir) do Core.eval(mod, ex) end end @@ -647,7 +647,7 @@ function Selectors.runner(::Type{SetupBlocks}, x, page, doc) # Evaluate whole @setup block at once instead of piecewise page.mapping[x] = try - cd(dirname(page.build)) do + cd(page.workdir) do include_string(mod, x.code) end Markdown.MD([]) diff --git a/test/runtests.jl b/test/runtests.jl index f94dfcaaa5..5833589a91 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,6 +49,9 @@ println("="^50) # A simple build outside of a Git repository include("nongit/tests.jl") + + # A simple build evaluating code outside build directory + include("workdir/tests.jl") end # Additional tests diff --git a/test/utilities.jl b/test/utilities.jl index 112af129fa..9b4c39ebd5 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -222,7 +222,7 @@ end end import Documenter.Documents: Document, Page, Globals - let page = Page("source", "build", [], IdDict{Any,Any}(), Globals()), doc = Document() + let page = Page("source", "build", :build, [], IdDict{Any,Any}(), Globals()), doc = Document() code = """ x += 3 γγγ_γγγ diff --git a/test/workdir/make.jl b/test/workdir/make.jl new file mode 100644 index 0000000000..8119e2780c --- /dev/null +++ b/test/workdir/make.jl @@ -0,0 +1,19 @@ +using Documenter + +const pages = [ + "Home" => "index.md", + "File" => "file.md", + "Subdir" => "subdir/index.md", + "Subfile" => "subdir/file.md", +] + +@info "Building builds/default" +makedocs(sitename="Test", pages = pages, build="builds/default") + +@info "Building builds/absolute" +mkdir(joinpath(@__DIR__, "builds/absolute-workdir")) +makedocs(sitename="Test", pages = pages, build="builds/absolute", workdir=joinpath(@__DIR__, "builds/absolute-workdir")) + +@info "Building builds/relative" +mkdir(joinpath(@__DIR__, "builds/relative-workdir")) +makedocs(sitename="Test", pages = pages, build="builds/relative", workdir="builds/relative-workdir") diff --git a/test/workdir/src/file.md b/test/workdir/src/file.md new file mode 100644 index 0000000000..78034fb94d --- /dev/null +++ b/test/workdir/src/file.md @@ -0,0 +1,9 @@ +# Testing + +Here's a test + +```@repl +pwd() + +touch("root_file.txt") +``` diff --git a/test/workdir/src/index.md b/test/workdir/src/index.md new file mode 100644 index 0000000000..dc285e4ae8 --- /dev/null +++ b/test/workdir/src/index.md @@ -0,0 +1,9 @@ +# Testing + +Here's a test + +```@repl +pwd() + +touch("root_index.txt") +``` diff --git a/test/workdir/src/subdir/file.md b/test/workdir/src/subdir/file.md new file mode 100644 index 0000000000..797f8efeb4 --- /dev/null +++ b/test/workdir/src/subdir/file.md @@ -0,0 +1,9 @@ +# Testing + +Here's a test + +```@repl +pwd() + +touch("subdir_file.txt") +``` diff --git a/test/workdir/src/subdir/index.md b/test/workdir/src/subdir/index.md new file mode 100644 index 0000000000..4e1c207a14 --- /dev/null +++ b/test/workdir/src/subdir/index.md @@ -0,0 +1,9 @@ +# Testing + +Here's a test + +```@repl +pwd() + +touch("subdir_index.txt") +``` diff --git a/test/workdir/tests.jl b/test/workdir/tests.jl new file mode 100644 index 0000000000..6de8486a74 --- /dev/null +++ b/test/workdir/tests.jl @@ -0,0 +1,36 @@ +using Test + +# for convenience, when debugging, the build step can be disabled when running the tests with +# julia test/workdir/tests.jl skipbuild +if !("skipbuild" in ARGS) + rm(joinpath(@__DIR__, "builds"), recursive=true, force=true) # cleanup of previous test run + include(joinpath(@__DIR__, "make.jl")) +end + +@testset "makedocs workdir" begin + # test for the default build + @test isfile(joinpath(@__DIR__, "builds", "default", "root_index.txt")) + @test isfile(joinpath(@__DIR__, "builds", "default", "root_file.txt")) + @test isfile(joinpath(@__DIR__, "builds", "default", "subdir", "subdir_index.txt")) + @test isfile(joinpath(@__DIR__, "builds", "default", "subdir", "subdir_file.txt")) + + # absolute path + @test !isfile(joinpath(@__DIR__, "builds", "absolute", "root_index.txt")) + @test !isfile(joinpath(@__DIR__, "builds", "absolute", "root_file.txt")) + @test !isfile(joinpath(@__DIR__, "builds", "absolute", "subdir", "subdir_index.txt")) + @test !isfile(joinpath(@__DIR__, "builds", "absolute", "subdir", "subdir_file.txt")) + @test isfile(joinpath(@__DIR__, "builds", "absolute-workdir", "root_index.txt")) + @test isfile(joinpath(@__DIR__, "builds", "absolute-workdir", "root_file.txt")) + @test isfile(joinpath(@__DIR__, "builds", "absolute-workdir", "subdir_index.txt")) + @test isfile(joinpath(@__DIR__, "builds", "absolute-workdir", "subdir_file.txt")) + + # relative path + @test !isfile(joinpath(@__DIR__, "builds", "relative", "root_index.txt")) + @test !isfile(joinpath(@__DIR__, "builds", "relative", "root_file.txt")) + @test !isfile(joinpath(@__DIR__, "builds", "relative", "subdir", "subdir_index.txt")) + @test !isfile(joinpath(@__DIR__, "builds", "relative", "subdir", "subdir_file.txt")) + @test isfile(joinpath(@__DIR__, "builds", "relative-workdir", "root_index.txt")) + @test isfile(joinpath(@__DIR__, "builds", "relative-workdir", "root_file.txt")) + @test isfile(joinpath(@__DIR__, "builds", "relative-workdir", "subdir_index.txt")) + @test isfile(joinpath(@__DIR__, "builds", "relative-workdir", "subdir_file.txt")) +end