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))
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)
println(io, "Template:")
foreach(fieldnames(Template)) do n
@ -41,6 +32,29 @@ function Base.show(io::IO, m::MIME"text/plain", t::Template)
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))...)
function plugin_types()
@ -56,28 +70,34 @@ function Template(::Val{true}; kwargs...)
opts = Dict{Symbol, Any}(kwargs)
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
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
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
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
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
if !haskey(opts, :disable_defaults)
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
if !haskey(opts, :plugins)
@ -91,11 +111,27 @@ function Template(::Val{true}; kwargs...)
return Template(Val(false); opts...)
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_display = required ? "REQUIRED" : repr(default)
default_display = if required
"REQUIRED"
elseif default === nothing
"None"
else
repr(default)
end
print("$s [$default_display]: ")
input = strip(readline())
return if isempty(input) && required
println("This option is required")
prompt(String, s, default; required=required)
@ -104,7 +140,6 @@ function prompt(::Type{String}, s::AbstractString, default; required::Bool=false
else
input
end
end
function prompt(
@ -123,11 +158,19 @@ end
function prompt(::Type{Bool}, s::AbstractString, default::Bool; required::Bool=false)
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
function prompt(::Type{Vector{String}}, s::AbstractString, default::Vector{<:AbstractString})
# TODO
function prompt(::Type{Vector}, s::AbstractString, default::Vector; required::Bool=false)
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
# 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
# Select one item frm oa collection.
function select(f::Function, s::AbstractString, xs::Vector, _initial)
# Can't use the initial value yet.
selection = request(s, RadioMenu(map(f, xs)))
function select(f::Function, s::AbstractString, xs::Vector, initial)
print(stdin.buffer, repeat("\e[B", findfirst(==(initial), xs) - 1))
selection = request("$s:", RadioMenu(map(f, xs); pagesize=length(xs)))
return xs[selection]
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
function prompt end
iscall(x, ::Symbol) = false
iscall(ex::Expr, s::Symbol) = ex.head === :call && ex.args[1] === s
"""
@with_defaults struct T #= ... =# end
@ -253,13 +258,28 @@ end
```
"""
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.
funcs = map(filter(arg -> arg isa Expr && arg.head === :(=), ex.args[3].args)) do arg
# This is a bit nasty.
funcs = Expr[]
foreach(filter(arg -> arg isa Expr, ex.args[3].args)) do arg
if iscall(arg, :<) && iscall(arg.args[3], :-) # x::T <- "prompt"
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])
val = arg.args[2]
:(PkgTemplates.defaultkw(::Type{$T}, ::Val{$name}) = $(esc(arg.args[2])))
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
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
"""
@with_defaults struct TravisCI <: BasicPlugin
file::String = default_file("travis.yml")
linux::Bool = true
osx::Bool = true
windows::Bool = true
x86::Bool = false
coverage::Bool = true
extra_versions::Vector = DEFAULT_CI_VERSIONS
file::String = default_file("travis.yml") <- "Path to .travis.yml template"
linux::Bool = true <- "Enable Linux bulds"
osx::Bool = true <- "Enable OSX builds"
windows::Bool = true <- "Enable Windows builds"
x86::Bool = false <- "Enable 32-bit builds"
coverage::Bool = true <- "Enable code coverage submission"
extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test"
end
source(p::TravisCI) = p.file
@ -114,10 +114,10 @@ Integrates your packages with [AppVeyor](https://appveyor.com) via [AppVeyor.jl]
$EXTRA_VERSIONS_DOC
"""
@with_defaults struct AppVeyor <: BasicPlugin
file::String = default_file("appveyor.yml")
x86::Bool = false
coverage::Bool = true
extra_versions::Vector = DEFAULT_CI_VERSIONS
file::String = default_file("appveyor.yml") <- "Path to .appveyor.yml template"
x86::Bool = false <- "Enable 32-bit builds"
coverage::Bool = true <- "Enable code coverage submission"
extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test"
end
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).
"""
@with_defaults struct CirrusCI <: BasicPlugin
file::String = default_file("cirrus.yml")
image::String = "freebsd-12-0-release-amd64"
coverage::Bool = true
extra_versions::Vector = DEFAULT_CI_VERSIONS
file::String = default_file("cirrus.yml") <- "Path to .cirrus.yml template"
image::String = "freebsd-12-0-release-amd64" <- "FreeBSD image"
coverage::Bool = true <- "Enable code coverage submission"
extra_versions::Vector = DEFAULT_CI_VERSIONS <- "Extra Julia versions to test"
end
source(p::CirrusCI) = p.file
@ -216,10 +216,10 @@ See [`Documenter`](@ref) for more information.
Nightly Julia is not supported.
"""
@with_defaults struct GitLabCI <: BasicPlugin
file::String = default_file("gitlab-ci.yml")
coverage::Bool = true
file::String = default_file("gitlab-ci.yml") <- "Path to .gitlab-ci.yml template"
coverage::Bool = true <- "Enable code coverage submission"
# 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
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.
"""
@with_defaults struct Citation <: BasicPlugin
file::String = default_file("CITATION.bib")
readme::Bool = false
file::String = default_file("CITATION.bib") <- "Path to CITATION.bib template"
readme::Bool = false <- """Enable "Citing" README section"""
end
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.
"""
@with_defaults struct Codecov <: BasicPlugin
file::Union{String, Nothing} = nothing
file::Union{String, Nothing} = nothing <- "Path to .codecov.yml template"
end
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.
"""
@with_defaults struct Coveralls <: BasicPlugin
file::Union{String, Nothing} = nothing
file::Union{String, Nothing} = nothing <- "Path to .coveralls.yml template"
end
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.
"""
@with_defaults struct Git <: Plugin
ignore::Vector{String} = []
ssh::Bool = false
manifest::Bool = false
gpgsign::Bool = false
ignore::Vector{String} = String[] <- "Gitignore entries"
ssh::Bool = false <- "Enable SSH Git remote"
manifest::Bool = false <- "Commit Manifest.toml"
gpgsign::Bool = false <- "GPG-sign commits"
end
# Try to make sure that no files are created after we commit.

View File

@ -16,7 +16,7 @@ struct License <: BasicPlugin
destination::String
end
function License(
function License(;
name::AbstractString="MIT",
path::Union{AbstractString, Nothing}=nothing,
destination::AbstractString="LICENSE",
@ -28,10 +28,21 @@ function License(
return License(path, destination)
end
source(p::License) = p.path
destination(p::License) = p.destination
view(::License, t::Template, ::AbstractString) = Dict(
"AUTHORS" => join(t.authors, ", "),
"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.
"""
@with_defaults struct Readme <: BasicPlugin
file::String = default_file("README.md")
destination::String = "README.md"
inline_badges::Bool = false
file::String = default_file("README.md") <- "Path to README.md template"
destination::String = "README.md" <- "README file destination"
inline_badges::Bool = false <- "Enable inline badges"
end
source(p::Readme) = p.file

View File

@ -7,7 +7,7 @@ Creates a module entrypoint.
- `file::AbstractString`: Template file for `src/<module>.jl`.
"""
@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")
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.
"""
@with_defaults struct Tests <: BasicPlugin
file::String = default_file("test", "runtests.jl")
project::Bool = false
file::String = default_file("test", "runtests.jl") <- "Path to runtests.jl template"
project::Bool = false <- "Enable test/Project.toml"
end
source(p::Tests) = p.file

View File

@ -127,14 +127,14 @@ function getplugin(t::Template, ::Type{T}) where T <: Plugin
end
# 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.
defaultkw(s::Symbol) = defaultkw(Val(s))
defaultkw(::Val{:authors}) = default_authors()
defaultkw(::Val{:dir}) = Pkg.devdir()
defaultkw(::Val{:disable_defaults}) = DataType[]
defaultkw(::Val{:host}) = "github.com"
defaultkw(::Val{:julia}) = default_version()
defaultkw(::Val{:plugins}) = Plugin[]
defaultkw(::Val{:user}) = default_user()
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{:disable_defaults}) = DataType[]
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()