Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise Plugins API, add plugins keyword arg for makedocs and doctest #2249

Merged
merged 3 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test/formats/builds/
test/missingdocs/build/
test/nongit/build/
test/errors/build/
test/plugins/build/
test/prerender/build/
test/themes/dev/
test/workdir/builds/
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

**For upgrading:** The JS file can still be include by passing it to the `assets` keyword of `format = HTML(...)`. However, it will likely conflict with Documenter's default search implementation. If you require an API to override Documenter's search engine, please open an issue.

* Plugin objects which were formally passed as (undocumented) positional keyword arguments to `makedocs` are now given as elements of a list `plugins` passed as a keyword argument ([#2245], [#2249])

**For upgrading:** If you are passing any plugin objects to `makedocs` (positionally), pass them via the `plugins` keyword instead.

goerz marked this conversation as resolved.
Show resolved Hide resolved
### Added

* Doctest filters can now be specified as regex/substitution pairs, i.e. `r"..." => s"..."`, in order to control the replacement (which defaults to the empty string, `""`). ([#1989], [#1271])
Expand Down Expand Up @@ -128,6 +132,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* The search UI has had a complete overhaul, with a fresh new modal UI with better context for search results and a filter mechanism to remove unwanted results. The client-side search engine has been changed from LunrJs to MinisearchJs. ([#1437], [#2141], [#2147], [#2202])

* The `doctest` routine can now receive the same `plugins` keyword argument as `makedocs`. This enables `doctest` to run if any plugin with a mandatory `Plugin` object is loaded, e.g., [DocumenterCitations](https://github.com/JuliaDocs/DocumenterCitations.jl). ([#2245])

### Fixed

* Line endings in Markdown source files are now normalized to `LF` before parsing, to work around [a bug in the Julia Markdown parser][julia-29344] where parsing is sensitive to line endings, and can therefore cause platform-dependent behavior. ([#1906])
Expand Down
14 changes: 8 additions & 6 deletions src/Documenter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ const ERROR_NAMES = [:autodocs_block, :cross_references, :docs_block, :doctest,
abstract type Plugin end

Any plugin that needs to either solicit user input or store information in a
[`Document`](@ref) should create a subtype of `Plugin`. The
subtype, `T <: Documenter.Plugin`, must have an empty constructor `T()` that
initialized `T` with the appropriate default values.
[`Document`](@ref) should create a subtype of `Plugin`, i.e., `T <: DocumenterPlugin`.

To retrieve the values stored in `T`, the plugin can call [`Documenter.getplugin`](@ref).
If `T` was passed to [`makedocs`](@ref), the passed type will be returned. Otherwise,
a new `T` object will be created.
Initialized objects of type `T` can be elements of the list of `plugins` passed as a
keyword argument to [`makedocs`](@ref).

A plugin may retrieve the existing object holding its state via the
[`Documenter.getplugin`](@ref) function. Alternatively, `getplugin` can also instantiate
`T()` on demand, if there is no existing object. This requires that `T` implements an empty
constructor `T()`.
"""
abstract type Plugin end

Expand Down
9 changes: 7 additions & 2 deletions src/doctest.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@

# Keywords

**`testset`** specifies the name of test testset (default `Doctests`).
**`testset`** specifies the name of test testset (default `"Doctests"`).

**`doctestfilters`** vector of regex to filter tests (see the manual on [Filtering Doctests](@ref))

**`fix`**, if set to `true`, updates all the doctests that fail with the correct output
(default `false`).

**`plugins`** is a list of [`Documenter.Plugin`](@ref) objects to be forwarded to
[`makedocs`](@ref). Use as directed by the documentation of a third-party plugin.

!!! warning
When running `doctest(...; fix=true)`, Documenter will modify the Markdown and Julia
source files. It is strongly recommended that you only run it on packages in Pkg's
Expand All @@ -66,6 +69,7 @@
fix = false,
testset = "Doctests",
doctestfilters = Regex[],
plugins = Plugin[],
)
function all_doctests()
dir = mktempdir()
Expand All @@ -75,7 +79,7 @@
source = joinpath(dir, "src")
mkdir(source)
end
makedocs(
makedocs(;

Check warning on line 82 in src/doctest.jl

View check run for this annotation

Codecov / codecov/patch

src/doctest.jl#L82

Added line #L82 was not covered by tests
root = dir,
source = source,
sitename = "",
Expand All @@ -85,6 +89,7 @@
# When doctesting, we don't really want to get bogged down with issues
# related to determining the remote repositories for edit URLs and such
remotes = nothing,
plugins = plugins,
)
true
catch err
Expand Down
26 changes: 13 additions & 13 deletions src/documents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@
blueprint :: DocumentBlueprint
end

function Document(plugins = nothing;
function Document(;
root :: AbstractString = currentdir(),
source :: AbstractString = "src",
build :: AbstractString = "build",
Expand All @@ -376,6 +376,7 @@
pages :: Vector = Any[],
pagesonly:: Bool = false,
expandfirst :: Vector = String[],
plugins :: Vector = Plugin[],
repo :: Union{Remotes.Remote, AbstractString} = "",
remotes :: Union{Dict, Nothing} = Dict(),
sitename :: AbstractString = "",
Expand Down Expand Up @@ -455,14 +456,12 @@
)

plugin_dict = Dict{DataType, Plugin}()
if plugins !== nothing
for plugin in plugins
plugin isa Plugin ||
throw(ArgumentError("$(typeof(plugin)) is not a subtype of `Plugin`."))
haskey(plugin_dict, typeof(plugin)) &&
throw(ArgumentError("only one copy of $(typeof(plugin)) may be passed."))
plugin_dict[typeof(plugin)] = plugin
end
for plugin in plugins
plugin isa Plugin ||

Check warning on line 460 in src/documents.jl

View check run for this annotation

Codecov / codecov/patch

src/documents.jl#L460

Added line #L460 was not covered by tests
throw(ArgumentError("$(typeof(plugin)) in `plugins=` is not a subtype of `Documenter.Plugin`."))
haskey(plugin_dict, typeof(plugin)) &&

Check warning on line 462 in src/documents.jl

View check run for this annotation

Codecov / codecov/patch

src/documents.jl#L462

Added line #L462 was not covered by tests
throw(ArgumentError("only one copy of $(typeof(plugin)) may be passed."))
plugin_dict[typeof(plugin)] = plugin

Check warning on line 464 in src/documents.jl

View check run for this annotation

Codecov / codecov/patch

src/documents.jl#L464

Added line #L464 was not covered by tests
end

blueprint = DocumentBlueprint(
Expand Down Expand Up @@ -796,11 +795,12 @@
end

"""
getplugin(doc::Document, T)
Documenter.getplugin(doc::Document, T) -> Plugin

Retrieves the [`Plugin`](@ref Plugin) type for `T` stored in `doc`. If `T` was passed to
[`makedocs`](@ref makedocs), the passed type will be returned. Otherwise, a new `T` object
will be created using the default constructor `T()`.
Retrieves the object for the [`Plugin`](@ref Plugin) sub-type `T` stored in `doc`. If an
object of type `T` was an element of the `plugins` list passed to [`makedocs`](@ref makedocs),
that object will be returned. Otherwise, a new `T` object will be created using the default
constructor `T()`. Subsequent calls to `getplugin(doc, T)` return the same object.
"""
function getplugin(doc::Document, plugin_type::Type{T}) where T <: Plugin
if !haskey(doc.plugins, plugin_type)
Expand Down
11 changes: 8 additions & 3 deletions src/makedocs.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Implements the makedocs() and functions directly related to it.

"""
makedocs(
makedocs(;
root = "<current-directory>",
source = "src",
build = "build",
Expand All @@ -13,6 +13,7 @@
sitename = "",
expandfirst = [],
draft = false,
others...
)

Combines markdown files and inline docstrings into an interlinked document.
Expand Down Expand Up @@ -217,6 +218,10 @@ determined from the source file path. E.g. for `src/foo.md` it is set to `build/

Note that `workdir` does not affect doctests.

**`plugins`** is a list of [`Documenter.Plugin`](@ref) objects. Use as directed by the
documentation of a third-party plugin. For any subtype `T <: Plugin`, the
`plugins` list may contain at most a single object of type `T`.

## Output formats

**`format`** allows the output format to be specified. The default format is
Expand All @@ -233,8 +238,8 @@ information.
A guide detailing how to document a package using Documenter's [`makedocs`](@ref) is provided
in the [setup guide in the manual](@ref Package-Guide).
"""
function makedocs(components...; debug = false, format = HTML(), kwargs...)
document = Documenter.Document(components; format=format, kwargs...)
function makedocs(; debug = false, format = HTML(), kwargs...)
document = Documenter.Document(; format=format, kwargs...)
# Before starting the build pipeline, we empty out the subtype cache used by
# Selectors.dispatch. This is to make sure that we pick up any new selector stages that
# may have been added to the selector pipelines between makedocs calls.
Expand Down
130 changes: 130 additions & 0 deletions test/plugins/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
module PluginsTestModule

# Test the documented behavior of `Plugin` and `getplugin`.

using Documenter, Test


# Flag whether the runner should do testing
mutable struct _RunPluginTests <: Documenter.Plugin
enabled::Bool
_RunPluginTests(enabled=false) = new(enabled)
end


mutable struct _TestPluginA <: Documenter.Plugin
processed::Bool
# no empty constructor: must be passed as object
end

mutable struct _TestPluginB <: Documenter.Plugin
processed::Bool
_TestPluginB() = new(false)
# empty constructor: can be instantiated on demand
end

mutable struct _TestPluginC <: Documenter.Plugin
processed::Bool
# no empty constructor: for checking error behavior
end


# Pipeline step for testing all of the above dummy plugins
abstract type _TestPlugins <: Documenter.Builder.DocumentPipeline end

# Run the pipeline early within DocumentPipeline (not that it really matters)
Documenter.Selectors.order(::Type{_TestPlugins}) = 0.001


function Documenter.Selectors.runner(::Type{_TestPlugins}, doc)

# Not sure if this runner might hook itself into other tests as a
# side-effect. Thus, we don't do anything unless the test was explicitly
# enabled.
if Documenter.getplugin(doc, _RunPluginTests).enabled

@info "_TestPlugins: testing plugin API"
@show doc.plugins # type => object or type, as passed to `makedocs`

# Plugin with passed object
@test _TestPluginA in keys(doc.plugins)
A = Documenter.getplugin(doc, _TestPluginA)
@test !(A.processed)
A.processed = true
@test A isa _TestPluginA

# plugin with empty constructor (no object passed)
@test !(_TestPluginB in keys(doc.plugins))
B = Documenter.getplugin(doc, _TestPluginB)
@test !(B.processed)
B.processed = true
@test B isa _TestPluginB

# subsequent calls to getplugin() should return the same object
B2 = Documenter.getplugin(doc, _TestPluginB)
@test B2.processed
@test B2 === B

# Missing object (no empty constructor)
@test !(_TestPluginC in keys(doc.plugins))
@test_throws MethodError begin
# getplugin is going to try to instantiate `_TestPluginC()`
C = Documenter.getplugin(doc, _TestPluginC)
end

end

end


A = _TestPluginA(false)
@test !(A.processed)
@test !(_TestPluginB().processed)
@test makedocs(;
plugins=[_RunPluginTests(true), A],
sitename="-", modules = [PluginsTestModule], warnonly=false
) === nothing
@test A.processed


# Errors


# The documentation for Plugin/getplugin made it sound like passing a Plugin
# class instead of a plugin object to `makedocs` was a possibility. This was
# never true, and we check here the specific error that is being thrown if
# someone were to try it.
err_msg = "DataType in `plugins=` is not a subtype of `Documenter.Plugin`."
@test_throws ArgumentError(err_msg) begin
makedocs(;
plugins=[_RunPluginTests(false), _TestPluginB],
sitename="-", modules = [PluginsTestModule], warnonly=false
)
end


# Only one instance of any given Plugin can be passed.
try # Use try-catch get get around @test_throws limitations in Julia 1.6
makedocs(;
plugins=[_RunPluginTests(false), _TestPluginA(true), _TestPluginA(false)],
sitename="-", modules = [PluginsTestModule], warnonly=false
)
@test false # makedocs should have thrown an ArgumentError
catch exc
@test exc isa ArgumentError
@test occursin(r"only one copy of .*_TestPluginA may be passed", exc.msg)
end


# Doctests - the `doctest` function must also be able to process plugins

A = _TestPluginA(false)
@test !(A.processed)
doctest(
joinpath(@__DIR__, "src"),
[PluginsTestModule];
plugins=[_RunPluginTests(true), A]
)
@test A.processed

end
4 changes: 4 additions & 0 deletions test/plugins/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```jldoctest
julia> 1 + 1
2
```
6 changes: 5 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ end
@info "Building errors/make.jl"
@quietly include("errors/make.jl")

# Plugin API
@info "Building plugins/make.jl"
@quietly include("plugins/make.jl")

# Unit tests for module internals.
include("except.jl")
include("utilities.jl")
Expand Down Expand Up @@ -83,7 +87,7 @@ end
@quietly include("workdir/tests.jl")

# Passing a writer positionally (https://github.com/JuliaDocs/Documenter.jl/issues/1046)
@test_throws ArgumentError makedocs(sitename="", HTML())
@test_throws MethodError makedocs(sitename="", HTML())

# Running doctest() on our own manual
@info "doctest() Documenter's manual"
Expand Down
Loading