Mercurial > octave
view scripts/general/inputParser.m @ 23333:b636f10ce53f
fix display methods to only expect one argument
* @ftp/display.m, @audioplayer/display.m, @audiorecorder/display.m:
Accept one argument only. Use inputname to get object name.
* inputParser.m (display): Likewise.
author | John W. Eaton <jwe@octave.org> |
---|---|
date | Wed, 29 Mar 2017 14:32:14 -0400 |
parents | 092078913d54 |
children | f04f32f08590 |
line wrap: on
line source
## Copyright (C) 2011-2017 Carnë Draug ## ## This file is part of Octave. ## ## Octave is free software; you can redistribute it and/or modify it ## under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 3 of the License, or ## (at your option) any later version. ## ## Octave is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Octave; see the file COPYING. If not, see ## <http://www.gnu.org/licenses/>. ## -*- texinfo -*- ## @deftypefn {} {@var{p} =} inputParser () ## Create object @var{p} of the inputParser class. ## ## This class is designed to allow easy parsing of function arguments. The ## class supports four types of arguments: ## ## @enumerate ## @item mandatory (see @command{addRequired}); ## ## @item optional (see @command{addOptional}); ## ## @item named (see @command{addParameter}); ## ## @item switch (see @command{addSwitch}). ## @end enumerate ## ## After defining the function API with these methods, the supplied arguments ## can be parsed with the @command{parse} method and the parsing results ## accessed with the @command{Results} accessor. ## ## @end deftypefn ## @deftypefn {} {} inputParser.Parameters ## Return list of parameter names already defined. ## ## @end deftypefn ## @deftypefn {} {} inputParser.Results ## Return structure with argument names as fieldnames and corresponding values. ## ## @end deftypefn ## @deftypefn {} {} inputParser.Unmatched ## Return structure similar to @command{Results}, but for unmatched parameters. ## See the @command{KeepUnmatched} property. ## ## @end deftypefn ## @deftypefn {} {} inputParser.UsingDefaults ## Return cell array with the names of arguments that are using default values. ## ## @end deftypefn ## @deftypefn {} {} inputParser.CaseSensitive = @var{boolean} ## Set whether matching of argument names should be case sensitive. Defaults ## to false. ## ## @end deftypefn ## @deftypefn {} {} inputParser.FunctionName = @var{name} ## Set function name to be used in error messages; Defaults to empty string. ## ## @end deftypefn ## @deftypefn {} {} inputParser.KeepUnmatched = @var{boolean} ## Set whether an error should be given for non-defined arguments. Defaults to ## false. If set to true, the extra arguments can be accessed through ## @code{Unmatched} after the @code{parse} method. Note that since ## @command{Switch} and @command{Parameter} arguments can be mixed, it is ## not possible to know the unmatched type. If argument is found unmatched ## it is assumed to be of the @command{Parameter} type and it is expected to ## be followed by a value. ## ## @end deftypefn ## @deftypefn {} {} inputParser.StructExpand = @var{boolean} ## Set whether a structure can be passed to the function instead of ## parameter/value pairs. Defaults to true. ## ## The following example shows how to use this class: ## ## @example ## @group ## function check (varargin) ## @c The next two comments need to be indented by one for alignment ## p = inputParser (); # create object ## p.FunctionName = "check"; # set function name ## p.addRequired ("pack", @@ischar); # mandatory argument ## p.addOptional ("path", pwd(), @@ischar); # optional argument ## ## ## create a function handle to anonymous functions for validators ## val_mat = @@(x) isvector (x) && all (x <= 1) && all (x >= 0); ## p.addOptional ("mat", [0 0], val_mat); ## ## ## create two arguments of type "Parameter" ## val_type = @@(x) any (strcmp (x, @{"linear", "quadratic"@})); ## p.addParameter ("type", "linear", val_type); ## val_verb = @@(x) any (strcmp (x, @{"low", "medium", "high"@})); ## p.addParameter ("tolerance", "low", val_verb); ## ## ## create a switch type of argument ## p.addSwitch ("verbose"); ## ## p.parse (varargin@{:@}); # Run created parser on inputs ## ## ## the rest of the function can access inputs by using p.Results. ## ## for example, get the tolerance input with p.Results.tolerance ## endfunction ## @end group ## @end example ## ## @example ## @group ## check ("mech"); # valid, use defaults for other arguments ## check (); # error, one argument is mandatory ## check (1); # error, since ! ischar ## check ("mech", "~/dev"); # valid, use defaults for other arguments ## ## check ("mech", "~/dev", [0 1 0 0], "type", "linear"); # valid ## ## ## following is also valid. Note how the Switch argument type can ## ## be mixed into or before the Parameter argument type (but it ## ## must still appear after any Optional argument). ## check ("mech", "~/dev", [0 1 0 0], "verbose", "tolerance", "high"); ## ## ## following returns an error since not all optional arguments, ## ## `path' and `mat', were given before the named argument `type'. ## check ("mech", "~/dev", "type", "linear"); ## @end group ## @end example ## ## @emph{Note 1}: A function can have any mixture of the four API types but ## they must appear in a specific order. @command{Required} arguments must be ## first and can be followed by any @command{Optional} arguments. Only ## the @command{Parameter} and @command{Switch} arguments may be mixed ## together and they must appear at the end. ## ## @emph{Note 2}: If both @command{Optional} and @command{Parameter} arguments ## are mixed in a function API then once a string Optional argument fails to ## validate it will be considered the end of the @command{Optional} ## arguments. The remaining arguments will be compared against any ## @command{Parameter} or @command{Switch} arguments. ## ## @seealso{nargin, validateattributes, validatestring, varargin} ## @end deftypefn ## -*- texinfo -*- ## @deftypefn {} {} addOptional (@var{argname}, @var{default}) ## @deftypefnx {} {} addOptional (@var{argname}, @var{default}, @var{validator}) ## Add new optional argument to the object @var{parser} of the class ## inputParser to implement an ordered arguments type of API ## ## @var{argname} must be a string with the name of the new argument. The order ## in which new arguments are added with @command{addOptional}, represents the ## expected order of arguments. ## ## @var{default} will be the value used when the argument is not specified. ## ## @var{validator} is an optional anonymous function to validate the given ## values for the argument with name @var{argname}. Alternatively, a ## function name can be used. ## ## See @command{help inputParser} for examples. ## ## @emph{Note}: if a string argument does not validate, it will be considered a ## ParamValue key. If an optional argument is not given a validator, anything ## will be valid, and so any string will be considered will be the value of the ## optional argument (in @sc{matlab}, if no validator is given and argument is ## a string it will also be considered a ParamValue key). ## ## @end deftypefn ## -*- texinfo -*- ## @deftypefn {} {} addParameter (@var{argname}, @var{default}) ## @deftypefnx {} {} addParameter (@var{argname}, @var{default}, @var{validator}) ## Add new parameter to the object @var{parser} of the class inputParser to ## implement a name/value pair type of API. ## ## @var{argname} must be a string with the name of the new parameter. ## ## @var{default} will be the value used when the parameter is not specified. ## ## @var{validator} is an optional function handle to validate the given values ## for the parameter with name @var{argname}. Alternatively, a function name ## can be used. ## ## See @command{help inputParser} for examples. ## ## @end deftypefn ## -*- texinfo -*- ## @deftypefn {} {} addParamValue (@var{argname}, @var{default}) ## @deftypefnx {} {} addParamValue (@var{argname}, @var{default}, @var{validator}) ## Add new parameter to the object @var{parser} of the class inputParser to ## implement a name/value pair type of API. ## ## This is an alias for @command{addParameter} method without the ## @qcode{"PartialMatchPriority"} option. See it for the help text. ## ## @end deftypefn ## -*- texinfo -*- ## @deftypefn {} {} addRequired (@var{argname}) ## @deftypefnx {} {} addRequired (@var{argname}, @var{validator}) ## Add new mandatory argument to the object @var{parser} of inputParser class. ## ## This method belongs to the inputParser class and implements an ordered ## arguments type of API. ## ## @var{argname} must be a string with the name of the new argument. The order ## in which new arguments are added with @command{addrequired}, represents the ## expected order of arguments. ## ## @var{validator} is an optional function handle to validate the given values ## for the argument with name @var{argname}. Alternatively, a function name ## can be used. ## ## See @command{help inputParser} for examples. ## ## @emph{Note}: this can be used together with the other type of arguments but ## it must be the first (see @command{@@inputParser}). ## ## @end deftypefn ## -*- texinfo -*- ## @deftypefn {} {} addSwitch (@var{argname}) ## Add new switch type of argument to the object @var{parser} of inputParser ## class. ## ## This method belongs to the inputParser class and implements a switch ## arguments type of API. ## ## @var{argname} must be a string with the name of the new argument. Arguments ## of this type can be specified at the end, after @code{Required} and ## @code{Optional}, and mixed between the @code{Parameter}. They default to ## false. If one of the arguments supplied is a string like @var{argname}, ## then after parsing the value of @var{parse}.Results.@var{argname} will be ## true. ## ## See @command{help inputParser} for examples. ## ## @end deftypefn ## -*- texinfo -*- ## @deftypefn {} {} parse (@var{varargin}) ## Parses and validates list of arguments according to object @var{parser} of ## the class inputParser. ## ## After parsing, the results can be accessed with the @command{Results} ## accessor. See @command{help inputParser} for a more complete description. ## ## @end deftypefn ## Author: Carnë Draug <carandraug@octave.org> classdef inputParser < handle properties ## FIXME: set input checking for these properties CaseSensitive = false; FunctionName = ""; KeepUnmatched = false; PartialMatching = false; # FIXME: unimplemented (and default should be true) StructExpand = true; endproperties properties (SetAccess = protected) Parameters = cell (); Results = struct (); Unmatched = struct (); UsingDefaults = cell (); endproperties properties (Access = protected) ## Since Required and Optional are ordered, they get a cell array of ## structs with the fields "name", "def" (default), and "val" (validator). Required = cell (); Optional = cell (); ## Parameter and Switch are unordered so we have a struct whose fieldnames ## are the argname, and values are a struct with fields "def" and "val" Parameter = struct (); Switch = struct (); ## List of Parameter and Switch names to ease searches ParameterNames = cell (); SwitchNames = cell (); ## When checking for fieldnames in a Case Insensitive way, this variable ## holds the correct identifier for the last searched named using the ## is_argname method. last_name = ""; endproperties properties (Access = protected, Constant = true) ## Default validator, always returns scalar true. def_val = @() true; endproperties methods function set.PartialMatching (this, val) if (val) error ("inputParser: PartialMatching is not yet implemented"); endif endfunction function addRequired (this, name, val = inputParser.def_val) if (nargin < 2 || nargin > 3) print_usage (); elseif (numel (this.Optional) || numfields (this.Parameter) || numfields (this.Switch)) error (["inputParser.addRequired: can't have a Required argument " ... "after Optional, Parameter, or Switch"]); endif this.validate_name ("Required", name); this.Required{end+1} = struct ("name", name, "val", val); endfunction function addOptional (this, name, def, val = inputParser.def_val) if (nargin < 3 || nargin > 4) print_usage (); elseif (numfields (this.Parameter) || numfields (this.Switch)) error (["inputParser.Optional: can't have Optional arguments " ... "after Parameter or Switch"]); endif this.validate_name ("Optional", name); this.Optional{end+1} = struct ("name", name, "def", def, "val", val); endfunction function addParamValue (this, name, def, val = inputParser.def_val) if (nargin < 3 || nargin > 4) print_usage (); endif this.addParameter (name, def, val); endfunction function addParameter (this, name, def, varargin) if (nargin < 3 || nargin > 6) print_usage (); endif n_opt = numel (varargin); if (n_opt == 0 || n_opt == 2) val = inputParser.def_val; else # n_opt is 1 or 3 val = varargin{1}; endif if (n_opt == 0 || n_opt == 1) match_priority = 1; else # n_opt is 2 or 3 if (! strcmpi (varargin{end-1}, "PartialMatchPriority")) error ("inputParser.addParameter: unrecognized option"); endif match_priority = varargin{end}; validateattributes (match_priority, {"numeric"}, {"positive", "integer"}, "inputParser.addParameter", "PartialMatchPriority"); endif this.validate_name ("Parameter", name); this.Parameter.(name).def = def; this.Parameter.(name).val = val; endfunction function addSwitch (this, name) if (nargin != 2) print_usage (); endif this.validate_name ("Switch", name); this.Switch.(name).def = false; endfunction function parse (this, varargin) this.Results = struct (); this.Unmatched = struct (); this.UsingDefaults = cell (); if (numel (varargin) < numel (this.Required)) if (this.FunctionName) print_usage (this.FunctionName); else this.error ("inputParser.parse: not enough input arguments"); endif endif pnargin = numel (varargin); this.ParameterNames = fieldnames (this.Parameter); this.SwitchNames = fieldnames (this.Switch); ## Evaluate the Required arguments first nReq = numel (this.Required); for idx = 1:nReq req = this.Required{idx}; this.validate_arg (req.name, req.val, varargin{idx}); endfor vidx = nReq; # current index in varargin ## Search for a list of Optional arguments idx = 0; # current index on the array of Optional nOpt = numel (this.Optional); while (vidx < pnargin && idx < nOpt) opt = this.Optional{++idx}; in = varargin{++vidx}; try valid_option = opt.val (in); catch valid_option = false; end_try_catch if (! valid_option) ## If it does not match there's two options: ## 1) input is actually wrong and we should error; ## 2) it's a Parameter or Switch name and we should use the ## the default for the rest. ## 3) it's a struct with the Parameter pairs. if (ischar (in) || (this.StructExpand && isstruct (in) && isscalar (in))) idx -= 1; vidx -= 1; break else this.error (sprintf (["failed validation of %s\n", ... "Validation function: %s"], toupper (opt.name), disp(opt.val))); endif endif this.Results.(opt.name) = in; endwhile ## Fill in with defaults of missing Optional while (idx++ < nOpt) opt = this.Optional{idx}; this.UsingDefaults{end+1} = opt.name; this.Results.(opt.name) = opt.def; endwhile ## Search unordered Options (Switch and Parameter) while (vidx++ < pnargin) name = varargin{vidx}; if (this.StructExpand && isstruct (name) && isscalar (name)) expanded_options = [fieldnames(name) struct2cell(name)]'(:); n_new_args = numel (expanded_options) -1; pnargin += n_new_args; varargin(vidx+n_new_args+1:pnargin) = varargin(vidx+1:end); varargin(vidx:vidx+n_new_args) = expanded_options; name = varargin{vidx}; endif if (this.is_argname ("Parameter", name)) if (vidx++ > pnargin) this.error (sprintf ("no matching value for option '%s'", toupper (name))); endif this.validate_arg (this.last_name, this.Parameter.(this.last_name).val, varargin{vidx}); elseif (this.is_argname ("Switch", name)) this.Results.(this.last_name) = true; else if (vidx++ < pnargin && this.KeepUnmatched) this.Unmatched.(name) = varargin{vidx}; else this.error (sprintf ("argument '%s' is not a valid parameter", toupper (name))); endif endif endwhile ## Add them to the UsingDeafults list this.add_missing ("Parameter"); this.add_missing ("Switch"); endfunction function display (this) if (nargin != 1) print_usage (); endif printf ("%s = inputParser object with properties:\n\n", inputname ()); b2s = @(x) ifelse (any (x), "true", "false"); printf ([" CaseSensitive : %s\n FunctionName : %s\n" ... " KeepUnmatched : %s\n PartialMatching : %s\n" ... " StructExpand : %s\n\n"], b2s (this.CaseSensitive), b2s (this.FunctionName), b2s (this.KeepUnmatched), b2s (this.PartialMatching), b2s (this.StructExpand)); printf ("Defined parameters:\n\n {%s}\n", strjoin (this.Parameters, ", ")); endfunction endmethods methods (Access = private) function validate_name (this, type, name) if (! isvarname (name)) error ("inputParser.add%s: NAME is an invalid identifier", method); elseif (any (strcmpi (this.Parameters, name))) ## Even if CaseSensitive is "on", we still shouldn't allow ## two args with the same name. error ("inputParser.add%s: argname '%s' has already been specified", type, name); endif this.Parameters{end+1} = name; endfunction function validate_arg (this, name, val, in) if (! val (in)) this.error (sprintf ("failed validation of %s with %s", toupper (name), func2str (val))); endif this.Results.(name) = in; endfunction function r = is_argname (this, type, name) if (this.CaseSensitive) r = isfield (this.(type), name); this.last_name = name; else fnames = this.([type "Names"]); l = strcmpi (name, fnames); r = any (l(:)); if (r) this.last_name = fnames{l}; endif endif endfunction function add_missing (this, type) unmatched = setdiff (fieldnames (this.(type)), fieldnames (this.Results)); for namec = unmatched(:)' name = namec{1}; this.UsingDefaults{end+1} = name; this.Results.(name) = this.(type).(name).def; endfor endfunction function error (this, msg) where = ""; if (this.FunctionName) where = [this.FunctionName ": "]; endif error ("%s%s", where, msg); endfunction endmethods endclassdef %!function p = create_p () %! p = inputParser (); %! p.CaseSensitive = true; %! p.addRequired ("req1", @(x) ischar (x)); %! p.addOptional ("op1", "val", @(x) any (strcmp (x, {"val", "foo"}))); %! p.addOptional ("op2", 78, @(x) x > 50); %! p.addSwitch ("verbose"); %! p.addParameter ("line", "tree", @(x) any (strcmp (x, {"tree", "circle"}))); %!endfunction ## check normal use, only required are given %!test %! p = create_p (); %! p.parse ("file"); %! r = p.Results; %! assert (r.req1, "file"); %! assert (sort (p.UsingDefaults), sort ({"op1", "op2", "verbose", "line"})); %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, %! {"file", "val", 78, false, "tree"}); ## check normal use, but give values different than defaults %!test %! p = create_p (); %! p.parse ("file", "foo", 80, "line", "circle", "verbose"); %! r = p.Results; %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, %! {"file", "foo", 80, true, "circle"}); ## check optional is skipped and considered Parameter if unvalidated string %!test %! p = create_p (); %! p.parse ("file", "line", "circle"); %! r = p.Results; %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, %! {"file", "val", 78, false, "circle"}); ## check case insensitivity %!test %! p = create_p (); %! p.CaseSensitive = false; %! p.parse ("file", "foo", 80, "LiNE", "circle", "vERbOSe"); %! r = p.Results; %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, %! {"file", "foo", 80, true, "circle"}); ## check KeepUnmatched %!test %! p = create_p (); %! p.KeepUnmatched = true; %! p.parse ("file", "foo", 80, "line", "circle", "verbose", "extra", 50); %! assert (p.Unmatched.extra, 50); ## check error when missing required %!error <not enough input arguments> %! p = create_p (); %! p.parse (); ## check error when given required does not validate %!error <failed validation of > %! p = create_p (); %! p.parse (50); ## check error when given optional does not validate %!error <is not a valid parameter> %! p = create_p (); %! p.parse ("file", "no-val"); ## check error when given Parameter does not validate %!error <failed validation of > %! p = create_p (); %! p.parse ("file", "foo", 51, "line", "round"); ## check alternative method (obj, ...) API %!function p2 = create_p2 (); %! p2 = inputParser; %! addRequired (p2, "req1", @(x) ischar (x)); %! addOptional (p2, "op1", "val", @(x) any (strcmp (x, {"val", "foo"}))); %! addOptional (p2, "op2", 78, @(x) x > 50); %! addSwitch (p2, "verbose"); %! addParameter (p2, "line", "tree", @(x) any (strcmp (x, {"tree", "circle"}))); %!endfunction ## check normal use, only required are given %!test %! p2 = create_p2 (); %! parse (p2, "file"); %! r = p2.Results; %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, %! {"file", "val", 78, false, "tree"}); %! assert (sort (p2.UsingDefaults), sort ({"op1", "op2", "verbose", "line"})); ## check normal use, but give values different than defaults %!test %! p2 = create_p2 (); %! parse (p2, "file", "foo", 80, "line", "circle", "verbose"); %! r = p2.Results; %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, %! {"file", "foo", 80, true, "circle"}); ## We must not perform validation of default values %!test <45837> %! p = inputParser; %! p.addParameter ("Dir", [], @ischar); %! p.parse (); %! assert (p.Results.Dir, []); %!test %! p = inputParser; %! p.addParameter ("positive", -1, @(x) x > 5); %! p.parse (); %! assert (p.Results.positive, -1); ## Throw an error on validation of optional argument to check that it ## is caught without preventing continuation into param/value pairs. %!test %! p = inputParser (); %! p.addOptional ("err", "foo", @error); %! p.addParameter ("not_err", "bar", @ischar); %! p.parse ("not_err", "qux"); %! assert (p.Results.err, "foo") %! assert (p.Results.not_err, "qux") ## With more Parameters to test StructExpand %!function p3 = create_p3 (); %! p3 = inputParser; %! addOptional (p3, "op1", "val", @(x) any (strcmp (x, {"val", "foo"}))); %! addOptional (p3, "op2", 78, @(x) x > 50); %! addSwitch (p3, "verbose"); %! addParameter (p3, "line", "tree", @(x) any (strcmp (x, {"tree", "circle"}))); %! addParameter (p3, "color", "red", @(x) any (strcmp (x, {"red", "green"}))); %! addParameter (p3, "style", "tt", @(x) any (strcmp (x, {"tt", "f", "i"}))); %!endfunction ## Test StructExpand %!test %! p3 = create_p3 (); %! p3.parse (struct ("line", "circle", "color", "green")); %! assert (p3.Results, struct ("op1", "val", "op2", 78, "verbose", false, %! "line", "circle", "color", "green", %! "style", "tt")) %!test %! p3 = create_p3 (); %! p3.parse (struct ("line", "circle", "color", "green"), "line", "tree"); %! assert (p3.Results.line, "tree") %! p3.parse ("line", "tree", struct ("line", "circle", "color", "green")); %! assert (p3.Results.line, "circle") %!test # unmatched parameters with StructExpand %! p3 = create_p3 (); %! p3.KeepUnmatched = true; %! p3.parse (struct ("line", "circle", "color", "green", "bar", "baz")); %! assert (p3.Unmatched.bar, "baz") ## The validation for the second optional argument throws an error with ## a struct so check that we can handle it. %!test %! p3 = create_p3 (); %! p3.parse ("foo", struct ("color", "green"), "line", "tree"); %! assert (p3.Results.op1, "foo") %! assert (p3.Results.line, "tree") %! assert (p3.Results.color, "green") %! assert (p3.Results.verbose, false) ## Some simple tests for addParamValue since all the other ones use add ## addParameter but they use the same codepath. %!test %! p = inputParser; %! addParameter (p, "line", "tree", @(x) any (strcmp (x, {"tree", "circle"}))); %! addParameter (p, "color", "red", @(x) any (strcmp (x, {"red", "green"}))); %! p.parse ("line", "circle"); %! assert ({p.Results.line, p.Results.color}, {"circle", "red"}) %!test %! p = inputParser; %! p.addParameter ("foo", "bar", @ischar); %! p.parse (); %! assert (p.Results, struct ("foo", "bar")) %! p.parse ("foo", "qux"); %! assert (p.Results, struct ("foo", "qux")) ## FIXME: This somehow works in Matlab %!xtest %! p = inputParser; %! p.addOptional ("op1", "val"); %! p.addParameter ("line", "tree"); %! p.parse ("line", "circle"); %! assert (p.Results, struct ("op1", "val", "line", "circle")); %!test %! p = inputParser; %! p.addParameter ("a", []); %! p.addParameter ("b", []); %! p.parse ("a", 1); %! p.parse ("b", 1); %! assert (p.Results, struct ("a", [], "b", 1)); %! assert (p.UsingDefaults, {"a"}); %!test %! p = inputParser; %! p.addParameter ("b", []); %! p.KeepUnmatched = true; %! p.parse ("a", 1); %! p.parse ("b", 1); %! assert (p.Results, struct ("b", 1)); %! assert (p.Unmatched, struct ()); ## Test for patch #9241 %!error<failed validation of A with ischar> %! p = inputParser; %! p.addParameter ("a", [], @ischar); %! p.parse ("a", 1);