A bunch of experimental interactive code

This needs some good tests, but it seems to be mostly working in the
REPL! Some general cleanup is needed, too.
This commit is contained in:
Chris de Graaf 2019-09-26 23:02:02 +07:00
parent 3f886525a7
commit 3a9c9634e6
No known key found for this signature in database
GPG Key ID: 150FFDD9B0073C7B
11 changed files with 148 additions and 74 deletions

View File

@ -16,15 +16,6 @@ else
show_field(x::AbstractString) = repr(contractuser(x)) show_field(x::AbstractString) = repr(contractuser(x))
end end
"""
interactive(::Type{T<:Plugin}) -> T
Create a [`Plugin`](@ref) of type `T` interactively from user input.
"""
function interactive(::Type{T}) where T <: Plugin
return T() # TODO
end
function Base.show(io::IO, m::MIME"text/plain", t::Template) function Base.show(io::IO, m::MIME"text/plain", t::Template)
println(io, "Template:") println(io, "Template:")
foreach(fieldnames(Template)) do n foreach(fieldnames(Template)) do n
@ -41,6 +32,29 @@ function Base.show(io::IO, m::MIME"text/plain", t::Template)
end end
end end
"""
interactive(::Type{T<:Plugin}) -> T
Create a [`Plugin`](@ref) of type `T` interactively from user input.
"""
function interactive(::Type{T}) where T <: Plugin
kwargs = Dict{Symbol, Any}()
foreach(fieldnames(T)) do name
F = fieldtype(T, name)
v = Val(name)
required = !applicable(defaultkw, T, v)
default = required ? defaultkw(F) : defaultkw(T, v)
kwargs[name] = if applicable(prompt, T, v)
prompt(F, "$T: $(prompt(T, v))", default, required=required)
else
prompt(F, "$T: Value for field '$name' ($F)", default; required=required)
end
end
return T(; kwargs...)
end
leaves(T::Type) = isconcretetype(T) ? [T] : vcat(map(leaves, subtypes(T))...) leaves(T::Type) = isconcretetype(T) ? [T] : vcat(map(leaves, subtypes(T))...)
function plugin_types() function plugin_types()
@ -56,28 +70,34 @@ function Template(::Val{true}; kwargs...)
opts = Dict{Symbol, Any}(kwargs) opts = Dict{Symbol, Any}(kwargs)
if !haskey(opts, :user) if !haskey(opts, :user)
opts[:user] = prompt(String, "Git hosting service username", defaultkw(:user)) default = defaultkw(Template, :user)
opts[:user] = prompt(String, "Git hosting service username", default)
end end
if !haskey(opts, :host) if !haskey(opts, :host)
opts[:host] = prompt(String, "Git hosting service URL", defaultkw(:host)) default = defaultkw(Template, :host)
opts[:host] = prompt(String, "Git hosting service URL", default)
end end
if !haskey(opts, :authors) if !haskey(opts, :authors)
opts[:authors] = prompt(String, "Package author(s)", defaultkw(:authors)) default = defaultkw(Template, :authors)
opts[:authors] = prompt(String, "Package author(s)", default)
end end
if !haskey(opts, :dir) if !haskey(opts, :dir)
opts[:dir] = prompt(String, "Path to package parent directory", defaultkw(:dir)) default = defaultkw(Template, :dir)
opts[:dir] = prompt(String, "Path to package parent directory", default)
end end
if !haskey(opts, :julia) if !haskey(opts, :julia)
opts[:julia] = prompt(VersionNumber, "Supported Julia version", defaultkw(:julia)) default = defaultkw(Template, :julia)
opts[:julia] = prompt(VersionNumber, "Supported Julia version", default)
end end
if !haskey(opts, :disable_defaults) if !haskey(opts, :disable_defaults)
available = map(typeof, default_plugins()) available = map(typeof, default_plugins())
opts[:disable_defaults] = select("Select defaults to disable:", available, []) initial = defaultkw(Template, :disable_defaults)
opts[:disable_defaults] = select("Select defaults to disable:", available, initial)
end end
if !haskey(opts, :plugins) if !haskey(opts, :plugins)
@ -91,20 +111,35 @@ function Template(::Val{true}; kwargs...)
return Template(Val(false); opts...) return Template(Val(false); opts...)
end end
function prompt(::Type{String}, s::AbstractString, default; required::Bool=false) defaultkw(::Type{String}) = ""
defaultkw(::Type{Union{T, Nothing}}) where T = nothing
defaultkw(::Type{T}) where T <: Number = zero(T)
defaultkw(::Type{Vector{T}}) where T = T[]
function prompt(
::Type{<:Union{String, Nothing}}, s::AbstractString, default;
required::Bool=false,
)
default isa AbstractString && (default = contractuser(default)) default isa AbstractString && (default = contractuser(default))
default_display = required ? "REQUIRED" : repr(default) default_display = if required
"REQUIRED"
elseif default === nothing
"None"
else
repr(default)
end
print("$s [$default_display]: ") print("$s [$default_display]: ")
input = strip(readline()) input = strip(readline())
return if isempty(input) && required return if isempty(input) && required
println("This option is required") println("This option is required")
prompt(String, s, default; required=required) prompt(String, s, default; required=required)
elseif isempty(input) elseif isempty(input)
default default
else else
input input
end end
end end
function prompt( function prompt(
@ -123,11 +158,19 @@ end
function prompt(::Type{Bool}, s::AbstractString, default::Bool; required::Bool=false) function prompt(::Type{Bool}, s::AbstractString, default::Bool; required::Bool=false)
b = prompt(String, s, default; required=required) b = prompt(String, s, default; required=required)
return uppercase(b) in ("Y", "YES", "T", "TRUE") return b === default ? default : uppercase(b) in ("Y", "YES", "T", "TRUE")
end end
function prompt(::Type{Vector{String}}, s::AbstractString, default::Vector{<:AbstractString}) function prompt(::Type{Vector}, s::AbstractString, default::Vector; required::Bool=false)
# TODO return prompt(Vector{String}, s, default; required=required)
end
function prompt(
::Type{Vector{String}}, s::AbstractString, default::Vector{<:AbstractString};
required::Bool=false,
)
s = prompt(String, "$s (comma-delimited)", join(default, ", "); required=required)
return convert(Vector{String}, map(strip, split(s, ","; keepempty=false)))
end end
# TODO: These can be made simpler when this is merged: # TODO: These can be made simpler when this is merged:
@ -144,8 +187,8 @@ function select(f::Function, s::AbstractString, xs::Vector, initial::Vector)
end end
# Select one item frm oa collection. # Select one item frm oa collection.
function select(f::Function, s::AbstractString, xs::Vector, _initial) function select(f::Function, s::AbstractString, xs::Vector, initial)
# Can't use the initial value yet. print(stdin.buffer, repeat("\e[B", findfirst(==(initial), xs) - 1))
selection = request(s, RadioMenu(map(f, xs))) selection = request("$s:", RadioMenu(map(f, xs); pagesize=length(xs)))
return xs[selection] return xs[selection]
end end

View File

@ -235,6 +235,11 @@ If you are implementing a plugin that uses the `user` field of a [`Template`](@r
""" """
needs_username(::Plugin) = false needs_username(::Plugin) = false
function prompt end
iscall(x, ::Symbol) = false
iscall(ex::Expr, s::Symbol) = ex.head === :call && ex.args[1] === s
""" """
@with_defaults struct T #= ... =# end @with_defaults struct T #= ... =# end
@ -253,13 +258,28 @@ end
``` ```
""" """
macro with_defaults(ex::Expr) macro with_defaults(ex::Expr)
T = esc(ex.args[2].args[1]) T = esc(ex.args[2].args[1]) # This assumes T <: U.
# TODO: Parse out `<- "prompt"` stuff. # This is a bit nasty.
funcs = map(filter(arg -> arg isa Expr && arg.head === :(=), ex.args[3].args)) do arg funcs = Expr[]
name = QuoteNode(arg.args[1].args[1]) foreach(filter(arg -> arg isa Expr, ex.args[3].args)) do arg
val = arg.args[2] if iscall(arg, :<) && iscall(arg.args[3], :-) # x::T <- "prompt"
:(PkgTemplates.defaultkw(::Type{$T}, ::Val{$name}) = $(esc(arg.args[2]))) name = QuoteNode(arg.args[2].args[1])
prompt = arg.args[2]
push!(funcs, :(PkgTemplates.prompt(::Type{$T}, ::Val{$name}) = $(esc(prompt))))
elseif arg.head === :(=)
rhs = arg.args[2]
if iscall(rhs, :<) && iscall(rhs.args[3], :-) # x::T = "foo" <- "prompt"
name = QuoteNode(arg.args[1].args[1])
prompt = rhs.args[3].args[2]
default = arg.args[2] = rhs.args[2]
push!(
funcs,
:(PkgTemplates.prompt(::Type{$T}, ::Val{$name}) = $(esc(prompt))),
:(PkgTemplates.defaultkw(::Type{$T}, ::Val{$name}) = $(esc(default))),
)
end
end
end end
return Expr(:block, esc(with_kw(ex, __module__, false)), funcs...) return Expr(:block, esc(with_kw(ex, __module__, false)), funcs...)

View File

@ -45,13 +45,13 @@ Integrates your packages with [Travis CI](https://travis-ci.com).
$EXTRA_VERSIONS_DOC $EXTRA_VERSIONS_DOC
""" """
@with_defaults struct TravisCI <: BasicPlugin @with_defaults struct TravisCI <: BasicPlugin
file::String = default_file("travis.yml") file::String = default_file("travis.yml") <- "Path to .travis.yml template"
linux::Bool = true linux::Bool = true <- "Enable Linux bulds"
osx::Bool = true osx::Bool = true <- "Enable OSX builds"
windows::Bool = true windows::Bool = true <- "Enable Windows builds"
x86::Bool = false x86::Bool = false <- "Enable 32-bit builds"
coverage::Bool = true coverage::Bool = true <- "Enable code coverage submission"
extra_versions::Vector = DEFAULT_CI_VERSIONS extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test"
end end
source(p::TravisCI) = p.file source(p::TravisCI) = p.file
@ -114,10 +114,10 @@ Integrates your packages with [AppVeyor](https://appveyor.com) via [AppVeyor.jl]
$EXTRA_VERSIONS_DOC $EXTRA_VERSIONS_DOC
""" """
@with_defaults struct AppVeyor <: BasicPlugin @with_defaults struct AppVeyor <: BasicPlugin
file::String = default_file("appveyor.yml") file::String = default_file("appveyor.yml") <- "Path to .appveyor.yml template"
x86::Bool = false x86::Bool = false <- "Enable 32-bit builds"
coverage::Bool = true coverage::Bool = true <- "Enable code coverage submission"
extra_versions::Vector = DEFAULT_CI_VERSIONS extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test"
end end
source(p::AppVeyor) = p.file source(p::AppVeyor) = p.file
@ -167,10 +167,10 @@ $EXTRA_VERSIONS_DOC
Code coverage submission from Cirrus CI is not yet supported by [Coverage.jl](https://github.com/JuliaCI/Coverage.jl). Code coverage submission from Cirrus CI is not yet supported by [Coverage.jl](https://github.com/JuliaCI/Coverage.jl).
""" """
@with_defaults struct CirrusCI <: BasicPlugin @with_defaults struct CirrusCI <: BasicPlugin
file::String = default_file("cirrus.yml") file::String = default_file("cirrus.yml") <- "Path to .cirrus.yml template"
image::String = "freebsd-12-0-release-amd64" image::String = "freebsd-12-0-release-amd64" <- "FreeBSD image"
coverage::Bool = true coverage::Bool = true <- "Enable code coverage submission"
extra_versions::Vector = DEFAULT_CI_VERSIONS extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test"
end end
source(p::CirrusCI) = p.file source(p::CirrusCI) = p.file
@ -216,10 +216,10 @@ See [`Documenter`](@ref) for more information.
Nightly Julia is not supported. Nightly Julia is not supported.
""" """
@with_defaults struct GitLabCI <: BasicPlugin @with_defaults struct GitLabCI <: BasicPlugin
file::String = default_file("gitlab-ci.yml") file::String = default_file("gitlab-ci.yml") <- "Path to .gitlab-ci.yml template"
coverage::Bool = true coverage::Bool = true <- "Enable code coverage submission"
# Nightly has no Docker image. # Nightly has no Docker image.
extra_versions::Vector = map(format_version, [default_version(), VERSION]) extra_versions::Vector = map(format_version, [default_version(), VERSION]) <- "Extra Julia versions to test"
end end
gitignore(p::GitLabCI) = p.coverage ? COVERAGE_GITIGNORE : String[] gitignore(p::GitLabCI) = p.coverage ? COVERAGE_GITIGNORE : String[]

View File

@ -8,8 +8,8 @@ Creates a `CITATION.bib` file for citing package repositories.
- `readme::Bool`: Whether or not to include a section about citing in the README. - `readme::Bool`: Whether or not to include a section about citing in the README.
""" """
@with_defaults struct Citation <: BasicPlugin @with_defaults struct Citation <: BasicPlugin
file::String = default_file("CITATION.bib") file::String = default_file("CITATION.bib") <- "Path to CITATION.bib template"
readme::Bool = false readme::Bool = false <- """Enable "Citing" README section"""
end end
tags(::Citation) = "<<", ">>" tags(::Citation) = "<<", ">>"

View File

@ -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. - `file::Union{AbstractString, Nothing}`: Template file for `.codecov.yml`, or `nothing` to create no file.
""" """
@with_defaults struct Codecov <: BasicPlugin @with_defaults struct Codecov <: BasicPlugin
file::Union{String, Nothing} = nothing file::Union{String, Nothing} = nothing <- "Path to .codecov.yml template"
end end
source(p::Codecov) = p.file source(p::Codecov) = p.file
@ -30,7 +30,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. - `file::Union{AbstractString, Nothing}`: Template file for `.coveralls.yml`, or `nothing` to create no file.
""" """
@with_defaults struct Coveralls <: BasicPlugin @with_defaults struct Coveralls <: BasicPlugin
file::Union{String, Nothing} = nothing file::Union{String, Nothing} = nothing <- "Path to .coveralls.yml template"
end end
source(p::Coveralls) = p.file source(p::Coveralls) = p.file

View File

@ -13,10 +13,10 @@ 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. This option requires that the Git CLI is installed, and for you to have a GPG key associated with your committer identity.
""" """
@with_defaults struct Git <: Plugin @with_defaults struct Git <: Plugin
ignore::Vector{String} = [] ignore::Vector{String} = String[] <- "Gitignore entries"
ssh::Bool = false ssh::Bool = false <- "Enable SSH Git remote"
manifest::Bool = false manifest::Bool = false <- "Commit Manifest.toml"
gpgsign::Bool = false gpgsign::Bool = false <- "GPG-sign commits"
end end
# Try to make sure that no files are created after we commit. # Try to make sure that no files are created after we commit.

View File

@ -16,7 +16,7 @@ struct License <: BasicPlugin
destination::String destination::String
end end
function License( function License(;
name::AbstractString="MIT", name::AbstractString="MIT",
path::Union{AbstractString, Nothing}=nothing, path::Union{AbstractString, Nothing}=nothing,
destination::AbstractString="LICENSE", destination::AbstractString="LICENSE",
@ -28,10 +28,21 @@ function License(
return License(path, destination) return License(path, destination)
end end
source(p::License) = p.path source(p::License) = p.path
destination(p::License) = p.destination destination(p::License) = p.destination
view(::License, t::Template, ::AbstractString) = Dict( view(::License, t::Template, ::AbstractString) = Dict(
"AUTHORS" => join(t.authors, ", "), "AUTHORS" => join(t.authors, ", "),
"YEAR" => year(today()), "YEAR" => year(today()),
) )
function interactive(::Type{License})
destination = prompt(String, "License: License file destination", "LICENSE")
return if prompt(Bool, "License: Use custom license file", false)
path = prompt(String, "License: Path to custom license file", ""; required=true)
License(; path=path, destination=destination)
else
available = sort(readdir(joinpath(TEMPLATES_DIR, "licenses")))
name = select("License: Select a license", available, "MIT")
License(; name=name, destination=destination)
end
end

View File

@ -15,9 +15,9 @@ By default, it includes badges for other included plugins
- `inline_badges::Bool`: Whether or not to put the badges on the same line as the package name. - `inline_badges::Bool`: Whether or not to put the badges on the same line as the package name.
""" """
@with_defaults struct Readme <: BasicPlugin @with_defaults struct Readme <: BasicPlugin
file::String = default_file("README.md") file::String = default_file("README.md") <- "Path to README.md template"
destination::String = "README.md" destination::String = "README.md" <- "README file destination"
inline_badges::Bool = false inline_badges::Bool = false <- "Enable inline badges"
end end
source(p::Readme) = p.file source(p::Readme) = p.file

View File

@ -7,7 +7,7 @@ Creates a module entrypoint.
- `file::AbstractString`: Template file for `src/<module>.jl`. - `file::AbstractString`: Template file for `src/<module>.jl`.
""" """
@with_defaults mutable struct SrcDir <: BasicPlugin @with_defaults mutable struct SrcDir <: BasicPlugin
file::String = default_file("src", "module.jl") file::String = default_file("src", "module.jl") <- "Path to src/<module>.jl template"
destination::String = joinpath("src", "<module>.jl") destination::String = joinpath("src", "<module>.jl")
end end

View File

@ -15,8 +15,8 @@ Sets up testing for packages.
Managing test dependencies with `test/Project.toml` is only supported in Julia 1.2 and later. Managing test dependencies with `test/Project.toml` is only supported in Julia 1.2 and later.
""" """
@with_defaults struct Tests <: BasicPlugin @with_defaults struct Tests <: BasicPlugin
file::String = default_file("test", "runtests.jl") file::String = default_file("test", "runtests.jl") <- "Path to runtests.jl template"
project::Bool = false project::Bool = false <- "Enable test/Project.toml"
end end
source(p::Tests) = p.file source(p::Tests) = p.file

View File

@ -127,14 +127,14 @@ function getplugin(t::Template, ::Type{T}) where T <: Plugin
end end
# Get a keyword, or compute some default value. # Get a keyword, or compute some default value.
getkw(kwargs, k) = get(() -> defaultkw(k), kwargs, k) getkw(kwargs, k) = get(() -> defaultkw(Template, k), kwargs, k)
# Default Template keyword values. # Default Template keyword values.
defaultkw(s::Symbol) = defaultkw(Val(s)) defaultkw(::Type{T}, s::Symbol) where T = defaultkw(T, Val(s))
defaultkw(::Val{:authors}) = default_authors() defaultkw(::Type{Template}, ::Val{:authors}) = default_authors()
defaultkw(::Val{:dir}) = Pkg.devdir() defaultkw(::Type{Template}, ::Val{:dir}) = Pkg.devdir()
defaultkw(::Val{:disable_defaults}) = DataType[] defaultkw(::Type{Template}, ::Val{:disable_defaults}) = DataType[]
defaultkw(::Val{:host}) = "github.com" defaultkw(::Type{Template}, ::Val{:host}) = "github.com"
defaultkw(::Val{:julia}) = default_version() defaultkw(::Type{Template}, ::Val{:julia}) = default_version()
defaultkw(::Val{:plugins}) = Plugin[] defaultkw(::Type{Template}, ::Val{:plugins}) = Plugin[]
defaultkw(::Val{:user}) = default_user() defaultkw(::Type{Template}, ::Val{:user}) = default_user()