Repository: scipy/scipy
Pull request: MAINT: stats._axis_nan_policy: raise appropriate TypeErrors
#### Reference issue
Closes gh-17713

#### What does this implement/fix?
Ensures that functions wrapped with `_axis_nan_policy` decorator raise common TypeErrors as appropriate.


Problem statement:
BUG: `_axis_nan_policy` changes some common `TypeError`s
### Describe your issue.

Functions wrapped with the `_axis_nan_policy` decorator give the correct error message when unrecognized arguments are passed and when multiple values are passed for the same argument:

<details>

```python3
from scipy import stats
stats.gmean([1, 2, 3], invalid_arg=1)
```
raises:
```
TypeError: gmean() got an unexpected keyword argument 'invalid_arg'
```
and
```python3
stats.gmean([1, 2, 3], a=True)
```
raises
```
TypeError: gmean() got multiple values for argument 'a'
```
</details>

---

However, it looks like some other common cases did not get the same treatment:

```python3
stats.gmean()  # requires an argument
```
raises:
```
KeyError: 'a'
```
when it should be:
```
TypeError: gmean() missing 1 required positional argument: 'a'
```

and

```python3
stats.gmean([1, 2, 3], 0, float, [1, 1, 1], 10, 20, 30)  # too many positional arguments
```
raises nothing, but it should raise

```
TypeError: gmean() takes from 1 to 4 positional arguments but 7 were given
```

Fortunately, it seems that this hasn't been terribly bothersome, as the bug has been present for a year without report. These sorts of issues should be quite easy to fix (if we can find them), so please let me know if you can find any others. I'll try to fix them in time for 1.10.1.

### Reproducing Code Example

```python
from scipy import stats
stats.gmean()  # requires an argument
```


### Error message

```shell
Traceback (most recent call last):

  File "C:\Users\matth\AppData\Local\Temp\ipykernel_52816\3198214520.py", line 1, in <module>
    stats.gmean()

  File "C:\Users\matth\scipy\scipy\stats\_axis_nan_policy.py", line 434, in axis_nan_policy_wrapper
    samples = [np.atleast_1d(kwds.pop(param))

  File "C:\Users\matth\scipy\scipy\stats\_axis_nan_policy.py", line 434, in <listcomp>
    samples = [np.atleast_1d(kwds.pop(param))

KeyError: 'a'
```


### SciPy/NumPy/Python version information

Since 1.8.0

Hints from issue discussion:
Incidentally, I discovered this when trying to use `ttest_rel` with one sample (the difference `a - b`), which should be allowed (as it is for `wilcoxon`), but isn't. I won't open a separate issue for it, but let's fix that, too.
Sir, I want to work on this issue. Could you please guide me how to fix this bug?
May I suggest resolving the request discussed at https://github.com/scipy/scipy/issues/17250#issuecomment-1282964647 instead? I am working on this one.

There are currently checks on what `options` are passed into `linprog` with `method='highs'`. All options should be passed into HiGHS, but a warning should be emitted when there are options that are not currently permitted. Please make it behave similar to `milp` in this situation and add a test that shows that the warning is emitted when an unrecognized option is used.

Commit message: MAINT: stats._axis_nan_policy: raise appropriate TypeErrors

Patch to review:
diff --git a/scipy/stats/_axis_nan_policy.py b/scipy/stats/_axis_nan_policy.py
index c44c90c36963..58752d7697d4 100644
--- a/scipy/stats/_axis_nan_policy.py
+++ b/scipy/stats/_axis_nan_policy.py
@@ -403,12 +403,17 @@ def axis_nan_policy_wrapper(*args, _no_deco=False, **kwds):
                 # Note that *args can't be provided as a keyword argument
                 params = [f"arg{i}" for i in range(len(args))] + params[1:]
 
+            # raise if there are too many positional args
+            maxargs = (np.inf if inspect.getfullargspec(hypotest_fun_in).varargs
+                       else len(inspect.getfullargspec(hypotest_fun_in).args))
+            if len(args) > maxargs:  # let the function raise the right error
+                hypotest_fun_in(*args, **kwds)
+
+            # raise if multiple values passed for same parameter
             d_args = dict(zip(params, args))
             intersection = set(d_args) & set(kwds)
-            if intersection:
-                message = (f"{hypotest_fun_in.__name__}() got multiple values "
-                           f"for argument '{list(intersection)[0]}'")
-                raise TypeError(message)
+            if intersection:  # let the function raise the right error
+                hypotest_fun_in(*args, **kwds)
 
             # Consolidate other positional and keyword args into `kwds`
             kwds.update(d_args)
@@ -442,8 +447,12 @@ def hypotest_fun_out(*samples, **kwds):
                     return hypotest_fun_in(*samples[:n_samp], **kwds)
 
             # Extract the things we need here
-            samples = [np.atleast_1d(kwds.pop(param))
-                       for param in (params[:n_samp] + kwd_samp)]
+            try:  # if something is missing
+                samples = [np.atleast_1d(kwds.pop(param))
+                           for param in (params[:n_samp] + kwd_samp)]
+            except KeyError:  # let the function raise the right error
+                # might need to revisit this if a required arg is not a "sample"
+                hypotest_fun_in(*args, **kwds)
             vectorized = True if 'axis' in params else False
             vectorized = vectorized and not override['vectorization']
             axis = kwds.pop('axis', default_axis)
diff --git a/scipy/stats/tests/test_axis_nan_policy.py b/scipy/stats/tests/test_axis_nan_policy.py
index f6ec81d54b4b..85be5f30bcd1 100644
--- a/scipy/stats/tests/test_axis_nan_policy.py
+++ b/scipy/stats/tests/test_axis_nan_policy.py
@@ -77,6 +77,7 @@ def unpack_ttest_result(res):
 # For some functions, nan_policy='propagate' should not just return NaNs
 override_propagate_funcs = {stats.mode}
 
+
 def _mixed_data_generator(n_samples, n_repetitions, axis, rng,
                           paired=False):
     # generate random samples to check the response of hypothesis tests to
@@ -1050,3 +1051,23 @@ def weighted_fun(a, **kwargs):
     if weighted_fun_name not in {'pmean', 'gmean'}:
         # _no_deco mean returns masked array, last element was masked
         np.testing.assert_allclose(res5.compressed(), res[~np.isnan(res)])
+
+def test_raise_invalid_args_g17713():
+    # other cases are handled in:
+    # test_axis_nan_policy_decorated_positional_axis - multiple values for arg
+    # test_axis_nan_policy_decorated_positional_args - unexpected kwd arg
+    message = "got an unexpected keyword argument"
+    with pytest.raises(TypeError, match=message):
+        stats.gmean([1, 2, 3], invalid_arg=True)
+
+    message = " got multiple values for argument"
+    with pytest.raises(TypeError, match=message):
+        stats.gmean([1, 2, 3], a=True)
+
+    message = "missing 1 required positional argument"
+    with pytest.raises(TypeError, match=message):
+        stats.gmean()
+
+    message = "takes from 1 to 4 positional arguments but 5 were given"
+    with pytest.raises(TypeError, match=message):
+        stats.gmean([1, 2, 3], 0, float, [1, 1, 1], 10)


Code review comments:
diff_hunk: @@ -403,12 +403,17 @@ def axis_nan_policy_wrapper(*args, _no_deco=False, **kwds):
                 # Note that *args can't be provided as a keyword argument
                 params = [f"arg{i}" for i in range(len(args))] + params[1:]
 
+            # raise if there are too many positional args
+            maxargs = (np.inf if inspect.getfullargspec(hypotest_fun_in).varargs
+                       else len(inspect.getfullargspec(hypotest_fun_in).args))
+            if len(args) > maxargs:  # let the function raise the right error
path: scipy/stats/_axis_nan_policy.py
text: @author:
```suggestion
            maxarg = (np.inf if inspect.getfullargspec(hypotest_fun_in).varargs
                      else len(inspect.getfullargspec(hypotest_fun_in).args))
            if len(args) > maxarg:  # let the function raise the right error
```