Repository: matplotlib/matplotlib

Issue:
[ENH]: AttributeError('... got an unexpected keyword argument ...') should set the .name attribute to the keyword
### Problem

I am in the process to write a library that uses the matplotlib library for its graphics needs. As such, most of "our" graphics functions are wrappers around existing matplotlib functions such as tripcolor, tricontour… A typical function of ours would be:

```
def fun_name_altered(lib_objects, **kwargs):
    args = prepare_args(lib_objects, **kwargs)
    plt.fun_name(*args, **kwargs)
```

In this case the `kwargs` dictionnary may contain keywords aimed at the `prepare_args` process and keywords aimed at controlling the plotting. At the moment, `prepare_args` removes keywords from the dictionnary when used so that in principle, when `kwargs` arrive at `plt.fun_name`, it should only contain keywords known to `plt.fun_name`

However, if the user makes a mistake while setting the keyword for OUR part, it is not properly handled by `prepare_args` and it does remain inside `kwargs` and then is passed to `plt.fun_name` which then raises an `AttributeError('... got an unexpected keyword argument ...')`.

I would like to be able to try and recover if that happens.

### Proposed solution

From the  [Built-in Exceptions documentation page](https://docs.python.org/3/library/exceptions.html#bltin-exceptions)

> The name and obj attributes can be set using keyword-only arguments to the constructor. When set they represent the name of the attribute that was attempted to be accessed and the object that was accessed for said attribute, respectively.
Changed in version 3.10: Added the name and obj attributes

I looked at the matplotlib codebase but I am not an expert programmer so I may be wrong but I think what I would like was that `Artist._update_props`  set the `name` attribute to `k` when raising the `AttributeError`.

Since I am using Python 3.11 this would allow me to try and remove spurious keywords.


Discussion:
This seems reasonable to me.  My understanding is the same and it should just be a case of adding `name=k` here:

https://github.com/matplotlib/matplotlib/blob/0da2da00d5a884db9c7991c31ee81d1b5dc57b63/lib/matplotlib/artist.py#L1193-L1194

This will break for python 3.9 but, according to the [NEP29 schedule](https://numpy.org/neps/nep-0029-deprecation_policy.html#support-table), we should be dropping support for that in April anyway.  So if we target this for Matplotlib 3.10 there will be no problem.



Patch:
diff --git a/doc/users/next_whats_new/exception_prop_name.rst b/doc/users/next_whats_new/exception_prop_name.rst
new file mode 100644
index 000000000000..c887b879393c
--- /dev/null
+++ b/doc/users/next_whats_new/exception_prop_name.rst
@@ -0,0 +1,26 @@
+Exception handling control
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The exception raised when an invalid keyword parameter is passed now includes
+that parameter name as the exception's ``name`` property.  This provides more
+control for exception handling:
+
+
+.. code-block:: python
+
+    import matplotlib.pyplot as plt
+
+    def wobbly_plot(args, **kwargs):
+        w = kwargs.pop('wobble_factor', None)
+
+        try:
+            plt.plot(args, **kwargs)
+        except AttributeError as e:
+            raise AttributeError(f'wobbly_plot does not take parameter {e.name}') from e
+
+
+    wobbly_plot([0, 1], wibble_factor=5)
+
+.. code-block::
+
+    AttributeError: wobbly_plot does not take parameter wibble_factor
diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py
index baf3b01ee6e5..345a61bfc16a 100644
--- a/lib/matplotlib/artist.py
+++ b/lib/matplotlib/artist.py
@@ -1190,7 +1190,8 @@ def _update_props(self, props, errfmt):
         Helper for `.Artist.set` and `.Artist.update`.
 
         *errfmt* is used to generate error messages for invalid property
-        names; it gets formatted with ``type(self)`` and the property name.
+        names; it gets formatted with ``type(self)`` for "{cls}" and the
+        property name for "{prop_name}".
         """
         ret = []
         with cbook._setattr_cm(self, eventson=False):
@@ -1203,7 +1204,8 @@ def _update_props(self, props, errfmt):
                     func = getattr(self, f"set_{k}", None)
                     if not callable(func):
                         raise AttributeError(
-                            errfmt.format(cls=type(self), prop_name=k))
+                            errfmt.format(cls=type(self), prop_name=k),
+                            name=k)
                     ret.append(func(v))
         if ret:
             self.pchanged()


Test patch:
diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
index be988c31ee75..f519b42098e5 100644
--- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
+++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
@@ -1501,8 +1501,9 @@ def test_calling_conventions(self):
             ax.voxels(x, y)
         # x, y, z are positional only - this passes them on as attributes of
         # Poly3DCollection
-        with pytest.raises(AttributeError):
+        with pytest.raises(AttributeError, match="keyword argument 'x'") as exec_info:
             ax.voxels(filled=filled, x=x, y=y, z=z)
+        assert exec_info.value.name == 'x'
 
 
 def test_line3d_set_get_data_3d():