changeset 32022:5945e1bd73ea

inputParser.m: Handle validation functions which either return true/false or throw error (bug #49793) * inputParser.m (parse): Replace direct call to validating function ("valid_option = opt.val (in)") with call to subfunction validate_arg() which correctly handles all validation functions. * inputParser.m (validate_arg): Change function signature to have a true/false output. Replace multiple nested try/catch blocks with single try/catch block for performance and correctness (validation functions are not guaranteed to be re-entrant). If nargout > 1, return true/false output (new behavior). Otherwise, perform existing behavior (current invocations of function expect this). * inputParser.m: Add BIST test for bug #49793.
author Rik <rik@octave.org>
date Sat, 15 Apr 2023 07:17:15 -0700
parents da2954782945
children bce1850f8104
files scripts/miscellaneous/inputParser.m
diffstat 1 files changed, 38 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/scripts/miscellaneous/inputParser.m	Sat Apr 15 11:29:15 2023 +0200
+++ b/scripts/miscellaneous/inputParser.m	Sat Apr 15 07:17:15 2023 -0700
@@ -442,19 +442,15 @@
           vidx -= 1;
           break;
         endif
-        try
-          valid_option = opt.val (in);
-        catch
-          valid_option = false;
-        end_try_catch
+        valid_option = this.validate_arg ("", opt.val, in);
         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
+          ## If it does not match there are two options:
+          ##   1a) it's a Parameter or Switch name and we should use
           ##       the default for the rest;
-          ##    3) it's a struct with the Parameter pairs.
-          if (ischar (in) || (this.StructExpand && isstruct (in)
-                              && isscalar (in)))
+          ##   1b) it's a struct with the Parameter pairs.
+          ##   2) input is actually wrong and we should error;
+          if (ischar (in)
+              || (this.StructExpand && isstruct (in) && isscalar (in)))
             idx -= 1;
             vidx -= 1;
             break;
@@ -560,32 +556,31 @@
 
     endfunction
 
-    function validate_arg (this, name, val, in)
+    function r = validate_arg (this, name, val, in)
 
-      ## Checking "nargout (val)" doesn't work for builtin functions.
-      ## So, we need to use this nested try-catch construct.
+      ## Validation function can either produce a true/false result or have
+      ## no outputs but throw an error when failing.  Tricky code here
+      ## relies on side effect, but calls validation function only once
+      ## which is a performance win and may also be necessary if validation
+      ## function maintains state.  See bug #49793.
       err = sprintf ('Checked with "%s"', func2str (val));
+      ans = true;
       try
-        ok = val (in);
+        val (in);  # call function with no arguments in case nargout == 0
+        ok = ans;  # use side effect of assignment to 'ans' when nargout == 1
       catch exception
-        if (strcmp (exception.identifier, "Octave:invalid-fun-call"))
-          ## check if function also fails when called without output argument
-          try
-            val (in);
-            ok = true;
-          catch exception
-            ok = false;
-            err = exception.message;
-          end_try_catch
-        else
-          ok = false;
-          err = exception.message;
+        ok = false;
+        err = exception.message;
+      end_try_catch
+
+      if (nargout > 0)
+        r = ok;
+      else
+        if (! ok)
+          this.error (sprintf ("failed validation of '%s'.  %s", name, err));
         endif
-      end_try_catch
-      if (! ok)
-        this.error (sprintf ("failed validation of '%s'.  %s", name, err));
+        this.Results.(name) = in;
       endif
-      this.Results.(name) = in;
 
     endfunction
 
@@ -938,3 +933,15 @@
 %! p.addParameter ('a',1);
 %! p.parse (30, 'b', 20, 'a',10);
 %! assert (fieldnames (p.Results), {'a'; 'b'; 'c'; 'z'});
+
+%!test <*49793>
+%! p = inputParser ();
+%! p.addRequired ("name", @(x) validateattributes (x, {'char'}, {'nonempty'}));
+%! p.addOptional ("year", 0001, @(x) validateattributes (x, {'numeric'}, ...
+%!                                     {'nonempty','integer','positive'}));
+%! p.addParameter ("color", '-', @(x) validateattributes (x, {'char'}, ...
+%!                                                           {'nonempty'}));
+%! p.parse ('Jim', 1980, 'color', 'black');
+%! assert (p.Results.name, 'Jim');
+%! assert (p.Results.year, 1980);
+%! #assert (p.Results.color, 'black');