Skip to content

Commit

Permalink
Fall-back to constructors if convert fails
Browse files Browse the repository at this point in the history
Close #95
  • Loading branch information
carlobaldassi committed Feb 21, 2020
1 parent d0ac2b6 commit f9e00b9
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 27 deletions.
4 changes: 2 additions & 2 deletions docs/src/arg_table.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ This is the list of all available settings:
default is `:store_true`. See the section [Available actions and nargs values](@ref) for a list of all available actions and a
detailed explanation.
* `arg_type` (default = `Any`): the type of the argument. Only makes sense with non-flag arguments. Only works out-of-the-box with
string and number types, but see the section [Parsing to custom types](@ref) for details on how to make it work for general types
(including user-defined ones).
string, symbol and number types, but see the section [Parsing to custom types](@ref) for details on how to make it work for
general types (including user-defined ones).
* `default` (default = `nothing`): the default value if the option or positional argument is not parsed. Only makes sense with
non-flag arguments, or when the action is `:store_const` or `:append_const`. Unless it's `nothing`, it must be consistent with
`arg_type` and `range_tester`.
Expand Down
17 changes: 9 additions & 8 deletions docs/src/custom.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Parsing to custom types

If you specify an `arg_type` setting (see the [Argument entry settings](@ref) section) for an option
or an argument, `parse_args` will try to parse it, i.e. to convert the string to the specified type. This
only works for a limited number of types, which can either be directly constructed from strings or be parsed via
the Julia's built-in `parse` function. In order to extend this functionality to other types, including user-defined
custom types, you need to overload the `ArgParse.parse_item` function. Example:
If you specify an `arg_type` setting (see the [Argument entry settings](@ref) section) for an
option or an argument, `parse_args` will try to parse it, i.e. to convert the string to the
specified type. For `Number` types, Julia's built-in `parse` function will be used. For other
types, first `convert` and then the type's constructor will be tried. In order to extend this
functionality, e.g. to user-defined custom types, without adding methods to `convert` or the
constructor, you can overload the `ArgParse.parse_item` function. Example:

```julia
struct CustomType
Expand All @@ -16,6 +17,6 @@ function ArgParse.parse_item(::Type{CustomType}, x::AbstractString)
end
```

Note that the second argument needs to be of type `AbstractString` to avoid ambiguity warnings. Also note that if your
type is parametric (e.g. `CustomType{T}`), you need to overload the function like this:
`function ArgParse.parse_item(::Type{CustomType{T}}, x::AbstractString) where {T}`.
Note that the second argument needs to be of type `AbstractString` to avoid ambiguity errors. Also
note that if your type is parametric (e.g. `CustomType{T}`), you need to overload the function like
this: `function ArgParse.parse_item(::Type{CustomType{T}}, x::AbstractString) where {T}`.
22 changes: 11 additions & 11 deletions src/parsing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,29 @@ function check_settings_can_use_symbols(settings::ArgParseSettings)
end

# parsing aux functions
function parse_item_wrapper(it_type::Type, x::AbstractString)
local r::it_type
function parse_item_wrapper(::Type{T}, x::AbstractString) where {T}
local r::T
try
r = parse_item(it_type, x)
r = parse_item(T, x)
catch err
argparse_error("""
invalid argument: $x (conversion to type $it_type failed; you may need to overload
invalid argument: $x (conversion to type $T failed; you may need to overload
ArgParse.parse_item; the error was: $err)""")
end
return r
end

parse_item(it_type::Type{Any}, x::AbstractString) = x
parse_item(it_type::Type{T}, x::AbstractString) where {T<:Number} = parse(it_type, x)
parse_item(it_type::Type{T}, x::AbstractString) where {T} = convert(T, x)
parse_item(::Type{Any}, x::AbstractString) = x
parse_item(::Type{T}, x::AbstractString) where {T<:Number} = parse(T, x)
parse_item(::Type{T}, x::AbstractString) where {T} = applicable(convert, T, x) ? convert(T, x) : T(x)

function parse_item_eval(it_type::Type, x::AbstractString)
local r::it_type
function parse_item_eval(::Type{T}, x::AbstractString) where {T}
local r::T
try
r = convert(it_type, eval(Meta.parse(x)))
r = convert(T, eval(Meta.parse(x)))
catch err
argparse_error("""
invalid argument: $x (must evaluate or convert to type $it_type;
invalid argument: $x (must evaluate or convert to type $T;
the error was: $err)""")
end
return r
Expand Down
36 changes: 30 additions & 6 deletions test/argparse_test02.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ function ap_settings2()
default = 0 # this is used when the option is not passed
constant = 1 # this is used if --opt1 is paseed with no argument
help = "an option"
"-O"
arg_type = Symbol
default = :xyz
help = "another option"
"--flag", "-f"
action = :store_true # this makes it a flag
help = "a flag"
Expand Down Expand Up @@ -58,6 +62,10 @@ function ap_settings2b()
:default => 0, # this is used when the option is not passed
:constant => 1, # this is used if --opt1 is paseed with no argument
:help => "an option"),
["-O"], Dict(
:arg_type => Symbol,
:default => :xyz,
:help => "another option"),
["--flag", "-f"], Dict(
:action => :store_true, # this makes it a flag
:help => "a flag"),
Expand Down Expand Up @@ -96,6 +104,10 @@ function ap_settings2c()
, default = 0 # this is used when the option is not passed
, constant = 1 # this is used if --opt1 is paseed with no argument
, help = "an option"
, "-O"
, arg_type = Symbol
, default = :xyz
, help = "another option"
, ["--flag", "-f"]
, action = :store_true # this makes it a flag
, help = "a flag"
Expand Down Expand Up @@ -134,6 +146,10 @@ function ap_settings2d()
default = 0; # this is used when the option is not passed
constant = 1; # this is used if --opt1 is paseed with no argument
help = "an option"),
("-O";
arg_type = Symbol;
default = :xyz;
help = "another option"),
(["--flag", "-f"];
action = :store_true; # this makes it a flag
help = "a flag")
Expand Down Expand Up @@ -174,6 +190,12 @@ function ap_settings2e()
constant = 1 # this is used if --opt1 is paseed with no argument
help = "an option"
end,
"-O",
begin
arg_type = Symbol
default = :xyz
help = "another option"
end,
["--flag", "-f"],
begin
action = :store_true # this makes it a flag
Expand Down Expand Up @@ -208,7 +230,7 @@ for s = [ap_settings2(), ap_settings2b(), ap_settings2c(), ap_settings2d(), ap_s
ap_test2(args) = parse_args(args, s)

@test stringhelp(s) == """
usage: $(basename(Base.source_path())) [--opt1 [OPT1]] [-f] [-k] arg1 arg1
usage: $(basename(Base.source_path())) [--opt1 [OPT1]] [-O O] [-f] [-k] arg1 arg1
[arg2...]
Test 2 for ArgParse.jl
Expand All @@ -220,6 +242,7 @@ for s = [ap_settings2(), ap_settings2b(), ap_settings2c(), ap_settings2d(), ap_s
optional arguments:
--opt1 [OPT1] an option (type: $Int, default: 0, without arg: 1)
-O O another option (type: $Symbol, default: :xyz)
-f, --flag a flag
-k, --karma increase karma
Expand All @@ -230,11 +253,11 @@ for s = [ap_settings2(), ap_settings2b(), ap_settings2c(), ap_settings2d(), ap_s
@test stringversion(s) == "Version 1.0\n"

@ap_test_throws ap_test2([])
@test ap_test2(["X", "Y"]) == Dict{String,Any}("opt1"=>0, "flag"=>false, "karma"=>0, "arg1"=>Any["X", "Y"], "arg2"=>Any["no_arg_given"])
@test ap_test2(["X", "Y", "-k", "-f", "Z", "--karma", "--opt"]) == Dict{String,Any}("opt1"=>1, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["X", "Y", "--opt", "-k", "-f", "Z", "--karma"]) == Dict{String,Any}("opt1"=>1, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["X", "Y", "--opt", "--karma", "-f", "Z", "-k"]) == Dict{String,Any}("opt1"=>1, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["--opt", "-3", "X", "Y", "-k", "-f", "Z", "--karma"]) == Dict{String,Any}("opt1"=>-3, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["X", "Y"]) == Dict{String,Any}("opt1"=>0, "O"=>:xyz, "flag"=>false, "karma"=>0, "arg1"=>Any["X", "Y"], "arg2"=>Any["no_arg_given"])
@test ap_test2(["X", "Y", "-k", "-f", "Z", "--karma", "--opt"]) == Dict{String,Any}("opt1"=>1, "O"=>:xyz, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["X", "Y", "--opt", "-k", "-f", "Z", "--karma"]) == Dict{String,Any}("opt1"=>1, "O"=>:xyz, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["X", "Y", "--opt", "--karma", "-O", "XYZ", "-f", "Z", "-k"]) == Dict{String,Any}("opt1"=>1, "O"=>:XYZ, "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@test ap_test2(["--opt", "-3", "X", "Y", "-k", "-f", "Z", "-O", "a b c", "--karma"]) == Dict{String,Any}("opt1"=>-3, "O"=>Symbol("a b c"), "flag"=>true, "karma"=>2, "arg1"=>Any["X", "Y"], "arg2"=>Any["Z"])
@ap_test_throws ap_test2(["--opt"])
@ap_test_throws ap_test2(["--opt="])
@ap_test_throws ap_test2(["--opt", "", "X", "Y"])
Expand All @@ -243,6 +266,7 @@ for s = [ap_settings2(), ap_settings2b(), ap_settings2c(), ap_settings2d(), ap_s
@ee_test_throws @add_arg_table!(s, "required_arg_after_optional_args", required = true)
# wrong default
@ee_test_throws @add_arg_table!(s, "--opt", arg_type = Int, default = 1.5)
@ee_test_throws @add_arg_table!(s, "--opt3", arg_type = Symbol, default = "string")
# wrong range tester
@ee_test_throws @add_arg_table!(s, "--opt", arg_type = Int, range_tester = x->string(x), default = 1)
@ee_test_throws @add_arg_table!(s, "--opt", arg_type = Int, range_tester = x->sqrt(x)<1, default = -1)
Expand Down

0 comments on commit f9e00b9

Please sign in to comment.