Interactive mode (#145)
This commit is contained in:
parent
35002583b4
commit
18c32a1519
|
@ -2,3 +2,4 @@
|
|||
*.jl.*.cov
|
||||
*.jl.cov
|
||||
*.jl.mem
|
||||
/Manifest.toml
|
||||
|
|
|
@ -12,7 +12,6 @@ julia:
|
|||
before_script:
|
||||
- git config --global user.name Tester
|
||||
- git config --global user.email te@st.er
|
||||
script: travis_wait julia --project -e 'using Pkg; Pkg.test(coverage=true)'
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
|
|
115
Manifest.toml
115
Manifest.toml
|
@ -1,115 +0,0 @@
|
|||
# This file is machine-generated - editing it directly is not advised
|
||||
|
||||
[[Base64]]
|
||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||
|
||||
[[DataAPI]]
|
||||
git-tree-sha1 = "674b67f344687a88310213ddfa8a2b3c76cc4252"
|
||||
uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
|
||||
version = "1.1.0"
|
||||
|
||||
[[DataValueInterfaces]]
|
||||
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
|
||||
uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464"
|
||||
version = "1.0.0"
|
||||
|
||||
[[Dates]]
|
||||
deps = ["Printf"]
|
||||
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
||||
|
||||
[[Distributed]]
|
||||
deps = ["Random", "Serialization", "Sockets"]
|
||||
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"
|
||||
|
||||
[[InteractiveUtils]]
|
||||
deps = ["Markdown"]
|
||||
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
|
||||
|
||||
[[IteratorInterfaceExtensions]]
|
||||
git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856"
|
||||
uuid = "82899510-4779-5014-852e-03e436cf321d"
|
||||
version = "1.0.0"
|
||||
|
||||
[[LibGit2]]
|
||||
deps = ["Printf"]
|
||||
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
|
||||
|
||||
[[Libdl]]
|
||||
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
||||
|
||||
[[LinearAlgebra]]
|
||||
deps = ["Libdl"]
|
||||
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
|
||||
[[Logging]]
|
||||
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
|
||||
|
||||
[[Markdown]]
|
||||
deps = ["Base64"]
|
||||
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
|
||||
|
||||
[[Mustache]]
|
||||
deps = ["Printf", "Tables"]
|
||||
git-tree-sha1 = "e06eef2abee113c49695f5347668e15d4c02978a"
|
||||
uuid = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
|
||||
version = "1.0.0"
|
||||
|
||||
[[OrderedCollections]]
|
||||
deps = ["Random", "Serialization", "Test"]
|
||||
git-tree-sha1 = "c4c13474d23c60d20a67b217f1d7f22a40edf8f1"
|
||||
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
|
||||
version = "1.1.0"
|
||||
|
||||
[[Parameters]]
|
||||
deps = ["OrderedCollections"]
|
||||
git-tree-sha1 = "b62b2558efb1eef1fa44e4be5ff58a515c287e38"
|
||||
uuid = "d96e819e-fc66-5662-9728-84c9c7592b0a"
|
||||
version = "0.12.0"
|
||||
|
||||
[[Pkg]]
|
||||
deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"]
|
||||
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
|
||||
|
||||
[[Printf]]
|
||||
deps = ["Unicode"]
|
||||
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||
|
||||
[[REPL]]
|
||||
deps = ["InteractiveUtils", "Markdown", "Sockets"]
|
||||
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
|
||||
|
||||
[[Random]]
|
||||
deps = ["Serialization"]
|
||||
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||
|
||||
[[SHA]]
|
||||
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
|
||||
|
||||
[[Serialization]]
|
||||
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||
|
||||
[[Sockets]]
|
||||
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
|
||||
|
||||
[[TableTraits]]
|
||||
deps = ["IteratorInterfaceExtensions"]
|
||||
git-tree-sha1 = "b1ad568ba658d8cbb3b892ed5380a6f3e781a81e"
|
||||
uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c"
|
||||
version = "1.0.0"
|
||||
|
||||
[[Tables]]
|
||||
deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "TableTraits", "Test"]
|
||||
git-tree-sha1 = "aaed7b3b00248ff6a794375ad6adf30f30ca5591"
|
||||
uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
|
||||
version = "0.2.11"
|
||||
|
||||
[[Test]]
|
||||
deps = ["Distributed", "InteractiveUtils", "Logging", "Random"]
|
||||
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
|
||||
[[UUIDs]]
|
||||
deps = ["Random", "SHA"]
|
||||
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
|
||||
[[Unicode]]
|
||||
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
|
|
@ -5,10 +5,12 @@ version = "0.7.0-DEV"
|
|||
|
||||
[deps]
|
||||
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
||||
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
|
||||
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
|
||||
Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
|
||||
Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
|
||||
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
|
||||
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
|
||||
[compat]
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||
|
||||
[[DataAPI]]
|
||||
git-tree-sha1 = "674b67f344687a88310213ddfa8a2b3c76cc4252"
|
||||
git-tree-sha1 = "176e23402d80e7743fc26c19c681bfb11246af32"
|
||||
uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
|
||||
version = "1.1.0"
|
||||
version = "1.3.0"
|
||||
|
||||
[[DataValueInterfaces]]
|
||||
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
|
||||
|
@ -29,9 +29,9 @@ version = "0.8.1"
|
|||
|
||||
[[Documenter]]
|
||||
deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"]
|
||||
git-tree-sha1 = "646ebc3db49889ffeb4c36f89e5d82c6a26295ff"
|
||||
git-tree-sha1 = "395fa1554c69735802bba37d9e7d9586fd44326c"
|
||||
uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
version = "0.24.7"
|
||||
version = "0.24.11"
|
||||
|
||||
[[InteractiveUtils]]
|
||||
deps = ["Markdown"]
|
||||
|
@ -71,15 +71,14 @@ uuid = "a63ad114-7e13-5084-954f-fe012c677804"
|
|||
|
||||
[[Mustache]]
|
||||
deps = ["Printf", "Tables"]
|
||||
git-tree-sha1 = "e06eef2abee113c49695f5347668e15d4c02978a"
|
||||
git-tree-sha1 = "2e11fc5de3a01d23482a257e22009ddaab058d9a"
|
||||
uuid = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
[[OrderedCollections]]
|
||||
deps = ["Random", "Serialization", "Test"]
|
||||
git-tree-sha1 = "c4c13474d23c60d20a67b217f1d7f22a40edf8f1"
|
||||
git-tree-sha1 = "12ce190210d278e12644bcadf5b21cbdcf225cd3"
|
||||
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
|
||||
[[Parameters]]
|
||||
deps = ["OrderedCollections"]
|
||||
|
@ -89,16 +88,16 @@ version = "0.12.0"
|
|||
|
||||
[[Parsers]]
|
||||
deps = ["Dates", "Test"]
|
||||
git-tree-sha1 = "0c16b3179190d3046c073440d94172cfc3bb0553"
|
||||
git-tree-sha1 = "f8f5d2d4b4b07342e5811d2b6428e45524e241df"
|
||||
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
|
||||
version = "0.3.12"
|
||||
version = "1.0.2"
|
||||
|
||||
[[Pkg]]
|
||||
deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"]
|
||||
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
|
||||
|
||||
[[PkgTemplates]]
|
||||
deps = ["Dates", "LibGit2", "Mustache", "Parameters", "Pkg", "UUIDs"]
|
||||
deps = ["Dates", "InteractiveUtils", "LibGit2", "Mustache", "Parameters", "Pkg", "REPL", "UUIDs"]
|
||||
path = ".."
|
||||
uuid = "14b8a8f1-9102-5b29-a752-f990bacb7fe1"
|
||||
version = "0.7.0-DEV"
|
||||
|
@ -132,9 +131,9 @@ version = "1.0.0"
|
|||
|
||||
[[Tables]]
|
||||
deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "TableTraits", "Test"]
|
||||
git-tree-sha1 = "aaed7b3b00248ff6a794375ad6adf30f30ca5591"
|
||||
git-tree-sha1 = "c45dcc27331febabc20d86cb3974ef095257dcf3"
|
||||
uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
|
||||
version = "0.2.11"
|
||||
version = "1.0.4"
|
||||
|
||||
[[Test]]
|
||||
deps = ["Distributed", "InteractiveUtils", "Logging", "Random"]
|
||||
|
|
|
@ -75,7 +75,7 @@ To understand how they're implemented, let's look at simplified versions of two
|
|||
### Example: `Documenter`
|
||||
|
||||
```julia
|
||||
@with_kw_noshow struct Documenter <: Plugin
|
||||
@plugin struct Documenter <: Plugin
|
||||
make_jl::String = default_file("docs", "make.jl")
|
||||
index_md::String = default_file("docs", "src", "index.md")
|
||||
end
|
||||
|
@ -117,10 +117,11 @@ function hook(p::Documenter, t::Template, pkg_dir::AbstractString)
|
|||
end
|
||||
```
|
||||
|
||||
The `@with_kw_noshow` macro defines keyword constructors for us.
|
||||
The `@plugin` macro defines some helpful methods for us.
|
||||
Inside of our struct definition, we're using [`default_file`](@ref) to refer to files in this repository.
|
||||
|
||||
```@docs
|
||||
@plugin
|
||||
default_file
|
||||
```
|
||||
|
||||
|
@ -138,7 +139,11 @@ Badge
|
|||
```
|
||||
|
||||
These two functions, [`gitignore`](@ref) and [`badges`](@ref), are currently the only "special" functions for cross-plugin interactions.
|
||||
In other cases, you can still access the [`Template`](@ref)'s plugins to depend on the presence/properties of other plugins, although that's less powerful.
|
||||
In other cases, you can still access the [`Template`](@ref)'s plugins to depend on the presence/properties of other plugins via [`getplugin`](@ref), although that's less powerful.
|
||||
|
||||
```@docs
|
||||
getplugin
|
||||
```
|
||||
|
||||
Third, we implement [`view`](@ref), which is used to fill placeholders in badges and rendered files.
|
||||
|
||||
|
@ -197,6 +202,7 @@ function posthook(::Git, ::Template, pkg_dir::AbstractString)
|
|||
end
|
||||
```
|
||||
|
||||
We didn't use `@plugin` for this one, because there are no fields.
|
||||
Validation and all three hooks are implemented:
|
||||
|
||||
- [`validate`](@ref) makes sure that all required Git configuration is present.
|
||||
|
@ -217,7 +223,7 @@ In general, they just generate one templated file.
|
|||
To illustrate, let's look at the [`Citation`](@ref) plugin, which creates a `CITATION.bib` file.
|
||||
|
||||
```julia
|
||||
@with_kw_noshow struct Citation <: FilePlugin
|
||||
@plugin struct Citation <: FilePlugin
|
||||
file::String = default_file("CITATION.bib")
|
||||
end
|
||||
|
||||
|
@ -294,7 +300,7 @@ Of course, we could use a normal [`Plugin`](@ref), but it turns out there's a wa
|
|||
The plugin implements its own `hook`, but uses `invoke` to avoid duplicating the file creation code:
|
||||
|
||||
```julia
|
||||
@with_kw_noshow struct Tests <: FilePlugin
|
||||
@plugin struct Tests <: FilePlugin
|
||||
file::String = default_file("runtests.jl")
|
||||
end
|
||||
|
||||
|
@ -315,6 +321,20 @@ If you want to extend the validation but keep the file existence check, use the
|
|||
|
||||
For more examples, see the plugins in the [Continuous Integration (CI)](@ref) and [Code Coverage](@ref) sections.
|
||||
|
||||
## Supporting Interactive Mode
|
||||
|
||||
When it comes to supporting interactive mode for your custom plugins, you have two options: write your own [`interactive`](@ref) method, or use the default one.
|
||||
If you choose the first option, then you are free to implement the method however you want.
|
||||
If you want to use the default implementation, then there are a few functions that you should be aware of, although in many cases you will not need to add any new methods.
|
||||
|
||||
```@docs
|
||||
interactive
|
||||
prompt
|
||||
customizable
|
||||
input_tips
|
||||
convert_input
|
||||
```
|
||||
|
||||
## Miscellaneous Tips
|
||||
|
||||
### Writing Template Files
|
||||
|
|
|
@ -41,9 +41,12 @@ One less name to remember!
|
|||
| :-----------------------------------------: | :---------------------------------: |
|
||||
| `generate(::Template, pkg::AbstractString)` | `(::Template)(pkg::AbstractString)` |
|
||||
|
||||
## Interactive Templates
|
||||
## Interactive Mode
|
||||
|
||||
Currently not implemented, but will be in the future.
|
||||
| Old | New |
|
||||
| :-----------------------------------------: | :---------------------------------: |
|
||||
| `interactive_template()` | `Template(; interactive=true)` |
|
||||
| `generate_interactive(pkg::AbstractString)` | `Template(; interactive=true)(pkg)` |
|
||||
|
||||
## Other Functions
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ t("MyPkg")
|
|||
|
||||
```@docs
|
||||
Template
|
||||
generate
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
@ -40,6 +41,7 @@ Tests
|
|||
Readme
|
||||
License
|
||||
Git
|
||||
CompatHelper
|
||||
TagBot
|
||||
Secret
|
||||
```
|
||||
|
@ -76,7 +78,6 @@ Documenter
|
|||
|
||||
```@docs
|
||||
Develop
|
||||
CompatHelper
|
||||
Citation
|
||||
```
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@ module PkgTemplates
|
|||
using Base: active_project, contractuser
|
||||
|
||||
using Dates: month, today, year
|
||||
using InteractiveUtils: subtypes
|
||||
using LibGit2: LibGit2, GitConfig, GitRemote, GitRepo
|
||||
using Pkg: Pkg, TOML, PackageSpec
|
||||
using REPL.TerminalMenus: MultiSelectMenu, RadioMenu, request
|
||||
using UUIDs: uuid4
|
||||
|
||||
using Mustache: render
|
||||
|
@ -25,6 +27,7 @@ export
|
|||
GitHubActions,
|
||||
GitLabCI,
|
||||
License,
|
||||
NoDeploy,
|
||||
ProjectFile,
|
||||
Readme,
|
||||
Secret,
|
||||
|
@ -44,6 +47,7 @@ abstract type Plugin end
|
|||
include("template.jl")
|
||||
include("plugin.jl")
|
||||
include("show.jl")
|
||||
include("interactive.jl")
|
||||
include("deprecated.jl")
|
||||
|
||||
# Run some function with a project activated at the given path.
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
@deprecate interactive_template() Template(; interactive=true)
|
||||
@deprecate generate_interactive(pkg::AbstractString) Template(; interactive=true)(pkg)
|
||||
@deprecate GitHubPages(; kwargs...) Documenter{TravisCI}(; kwargs...)
|
||||
@deprecate GitLabPages(; kwargs...) Documenter{GitLabCI}(; kwargs...)
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
"""
|
||||
generate([pkg::AbstractString]) -> Template
|
||||
|
||||
Shortcut for `Template(; interactive=true)(pkg)`.
|
||||
If no package name is supplied, you will be prompted for one.
|
||||
"""
|
||||
function generate(pkg::AbstractString=prompt(Template, String, :pkg))
|
||||
t = Template(; interactive=true)
|
||||
t(pkg)
|
||||
return t
|
||||
end
|
||||
|
||||
"""
|
||||
interactive(T::Type{<:Plugin}) -> T
|
||||
|
||||
Interactively create a plugin of type `T`. Implement this method and ignore other
|
||||
related functions only if you want completely custom behaviour.
|
||||
"""
|
||||
function interactive(T::Type)
|
||||
pairs = interactive_pairs(T)
|
||||
|
||||
# There must be at least 2 MultiSelectMenu options.
|
||||
# If there are none, return immediately.
|
||||
# If there's just one, add a "dummy" option.
|
||||
isempty(pairs) && return T()
|
||||
just_one = length(pairs) == 1
|
||||
just_one && push!(pairs, :None => Nothing)
|
||||
|
||||
menu = MultiSelectMenu(
|
||||
collect(map(pair -> string(first(pair)), pairs));
|
||||
pagesize=length(pairs),
|
||||
)
|
||||
println("$(nameof(T)) keywords to customize:")
|
||||
customize = sort!(collect(request(menu)))
|
||||
|
||||
# If the "None" option was selected, don't customize anything.
|
||||
just_one && lastindex(pairs) in customize && return T()
|
||||
|
||||
kwargs = Dict{Symbol, Any}()
|
||||
foreach(pairs[customize]) do (name, F)
|
||||
kwargs[name] = prompt(T, F, name)
|
||||
end
|
||||
return T(; kwargs...)
|
||||
end
|
||||
|
||||
struct NotCustomizable end
|
||||
|
||||
"""
|
||||
customizable(::Type{<:Plugin}) -> Vector{Pair{Symbol, DataType}}
|
||||
|
||||
Return a list of keyword arguments that the given plugin type accepts,
|
||||
which are not fields of the type, and should be customizable in interactive mode.
|
||||
For example, for a constructor `Foo(; x::Bool)`, provide `[x => Bool]`.
|
||||
If `T` has fields which should not be customizable, use `NotCustomizable` as the type.
|
||||
"""
|
||||
customizable(::Type) = ()
|
||||
|
||||
function pretty_message(s::AbstractString)
|
||||
replacements = [
|
||||
r"Array{(.*?),1}" => s"Vector{\1}",
|
||||
r"Union{Nothing, (.*?)}" => s"Union{\1, Nothing}",
|
||||
]
|
||||
return reduce((s, p) -> replace(s, p), replacements; init=s)
|
||||
end
|
||||
|
||||
"""
|
||||
input_tips(::Type{T}) -> Vector{String}
|
||||
|
||||
Provide some extra tips to users on how to structure their input for the type `T`,
|
||||
for example if multiple delimited values are expected.
|
||||
"""
|
||||
input_tips(::Type{Vector{T}}) where T = ["comma-delimited", input_tips(T)...]
|
||||
input_tips(::Type{Nothing}) = String[]
|
||||
input_tips(::Type{Union{T, Nothing}}) where T = ["'nothing' for nothing", input_tips(T)...]
|
||||
input_tips(::Type{Secret}) = ["name only"]
|
||||
input_tips(::Type) = String[]
|
||||
|
||||
"""
|
||||
convert_input(::Type{P}, ::Type{T}, s::AbstractString) -> T
|
||||
|
||||
Convert the user input `s` into an instance of `T` for plugin of type `P`.
|
||||
A default implementation of `T(s)` exists.
|
||||
"""
|
||||
convert_input(::Type, T::Type{<:Real}, s::AbstractString) = parse(T, s)
|
||||
convert_input(::Type, T::Type, s::AbstractString) = T(s)
|
||||
|
||||
function convert_input(P::Type, ::Type{Union{T, Nothing}}, s::AbstractString) where T
|
||||
# This is kind of sketchy because technically, there might be some other input
|
||||
# whose value we want to instantiate with the string "nothing",
|
||||
# but I think that would be a pretty rare occurrence.
|
||||
# If that really happens, they can just override this method.
|
||||
return s == "nothing" ? nothing : convert_input(P, T, s)
|
||||
end
|
||||
|
||||
function convert_input(::Type, ::Type{Bool}, s::AbstractString)
|
||||
s = lowercase(s)
|
||||
return if startswith(s, 't') || startswith(s, 'y')
|
||||
true
|
||||
elseif startswith(s, 'f') || startswith(s, 'n')
|
||||
false
|
||||
else
|
||||
throw(ArgumentError("Unrecognized boolean response"))
|
||||
end
|
||||
end
|
||||
|
||||
function convert_input(P::Type, T::Type{<:Vector}, s::AbstractString)
|
||||
startswith(s, '[') && endswith(s, ']') && (s = s[2:end-1])
|
||||
xs = map(strip, split(s, ","))
|
||||
return map(x -> convert_input(P, eltype(T), x), xs)
|
||||
end
|
||||
|
||||
"""
|
||||
prompt(::Type{P}, ::Type{T}, ::Val{name::Symbol}) -> Any
|
||||
|
||||
Prompts for an input of type `T` for field `name` of plugin type `P`.
|
||||
Implement this method to customize particular fields of particular types.
|
||||
"""
|
||||
prompt(P::Type, T::Type, name::Symbol) = prompt(P, T, Val(name))
|
||||
|
||||
# The trailing `nothing` is a hack for `fallback_prompt` to use, ignore it.
|
||||
function prompt(P::Type, ::Type{T}, ::Val{name}, ::Nothing=nothing) where {T, name}
|
||||
tips = join([T; input_tips(T); "default=$(repr(defaultkw(P, name)))"], ", ")
|
||||
default = defaultkw(P, name)
|
||||
input = Base.prompt(pretty_message("Enter value for '$name' ($tips)"))
|
||||
input === nothing && throw(InterruptException())
|
||||
return if isempty(input)
|
||||
default
|
||||
else
|
||||
try
|
||||
# Working around what appears to be a bug in Julia 1.0:
|
||||
# #145#issuecomment-623049535
|
||||
if VERSION < v"1.1" && T isa Union && Nothing <: T
|
||||
if input == "nothing"
|
||||
nothing
|
||||
else
|
||||
convert_input(P, T.a === Nothing ? T.b : T.a, input)
|
||||
end
|
||||
else
|
||||
convert_input(P, T, input)
|
||||
end
|
||||
catch ex
|
||||
ex isa InterruptException && rethrow()
|
||||
@warn "Invalid input" ex
|
||||
prompt(P, T, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Compute all the concrete subtypes of T.
|
||||
concretes_rec(T::Type) = isabstracttype(T) ? vcat(map(concretes_rec, subtypes(T))...) : Any[T]
|
||||
concretes(T::Type) = sort!(concretes_rec(T); by=nameof)
|
||||
|
||||
# Compute name => type pairs for T's interactive options.
|
||||
function interactive_pairs(T::Type)
|
||||
pairs = collect(map(name -> name => fieldtype(T, name), fieldnames(T)))
|
||||
|
||||
# Use prepend! here so that users can override field types if they wish.
|
||||
prepend!(pairs, reverse(customizable(T)))
|
||||
uniqueby!(first, pairs)
|
||||
filter!(p -> last(p) !== NotCustomizable, pairs)
|
||||
sort!(pairs; by=first)
|
||||
|
||||
return pairs
|
||||
end
|
||||
|
||||
# unique!(f, xs) added here: https://github.com/JuliaLang/julia/pull/30141
|
||||
if VERSION >= v"1.1"
|
||||
const uniqueby! = unique!
|
||||
else
|
||||
function uniqueby!(f, xs)
|
||||
seen = Set()
|
||||
todelete = Int[]
|
||||
foreach(enumerate(map(f, xs))) do (i, out)
|
||||
out in seen && push!(todelete, i)
|
||||
push!(seen, out)
|
||||
end
|
||||
return deleteat!(xs, todelete)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,82 @@
|
|||
const TEMPLATES_DIR = normpath(joinpath(@__DIR__, "..", "templates"))
|
||||
const DEFAULT_PRIORITY = 1000
|
||||
|
||||
"""
|
||||
@plugin struct ... end
|
||||
|
||||
Define a plugin subtype with keyword constructors and default values.
|
||||
|
||||
For details on the general syntax, see
|
||||
[Parameters.jl](https://mauro3.github.io/Parameters.jl/stable/manual/#Types-with-default-values-and-keyword-constructors-1).
|
||||
|
||||
There are a few extra restrictions:
|
||||
|
||||
- Before using this macro, you must have imported `@with_kw_noshow`
|
||||
via `using PkgTemplates: @with_kw_noshow`
|
||||
- The type must be a subtype of [`Plugin`](@ref) (or one of its abstract subtypes)
|
||||
- The type cannot be parametric
|
||||
- All fields must have default values
|
||||
|
||||
## Example
|
||||
|
||||
```julia
|
||||
using PkgTemplates: @plugin, @with_kw_noshow, Plugin
|
||||
@plugin struct MyPlugin <: Plugin
|
||||
x::String = "hello!"
|
||||
y::Union{Int, Nothing} = nothing
|
||||
end
|
||||
```
|
||||
|
||||
## Implementing `@plugin` Manually
|
||||
|
||||
If for whatever reason, you are unable to meet the criteria outlined above,
|
||||
you can manually implement the methods that `@plugin` would have created for you.
|
||||
This is only mandatory if you want to use your plugin in interactive mode.
|
||||
|
||||
### Keyword Constructors
|
||||
|
||||
If possible, use `@with_kw_noshow` to create a keyword constructor for your type.
|
||||
Your type must be capable of being instantiated with no arguments.
|
||||
|
||||
### Default Values
|
||||
|
||||
If your type's fields have sensible default values, implement `defaultkw` like so:
|
||||
|
||||
```julia
|
||||
using PkgTemplates: PkgTemplates, Plugin
|
||||
struct MyPlugin <: Plugin
|
||||
x::String
|
||||
end
|
||||
PkgTemplates.defaultkw(::Type{MyPlugin}, ::Val{:x}) = "my default"
|
||||
```
|
||||
|
||||
Remember to add a method to the function belonging to PkgTemplates,
|
||||
rather than creating your own function that PkgTemplates won't see.
|
||||
|
||||
If your plugin's fields have no sane defaults, then you'll need to implement
|
||||
[`prompt`](@ref) appropriately instead.
|
||||
"""
|
||||
macro plugin(ex::Expr)
|
||||
@assert ex.head === :struct "Expression must be a struct definition"
|
||||
@assert ex.args[2] isa Expr && ex.args[2].head === :<: "Type must have a supertype"
|
||||
T = ex.args[2].args[1]
|
||||
@assert T isa Symbol "@plugin does not work for parametric types"
|
||||
|
||||
msg = "Run `using PkgTemplates: @with_kw_noshow` before using this macro"
|
||||
@assert isdefined(__module__, Symbol("@with_kw_noshow")) msg
|
||||
block = :(begin @with_kw_noshow $ex end)
|
||||
|
||||
foreach(filter(arg -> arg isa Expr, ex.args[3].args)) do field
|
||||
@assert field.head === :(=) "Field must have a default value"
|
||||
name = QuoteNode(field.args[1].args[1])
|
||||
default = field.args[2]
|
||||
def = :(PkgTemplates.defaultkw(::Type{$T}, ::Val{$name}) = $default)
|
||||
push!(block.args, def)
|
||||
end
|
||||
|
||||
return esc(block)
|
||||
end
|
||||
|
||||
function Base.:(==)(a::T, b::T) where T <: Plugin
|
||||
return all(n -> getfield(a, n) == getfield(b, n), fieldnames(T))
|
||||
end
|
||||
|
@ -240,13 +316,13 @@ function gen_file(file::AbstractString, text::AbstractString)
|
|||
end
|
||||
|
||||
"""
|
||||
render_file(file::AbstractString view::Dict{<:AbstractString}, tags) -> String
|
||||
render_file(file::AbstractString view::Dict{<:AbstractString}, tags=nothing) -> String
|
||||
|
||||
Render a template file with the data in `view`.
|
||||
`tags` should be a tuple of two strings, which are the opening and closing delimiters,
|
||||
or `nothing` to use the default delimiters.
|
||||
"""
|
||||
function render_file(file::AbstractString, view::Dict{<:AbstractString}, tags)
|
||||
function render_file(file::AbstractString, view::Dict{<:AbstractString}, tags=nothing)
|
||||
return render_text(read(file, String), view, tags)
|
||||
end
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ $EXTRA_VERSIONS_DOC
|
|||
If using coverage plugins, don't forget to manually add your API tokens as secrets,
|
||||
as described [here](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets).
|
||||
"""
|
||||
@with_kw_noshow struct GitHubActions <: FilePlugin
|
||||
@plugin struct GitHubActions <: FilePlugin
|
||||
file::String = default_file("github", "workflows", "ci.yml")
|
||||
destination::String = "ci.yml"
|
||||
linux::Bool = true
|
||||
|
@ -116,7 +116,7 @@ Integrates your packages with [Travis CI](https://travis-ci.com).
|
|||
Another code coverage plugin such as [`Codecov`](@ref) must also be included.
|
||||
$EXTRA_VERSIONS_DOC
|
||||
"""
|
||||
@with_kw_noshow struct TravisCI <: FilePlugin
|
||||
@plugin struct TravisCI <: FilePlugin
|
||||
file::String = default_file("travis.yml")
|
||||
linux::Bool = true
|
||||
osx::Bool = true
|
||||
|
@ -188,7 +188,7 @@ via [AppVeyor.jl](https://github.com/JuliaCI/Appveyor.jl).
|
|||
[`Codecov`](@ref) must also be included.
|
||||
$EXTRA_VERSIONS_DOC
|
||||
"""
|
||||
@with_kw_noshow struct AppVeyor <: FilePlugin
|
||||
@plugin struct AppVeyor <: FilePlugin
|
||||
file::String = default_file("appveyor.yml")
|
||||
x86::Bool = false
|
||||
coverage::Bool = true
|
||||
|
@ -244,7 +244,7 @@ $EXTRA_VERSIONS_DOC
|
|||
Code coverage submission from Cirrus CI is not yet supported by
|
||||
[Coverage.jl](https://github.com/JuliaCI/Coverage.jl).
|
||||
"""
|
||||
@with_kw_noshow struct CirrusCI <: FilePlugin
|
||||
@plugin struct CirrusCI <: FilePlugin
|
||||
file::String = default_file("cirrus.yml")
|
||||
image::String = "freebsd-12-0-release-amd64"
|
||||
coverage::Bool = true
|
||||
|
@ -293,7 +293,7 @@ See [`Documenter`](@ref) for more information.
|
|||
!!! note
|
||||
Nightly Julia is not supported.
|
||||
"""
|
||||
@with_kw_noshow struct GitLabCI <: FilePlugin
|
||||
@plugin struct GitLabCI <: FilePlugin
|
||||
file::String = default_file("gitlab-ci.yml")
|
||||
coverage::Bool = true
|
||||
# Nightly has no Docker image.
|
||||
|
@ -353,7 +353,7 @@ $EXTRA_VERSIONS_DOC
|
|||
!!! note
|
||||
Nightly Julia is not supported.
|
||||
"""
|
||||
@with_kw_noshow struct DroneCI <: FilePlugin
|
||||
@plugin struct DroneCI <: FilePlugin
|
||||
file::String = default_file("drone.star")
|
||||
destination::String = ".drone.star"
|
||||
amd64::Bool = true
|
||||
|
@ -396,6 +396,8 @@ function collect_versions(t::Template, versions::Vector)
|
|||
return sort(unique(vs))
|
||||
end
|
||||
|
||||
const AllCI = Union{AppVeyor, GitHubActions, TravisCI, CirrusCI, GitLabCI, DroneCI}
|
||||
|
||||
"""
|
||||
is_ci(::Plugin) -> Bool
|
||||
|
||||
|
@ -403,6 +405,7 @@ Determine whether or not a plugin is a CI plugin.
|
|||
If you are adding a CI plugin, you should implement this function and return `true`.
|
||||
"""
|
||||
is_ci(::Plugin) = false
|
||||
is_ci(::Union{AppVeyor, GitHubActions, TravisCI, CirrusCI, GitLabCI, DroneCI}) = true
|
||||
is_ci(::AllCI) = true
|
||||
|
||||
needs_username(::Union{AppVeyor, GitHubActions, TravisCI, CirrusCI, GitLabCI, DroneCI}) = true
|
||||
needs_username(::AllCI) = true
|
||||
customizable(::Type{<:AllCI}) = (:extra_versions => Vector{VersionNumber},)
|
||||
|
|
|
@ -7,7 +7,7 @@ Creates a `CITATION.bib` file for citing package repositories.
|
|||
- `file::AbstractString`: Template file for `CITATION.bib`.
|
||||
- `readme::Bool`: Whether or not to include a section about citing in the README.
|
||||
"""
|
||||
@with_kw_noshow struct Citation <: FilePlugin
|
||||
@plugin struct Citation <: FilePlugin
|
||||
file::String = default_file("CITATION.bib")
|
||||
readme::Bool = false
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ Integrates your packages with [CompatHelper](https://github.com/bcbi/CompatHelpe
|
|||
relative to `.github/workflows`.
|
||||
- `cron::AbstractString`: Cron expression for the schedule interval.
|
||||
"""
|
||||
@with_kw_noshow struct CompatHelper <: FilePlugin
|
||||
@plugin struct CompatHelper <: FilePlugin
|
||||
file::String = default_file("github", "workflows", "CompatHelper.yml")
|
||||
destination::String = "CompatHelper.yml"
|
||||
cron::String = "0 0 * * *"
|
||||
|
|
|
@ -9,7 +9,7 @@ Sets up code coverage submission from CI to [Codecov](https://codecov.io).
|
|||
- `file::Union{AbstractString, Nothing}`: Template file for `.codecov.yml`,
|
||||
or `nothing` to create no file.
|
||||
"""
|
||||
@with_kw_noshow struct Codecov <: FilePlugin
|
||||
@plugin struct Codecov <: FilePlugin
|
||||
file::Union{String, Nothing} = nothing
|
||||
end
|
||||
|
||||
|
@ -31,7 +31,7 @@ Sets up code coverage submission from CI to [Coveralls](https://coveralls.io).
|
|||
- `file::Union{AbstractString, Nothing}`: Template file for `.coveralls.yml`,
|
||||
or `nothing` to create no file.
|
||||
"""
|
||||
@with_kw_noshow struct Coveralls <: FilePlugin
|
||||
@plugin struct Coveralls <: FilePlugin
|
||||
file::Union{String, Nothing} = nothing
|
||||
end
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@ const DOCUMENTER_DEP = PackageSpec(;
|
|||
uuid="e30172f5-a6a5-5a46-863b-614d45cd2de4",
|
||||
)
|
||||
|
||||
const DeployStyle = Union{TravisCI, GitHubActions, GitLabCI, Nothing}
|
||||
struct NoDeploy end
|
||||
const DeployStyle = Union{TravisCI, GitHubActions, GitLabCI, NoDeploy}
|
||||
const GitHubPagesStyle = Union{TravisCI, GitHubActions}
|
||||
|
||||
"""
|
||||
Documenter{T<:Union{TravisCI, GitLabCI, Nothing}}(;
|
||||
Documenter{T<:Union{TravisCI, GitLabCI, NoDeploy}}(;
|
||||
make_jl="$(contractuser(default_file("docs", "make.jl")))",
|
||||
index_md="$(contractuser(default_file("docs", "src", "index.md")))",
|
||||
assets=String[],
|
||||
|
@ -26,7 +27,7 @@ or `Nothing` to only support local documentation builds.
|
|||
with the help of [`TravisCI`](@ref).
|
||||
- `GitLabCI`: Deploys documentation to [GitLab Pages](https://pages.gitlab.com)
|
||||
with the help of [`GitLabCI`](@ref).
|
||||
- `Nothing` (default): Does not set up documentation deployment.
|
||||
- `NoDeploy` (default): Does not set up documentation deployment.
|
||||
|
||||
## Keyword Arguments
|
||||
- `make_jl::AbstractString`: Template file for `make.jl`.
|
||||
|
@ -49,7 +50,7 @@ struct Documenter{T<:DeployStyle} <: Plugin
|
|||
make_jl::String
|
||||
index_md::String
|
||||
|
||||
# Can't use @with_kw_noshow due to some weird precompilation issues.
|
||||
# Can't use @plugin because we're implementing our own no-arguments constructor.
|
||||
function Documenter{T}(;
|
||||
assets::Vector{<:AbstractString}=String[],
|
||||
makedocs_kwargs::Dict{Symbol}=Dict{Symbol, Any}(),
|
||||
|
@ -61,7 +62,12 @@ struct Documenter{T<:DeployStyle} <: Plugin
|
|||
end
|
||||
end
|
||||
|
||||
Documenter(; kwargs...) = Documenter{Nothing}(; kwargs...)
|
||||
Documenter(; kwargs...) = Documenter{NoDeploy}(; kwargs...)
|
||||
|
||||
# We have to define these manually because we didn't use @plugin.
|
||||
defaultkw(::Type{<:Documenter}, ::Val{:assets}) = String[]
|
||||
defaultkw(::Type{<:Documenter}, ::Val{:make_jl}) = default_file("docs", "make.jl")
|
||||
defaultkw(::Type{<:Documenter}, ::Val{:index_md}) = default_file("docs", "src", "index.md")
|
||||
|
||||
gitignore(::Documenter) = ["/docs/build/"]
|
||||
priority(::Documenter, ::Function) = DEFAULT_PRIORITY - 1 # We need SrcDir to go first.
|
||||
|
@ -102,7 +108,7 @@ function view(p::Documenter{<:GitHubPagesStyle}, t::Template, pkg::AbstractStrin
|
|||
return merge(base, Dict("HAS_DEPLOY" => true))
|
||||
end
|
||||
|
||||
validate(::Documenter{Nothing}, ::Template) = nothing
|
||||
validate(::Documenter{NoDeploy}, ::Template) = nothing
|
||||
function validate(::Documenter{T}, t::Template) where T <: DeployStyle
|
||||
if !hasplugin(t, T)
|
||||
name = nameof(T)
|
||||
|
@ -138,6 +144,18 @@ gitlab_pages_url(t::Template, pkg::AbstractString) = "https://$(t.user).gitlab.i
|
|||
|
||||
make_canonical(::Type{<:GitHubPagesStyle}) = github_pages_url
|
||||
make_canonical(::Type{GitLabCI}) = gitlab_pages_url
|
||||
make_canonical(::Type{Nothing}) = nothing
|
||||
make_canonical(::Type{NoDeploy}) = nothing
|
||||
|
||||
needs_username(::Documenter) = true
|
||||
|
||||
function customizable(::Type{<:Documenter})
|
||||
return (:canonical_url => NotCustomizable, :makedocs_kwargs => NotCustomizable)
|
||||
end
|
||||
|
||||
function interactive(::Type{Documenter})
|
||||
styles = [Nothing, TravisCI, GitLabCI]
|
||||
menu = RadioMenu(map(string, styles); pagesize=length(styles))
|
||||
println("Documenter deploy style:")
|
||||
idx = request(menu)
|
||||
return interactive(Documenter{styles[idx]})
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ Creates a Git repository and a `.gitignore` file.
|
|||
This option requires that the Git CLI is installed,
|
||||
and for you to have a GPG key associated with your committer identity.
|
||||
"""
|
||||
@with_kw_noshow struct Git <: Plugin
|
||||
@plugin struct Git <: Plugin
|
||||
ignore::Vector{String} = String[]
|
||||
name::Union{String, Nothing} = nothing
|
||||
email::Union{String, Nothing} = nothing
|
||||
|
@ -34,8 +34,6 @@ end
|
|||
# Try to make sure that no files are created after we commit.
|
||||
priority(::Git, ::typeof(posthook)) = 5
|
||||
|
||||
Base.:(==)(a::Git, b::Git) = all(map(n -> getfield(a, n) == getfield(b, n), fieldnames(Git)))
|
||||
|
||||
function gitignore(p::Git)
|
||||
ignore = copy(p.ignore)
|
||||
p.manifest || push!(ignore, "Manifest.toml")
|
||||
|
@ -101,6 +99,7 @@ function posthook(p::Git, ::Template, pkg_dir::AbstractString)
|
|||
msg = "Files generated by PkgTemplates"
|
||||
v = version_of("PkgTemplates")
|
||||
v === nothing || (msg *= "\n\nPkgTemplates version: $v")
|
||||
# TODO: Put the template config in the message too?
|
||||
commit(p, repo, pkg_dir, msg)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,9 +29,26 @@ function License(;
|
|||
return License(path, destination)
|
||||
end
|
||||
|
||||
defaultkw(::Type{License}, ::Val{:path}) = nothing
|
||||
defaultkw(::Type{License}, ::Val{:name}) = "MIT"
|
||||
defaultkw(::Type{License}, ::Val{:destination}) = "LICENSE"
|
||||
|
||||
source(p::License) = p.path
|
||||
destination(p::License) = p.destination
|
||||
view(::License, t::Template, ::AbstractString) = Dict(
|
||||
"AUTHORS" => join(t.authors, ", "),
|
||||
"YEAR" => year(today()),
|
||||
)
|
||||
|
||||
function prompt(::Type{License}, ::Type, ::Val{:name})
|
||||
options = readdir(default_file("licenses"))
|
||||
# Move MIT to the top.
|
||||
deleteat!(options, findfirst(==("MIT"), options))
|
||||
pushfirst!(options, "MIT")
|
||||
menu = RadioMenu(options; pagesize=length(options))
|
||||
println("Select a license:")
|
||||
idx = request(menu)
|
||||
return options[idx]
|
||||
end
|
||||
|
||||
customizable(::Type{License}) = (:name => String,)
|
||||
|
|
|
@ -6,7 +6,7 @@ Creates a `Project.toml`.
|
|||
## Keyword Arguments
|
||||
- `version::VersionNumber`: The initial version of created packages.
|
||||
"""
|
||||
@with_kw_noshow struct ProjectFile <: Plugin
|
||||
@plugin struct ProjectFile <: Plugin
|
||||
version::VersionNumber = v"0.1.0"
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Creates a `README` file that contains badges for other included plugins.
|
|||
For example, values of `"README"` or `"README.rst"` might be desired.
|
||||
- `inline_badges::Bool`: Whether or not to put the badges on the same line as the package name.
|
||||
"""
|
||||
@with_kw_noshow struct Readme <: FilePlugin
|
||||
@plugin struct Readme <: FilePlugin
|
||||
file::String = default_file("README.md")
|
||||
destination::String = "README.md"
|
||||
inline_badges::Bool = false
|
||||
|
|
|
@ -6,7 +6,7 @@ Creates a module entrypoint.
|
|||
## Keyword Arguments
|
||||
- `file::AbstractString`: Template file for `src/<module>.jl`.
|
||||
"""
|
||||
@with_kw_noshow mutable struct SrcDir <: FilePlugin
|
||||
@plugin mutable struct SrcDir <: FilePlugin
|
||||
file::String = default_file("src", "module.jl")
|
||||
destination::String = ""
|
||||
end
|
||||
|
|
|
@ -34,7 +34,7 @@ Adds GitHub release support via [TagBot](https://github.com/JuliaRegistries/TagB
|
|||
- `dispatch::Bool`: Whether or not to enable the `dispatch` option.
|
||||
- `dispatch_delay::Int`: Number of minutes to delay for dispatch events.
|
||||
"""
|
||||
@with_kw_noshow struct TagBot <: FilePlugin
|
||||
@plugin struct TagBot <: FilePlugin
|
||||
file::String = default_file("github", "workflows", "TagBot.yml")
|
||||
destination::String = "TagBot.yml"
|
||||
cron::String = "0 0 * * *"
|
||||
|
|
|
@ -16,7 +16,7 @@ Sets up testing for packages.
|
|||
Managing test dependencies with `test/Project.toml` is only supported
|
||||
in Julia 1.2 and later.
|
||||
"""
|
||||
@with_kw_noshow struct Tests <: FilePlugin
|
||||
@plugin struct Tests <: FilePlugin
|
||||
file::String = default_file("test", "runtests.jl")
|
||||
project::Bool = false
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ end
|
|||
|
||||
function Base.show(io::IO, ::MIME"text/plain", p::T) where T <: Plugin
|
||||
indent = get(io, :indent, 0)
|
||||
print(io, repeat(' ', indent), T)
|
||||
print(io, repeat(' ', indent), nameof(T))
|
||||
ns = fieldnames(T)
|
||||
isempty(ns) || print(io, ":")
|
||||
foreach(ns) do n
|
||||
|
|
100
src/template.jl
100
src/template.jl
|
@ -42,10 +42,17 @@ A configuration used to generate packages.
|
|||
### Template Plugins
|
||||
- `plugins::Vector{<:Plugin}=Plugin[]`: A list of [`Plugin`](@ref)s used by the template.
|
||||
The default plugins are [`ProjectFile`](@ref), [`SrcDir`](@ref), [`Tests`](@ref),
|
||||
[`Readme`](@ref), [`License`](@ref), and [`Git`](@ref).
|
||||
[`Readme`](@ref), [`License`](@ref), [`Git`](@ref), [`CompatHelper`](@ref), and
|
||||
[`TagBot`](@ref).
|
||||
To disable a default plugin, pass in the negated type: `!PluginType`.
|
||||
To override a default plugin instead of disabling it, pass in your own instance.
|
||||
|
||||
### Interactive Mode
|
||||
- `interactive::Bool=false`: In addition to specifying the template options with keywords,
|
||||
you can also build up a template by following a set of prompts.
|
||||
To create a template interactively, set this keyword to `true`.
|
||||
See also the similar [`generate`](@ref) function.
|
||||
|
||||
---
|
||||
|
||||
To create a package from a `Template`, use the following syntax:
|
||||
|
@ -65,9 +72,9 @@ struct Template
|
|||
user::String
|
||||
end
|
||||
|
||||
Template(; kwargs...) = Template(Val(false); kwargs...)
|
||||
Template(; interactive::Bool=false, kwargs...) = Template(Val(interactive); kwargs...)
|
||||
Template(::Val{true}; kwargs...) = interactive(Template; kwargs...)
|
||||
|
||||
# Non-interactive constructor.
|
||||
function Template(::Val{false}; kwargs...)
|
||||
kwargs = Dict(kwargs)
|
||||
|
||||
|
@ -144,7 +151,11 @@ end
|
|||
hasplugin(t::Template, f::Function) = any(f, t.plugins)
|
||||
hasplugin(t::Template, ::Type{T}) where T <: Plugin = hasplugin(t, p -> p isa T)
|
||||
|
||||
# Get a plugin by type.
|
||||
"""
|
||||
getplugin(t::Template, ::Type{T<:Plugin}) -> Union{T, Nothing}
|
||||
|
||||
Get the plugin of type `T` from the template `t`, if it's present.
|
||||
"""
|
||||
function getplugin(t::Template, ::Type{T}) where T <: Plugin
|
||||
i = findfirst(p -> p isa T, t.plugins)
|
||||
return i === nothing ? nothing : t.plugins[i]
|
||||
|
@ -156,8 +167,87 @@ getkw!(kwargs, k) = pop!(kwargs, k, defaultkw(Template, k))
|
|||
# Default Template keyword values.
|
||||
defaultkw(::Type{T}, s::Symbol) where T = defaultkw(T, Val(s))
|
||||
defaultkw(::Type{Template}, ::Val{:authors}) = default_authors()
|
||||
defaultkw(::Type{Template}, ::Val{:dir}) = Pkg.devdir()
|
||||
defaultkw(::Type{Template}, ::Val{:dir}) = contractuser(Pkg.devdir())
|
||||
defaultkw(::Type{Template}, ::Val{:host}) = "github.com"
|
||||
defaultkw(::Type{Template}, ::Val{:julia}) = default_version()
|
||||
defaultkw(::Type{Template}, ::Val{:plugins}) = Plugin[]
|
||||
defaultkw(::Type{Template}, ::Val{:user}) = default_user()
|
||||
|
||||
function interactive(::Type{Template}; kwargs...)
|
||||
# If the user supplied any keywords themselves, don't prompt for them.
|
||||
kwargs = Dict{Symbol, Any}(kwargs)
|
||||
options = [:user, :authors, :dir, :host, :julia, :plugins]
|
||||
customizable = setdiff(options, keys(kwargs))
|
||||
|
||||
# Make sure we don't try to show a menu with < 2 options.
|
||||
isempty(customizable) && return Template(; kwargs...)
|
||||
just_one = length(customizable) == 1
|
||||
just_one && push(customizable, "None")
|
||||
|
||||
return try
|
||||
println("Template keywords to customize:")
|
||||
menu = MultiSelectMenu(map(string, customizable); pagesize=length(customizable))
|
||||
customize = customizable[sort!(collect(request(menu)))]
|
||||
just_one && lastindex(customizable) in customize && return Template(; kwargs...)
|
||||
|
||||
# Prompt for each keyword.
|
||||
foreach(customize) do k
|
||||
kwargs[k] = prompt(Template, fieldtype(Template, k), k)
|
||||
end
|
||||
|
||||
Template(; kwargs...)
|
||||
catch e
|
||||
e isa InterruptException || rethrow()
|
||||
println()
|
||||
@info "Cancelled"
|
||||
nothing
|
||||
end
|
||||
end
|
||||
|
||||
function prompt(::Type{Template}, ::Type, ::Val{:host})
|
||||
hosts = ["github.com", "gitlab.com", "bitbucket.org", "Other"]
|
||||
menu = RadioMenu(hosts; pagesize=length(hosts))
|
||||
println("Select Git repository hosting service:")
|
||||
idx = request(menu)
|
||||
return if idx == lastindex(hosts)
|
||||
fallback_prompt(String, :host)
|
||||
else
|
||||
hosts[idx]
|
||||
end
|
||||
end
|
||||
|
||||
function prompt(::Type{Template}, ::Type, ::Val{:julia})
|
||||
versions = map(format_version, VersionNumber.(1, 0:VERSION.minor))
|
||||
push!(versions, "Other")
|
||||
menu = RadioMenu(map(string, versions); pagesize=length(versions))
|
||||
println("Select minimum Julia version:")
|
||||
idx = request(menu)
|
||||
return if idx == lastindex(versions)
|
||||
fallback_prompt(VersionNumber, :julia)
|
||||
else
|
||||
VersionNumber(versions[idx])
|
||||
end
|
||||
end
|
||||
|
||||
const CR = "\r"
|
||||
const DOWN = "\eOB"
|
||||
|
||||
function prompt(::Type{Template}, ::Type, ::Val{:plugins})
|
||||
defaults = map(typeof, default_plugins())
|
||||
ndefaults = length(defaults)
|
||||
# Put the defaults first.
|
||||
options = unique!([defaults; concretes(Plugin)])
|
||||
menu = MultiSelectMenu(map(T -> string(nameof(T)), options); pagesize=length(options))
|
||||
println("Select plugins:")
|
||||
# Pre-select the default plugins and move the cursor to the first non-default.
|
||||
# To make this better, we need julia#30043.
|
||||
print(stdin.buffer, (CR * DOWN)^ndefaults)
|
||||
types = sort!(collect(request(menu)))
|
||||
plugins = Vector{Any}(map(interactive, options[types]))
|
||||
# Find any defaults that were disabled.
|
||||
foreach(i -> i in types || push!(plugins, !defaults[i]), 1:ndefaults)
|
||||
return plugins
|
||||
end
|
||||
|
||||
# Call the default prompt method even if a specialized one exists.
|
||||
fallback_prompt(T::Type, name::Symbol) = prompt(Template, T, Val(name), nothing)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@info "Running Git tests"
|
||||
|
||||
@testset "Git repositories" begin
|
||||
@testset "Does not create Git repo" begin
|
||||
t = tpl(; plugins=[!Git])
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
@info "Running interactive tests"
|
||||
|
||||
using PkgTemplates: @with_kw_noshow
|
||||
|
||||
const CR = "\r"
|
||||
const LF = "\n"
|
||||
const UP = "\eOA"
|
||||
const DOWN = "\eOB"
|
||||
const ALL = "a"
|
||||
const NONE = "n"
|
||||
const DONE = "d"
|
||||
|
||||
# Because the plugin selection dialog prints directly to stdin in the same way
|
||||
# as we do here, and our input prints happen first, we're going to need to insert
|
||||
# the plugin selection prints ourselves, and then "undo" the extra ones at the end
|
||||
# by consuming whatever is left in stdin.
|
||||
const NDEFAULTS = length(PT.default_plugins())
|
||||
const SELECT_DEFAULTS = (CR * DOWN)^NDEFAULTS
|
||||
|
||||
struct FromString
|
||||
s::String
|
||||
end
|
||||
|
||||
@testset "Interactive mode" begin
|
||||
@testset "Input conversion" begin
|
||||
generic(T, x) = PT.convert_input(PT.Plugin, T, x)
|
||||
@test generic(String, "foo") == "foo"
|
||||
@test generic(Float64, "1.23") == 1.23
|
||||
@test generic(Int, "01") == 1
|
||||
@test generic(Bool, "yes") === true
|
||||
@test generic(Bool, "True") === true
|
||||
@test generic(Bool, "No") === false
|
||||
@test generic(Bool, "false") === false
|
||||
@test generic(Vector{Int}, "1, 2, 3") == [1, 2, 3]
|
||||
@test generic(Vector{String}, "a, b,c") == ["a", "b", "c"]
|
||||
@test generic(FromString, "hello") == FromString("hello")
|
||||
if VERSION < v"1.1"
|
||||
@test_broken generic(Union{String, Nothing}, "nothing") === nothing
|
||||
else
|
||||
@test generic(Union{String, Nothing}, "nothing") === nothing
|
||||
end
|
||||
|
||||
@test_throws ArgumentError generic(Int, "hello")
|
||||
@test_throws ArgumentError generic(Float64, "hello")
|
||||
@test_throws ArgumentError generic(Bool, "hello")
|
||||
end
|
||||
|
||||
@testset "input_tips" begin
|
||||
@test isempty(PT.input_tips(Int))
|
||||
@test PT.input_tips(Vector{String}) == ["comma-delimited"]
|
||||
@test PT.input_tips(Union{Vector{String}, Nothing}) ==
|
||||
["'nothing' for nothing", "comma-delimited"]
|
||||
@test PT.input_tips(Union{String, Nothing}) == ["'nothing' for nothing"]
|
||||
@test PT.input_tips(Union{Vector{Secret}, Nothing}) ==
|
||||
["'nothing' for nothing", "comma-delimited", "name only"]
|
||||
end
|
||||
|
||||
@testset "Interactive name/type pair collection" begin
|
||||
name = gensym()
|
||||
@eval begin
|
||||
PT.@plugin struct $name <: PT.Plugin
|
||||
x::Int = 0
|
||||
y::String = ""
|
||||
end
|
||||
|
||||
@test PT.interactive_pairs($name) == [:x => Int, :y => String]
|
||||
PT.customizable(::Type{$name}) = (:x => PT.NotCustomizable, :y => Float64, :z => Int)
|
||||
@test PT.interactive_pairs($name) == [:y => Float64, :z => Int]
|
||||
end
|
||||
end
|
||||
|
||||
@testset "Simulated inputs" begin
|
||||
@testset "Default template" begin
|
||||
print(
|
||||
stdin.buffer,
|
||||
CR, # Select user
|
||||
DONE, # Finish menu
|
||||
USER, LF, # Enter user
|
||||
)
|
||||
@test Template(; interactive=true) == Template(; user=USER)
|
||||
end
|
||||
|
||||
@testset "Custom options, accept defaults" begin
|
||||
print(
|
||||
stdin.buffer,
|
||||
ALL, DONE, # Customize all fields
|
||||
"user", LF, # Enter user (don't assume we have default for this one).
|
||||
LF, # Enter authors
|
||||
LF, # Enter dir
|
||||
CR, # Enter host
|
||||
CR, # Enter julia
|
||||
SELECT_DEFAULTS, # Pre-select default plugins
|
||||
DONE, # Select no additional plugins
|
||||
DONE^NDEFAULTS, # Don't customize plugins
|
||||
)
|
||||
@test Template(; interactive=true) == Template(; user="user")
|
||||
readavailable(stdin.buffer)
|
||||
end
|
||||
|
||||
@testset "Custom options, custom values" begin
|
||||
nversions = VERSION.minor + 1
|
||||
print(
|
||||
stdin.buffer,
|
||||
ALL, DONE, # Customize all fields
|
||||
"user", LF, # Enter user
|
||||
"a, b", LF, # Enter authors
|
||||
"~", LF, # Enter dir
|
||||
DOWN^3, CR, # Choose "Other" for host
|
||||
"x.com", LF, # Enter host
|
||||
DOWN^nversions, CR, # Choose "Other" for julia
|
||||
"0.7", LF, # Enter Julia version
|
||||
SELECT_DEFAULTS, # Pre-select default plugins
|
||||
DONE, # Select no additional plugins
|
||||
DONE^NDEFAULTS, # Don't customize plugins
|
||||
)
|
||||
@test Template(; interactive=true) == Template(;
|
||||
user="user",
|
||||
authors=["a", "b"],
|
||||
dir="~",
|
||||
host="x.com",
|
||||
julia=v"0.7",
|
||||
)
|
||||
readavailable(stdin.buffer)
|
||||
end
|
||||
|
||||
@testset "Disabling default plugins" begin
|
||||
print(
|
||||
stdin.buffer,
|
||||
CR, DOWN^5, CR, DONE, # Customize user and plugins
|
||||
USER, LF, # Enter user
|
||||
SELECT_DEFAULTS, # Pre-select default plugins
|
||||
UP, CR, UP^2, CR, DONE, # Disable TagBot and Readme
|
||||
DONE^(NDEFAULTS - 2), # Don't customize plugins
|
||||
)
|
||||
@test Template(; interactive=true) == Template(;
|
||||
user=USER,
|
||||
plugins=[!Readme, !TagBot],
|
||||
)
|
||||
readavailable(stdin.buffer)
|
||||
end
|
||||
|
||||
@testset "Plugins" begin
|
||||
print(
|
||||
stdin.buffer,
|
||||
ALL, DONE, # Customize all fields
|
||||
"true", LF, # Enable ARM64
|
||||
"no", LF, # Disable coverage
|
||||
"1.1,v1.2", LF, # Enter extra versions
|
||||
"x.txt", LF, # Enter file
|
||||
"Yes", LF, # Enable Linux
|
||||
"false", LF, # Disable OSX
|
||||
"TRUE", LF, # Enable Windows
|
||||
"YES", LF, # Enable x64
|
||||
"NO", LF, # Disable x86
|
||||
)
|
||||
@test PT.interactive(TravisCI) == TravisCI(;
|
||||
arm64=true,
|
||||
coverage=false,
|
||||
extra_versions=[v"1.1", v"1.2"],
|
||||
file="x.txt",
|
||||
linux=true,
|
||||
osx=false,
|
||||
windows=true,
|
||||
x64=true,
|
||||
x86=false,
|
||||
)
|
||||
|
||||
print(
|
||||
stdin.buffer,
|
||||
DOWN^2, CR, # Select GitLabCI
|
||||
DOWN, CR, DONE, # Customize index_md
|
||||
"x.txt", LF, # Enter index file
|
||||
)
|
||||
@test PT.interactive(Documenter) == Documenter{GitLabCI}(; index_md="x.txt")
|
||||
|
||||
print(
|
||||
stdin.buffer,
|
||||
CR, DOWN, CR, DONE, # Customize name and destination
|
||||
"COPYING", LF, # Enter destination
|
||||
CR, # Choose MIT for name (it's at the top)
|
||||
)
|
||||
@test PT.interactive(License) == License(; destination="COPYING", name="MIT")
|
||||
end
|
||||
|
||||
@testset "Union{T, Nothing} weirdness" begin
|
||||
print(
|
||||
stdin.buffer,
|
||||
DOWN, CR, DONE, # Customize changelog
|
||||
"hello", LF, # Enter changelog
|
||||
)
|
||||
@test PT.interactive(TagBot) == TagBot(; changelog="hello")
|
||||
|
||||
print(
|
||||
stdin.buffer,
|
||||
DOWN, CR, DONE, # Customize changelog
|
||||
"nothing", LF, # Set to null
|
||||
)
|
||||
@test PT.interactive(TagBot) == TagBot(; changelog=nothing)
|
||||
end
|
||||
|
||||
println()
|
||||
end
|
||||
end
|
|
@ -1,5 +1,7 @@
|
|||
# Don't move this line from the top, please. {{X}} {{Y}} {{Z}}
|
||||
|
||||
@info "Running plugin tests"
|
||||
|
||||
struct FileTest <: PT.FilePlugin
|
||||
a::String
|
||||
b::Bool
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@info "Running reference tests"
|
||||
|
||||
const PROMPT = get(ENV, "PT_INTERACTIVE", "false") == "true" || !haskey(ENV, "CI")
|
||||
const STATIC_FILE = joinpath(@__DIR__, "fixtures", "static.txt")
|
||||
const STATIC_DOCUMENTER = [
|
||||
|
|
|
@ -4,7 +4,7 @@ using Base.Filesystem: path_separator
|
|||
using LibGit2: LibGit2, GitCommit, GitRemote, GitRepo
|
||||
using Pkg: Pkg, PackageSpec, TOML
|
||||
using Random: Random, randstring
|
||||
using Test: @test, @testset, @test_logs, @test_throws
|
||||
using Test: @test, @testset, @test_broken, @test_logs, @test_throws
|
||||
|
||||
using DeepDiffs: deepdiff
|
||||
using SimpleMock: mock
|
||||
|
@ -54,6 +54,7 @@ mktempdir() do dir
|
|||
include("template.jl")
|
||||
include("plugin.jl")
|
||||
include("show.jl")
|
||||
include("interactive.jl")
|
||||
|
||||
if PT.git_is_installed()
|
||||
include("git.jl")
|
||||
|
@ -64,7 +65,7 @@ mktempdir() do dir
|
|||
if VERSION.major == 1 && VERSION.minor == 4
|
||||
include("reference.jl")
|
||||
else
|
||||
@info "Skipping reference tests" julia=VERSION
|
||||
@info "Skipping reference tests" VERSION
|
||||
end
|
||||
else
|
||||
@info "Git is not installed, skipping Git and reference tests"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@info "Running show tests"
|
||||
|
||||
const TEMPLATES_DIR = contractuser(PT.TEMPLATES_DIR)
|
||||
const LICENSES_DIR = joinpath(TEMPLATES_DIR, "licenses")
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@info "Running template tests"
|
||||
|
||||
@testset "Template" begin
|
||||
@testset "Template constructor" begin
|
||||
@testset "user" begin
|
||||
|
|
Loading…
Reference in New Issue