Repository: matplotlib/matplotlib
Pull request: Add explicit signatures for pyplot.{polar,savefig,set_loglevel}
<!--
Thank you so much for your PR!  To help us review your contribution, please check
out the development guide https://matplotlib.org/devdocs/devel/index.html
-->

## PR summary
This PR update `lib/matplotlib/pyplot.py`to replace `*args, **kwargs` with the real signature for the following methods:
- `savefig`
- `set_loglevel`
- `polar`


## PR checklist
- [x] Closes #30160 
- [x] Changed code is [tested](https://matplotlib.org/devdocs/devel/testing.html)
- [x] New tests created

Problem statement:
[MNT]: pyplot type hints
### Summary

Is there any reason why some functions in `pyplot.py` use `*args` and `**kwargs` as their only parameters (e.g. show, figlegend, or savefig) and therefore have untyped parameters?

### Proposed fix

Specify the actual signature or type parameters with Unpack[TypedDict] and Unpack[tuple] (however, parameter names are lost here)

Hints from issue discussion:
It depends: in the majority of cases e.g. `savefig`, `figlegend` these are customized wrappers and `*args, **kwargs` are only shorthands to pass though all parameters. You could spell them out explicitly at the cost of having to ensure consistency manually whenever the signature changes. There's a part of pyplot wrappers that we auto-generate using `boilerplate.py`. Their signatures are automatically spelled out. You could add a similar mechanism for the above cases.

`show()` is different in that the supported parameters depend on the backend. Since third party backends can define additional parameters, its signature is truly dynamic.
If you are worried about typing, presumably you are writing scripts and/or library code not working in an interactive shell.  If that is case, than you may be better off using the explicit Axes API (https://matplotlib.org/stable/users/explain/figure/api_interfaces.html#the-explicit-axes-interface) instead of the implicit pyplot API.
I looked at `boilerplate.py` and I think I can improve it to adding any methods (like `xlim` or `ylim`)
Regarding typing methods based on the backend, why not use `@overload` to define the possible signature (as indicated in the documentation or in the `_Backend` class) ?

For example with show:
```py
@overload
def show(*, block: bool | None = None) -> None:...

@overload
def show(*args: Any, **kwargs: Any) -> None:...

def show(*args, **kwargs) -> None:
    # Function here
```
`xlim` and `ylim` are actually not good examples at they have a a relative complex signature. the *args, **kwargs.


Checking the whole of pyplot, there's actually not too much we can gain from automatic rewriting. Instead, I think we can get the signatures cheaper be writing out the few relevant cases explicitly and adding a test that the signatures are still in sync. Something like
```
def test_signatures_identical():
    # for manually written forwarding functions in pyplot, check that
    # the signatures are in sync
    assert (inspect.signature(plt.set_loglevel).parameters
            == inspect.signature(mpl.set_loglevel).parameters)
```

The list of `*args, **kwargs` functions is

| function |  comment |
| -- | -- |
| set_loglevel | can be written out explicitly / copied + test |
| new_figure_manager | special (dynamically overwritten at runtime) - leave as is |
| draw_if_interactive | special (dynamically overwritten at runtime) - leave as is |
| show | adding the overload is ok |
| savefig | turn into `savefig(fname : [...], **kwargs)` |
| figlegend | more complex as the wrapped `Figure.legend` is also `*args, **kwargs`. Maybe simply add overloads? |
| subplot | add overloads |
| xlim / ylim | could be written out explicitly / copied + test |
| polar | could be written out explicitly / copied + test |



I have improved `boilerplate.py` to automatically generate xlim and ylim on `pyplot.py`. I think we can use the same system to generate `xscale` / `yscale` and `xlabel` / `ylabel` (this will generate a single method to get or set it, like the current `xlim` / `ylim`). I'm not sure about title because the getter takes a parameter.
Furthermore, I don't know how to create their tests because there are overloads.

Is there any reason why we don't put the `transparent` keyword argument ?
As for the others, I will manually copy the signature and create the tests to check it.
See https://github.com/matplotlib/matplotlib/issues/24413 re: savefig()'s signature.

Commit message: Add type hint on polar and set_loglevel on pyplot.py. Correct polar content

(cherry picked from commit 92dc04501bab539586cac48a3266891c75a4cb7c)

Patch to review:
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index cf5c9b4b739f..f1ce4ff6deb8 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -50,7 +50,7 @@
 import sys
 import threading
 import time
-from typing import TYPE_CHECKING, cast, overload
+from typing import IO, TYPE_CHECKING, cast, overload
 
 from cycler import cycler  # noqa: F401
 import matplotlib
@@ -338,8 +338,8 @@ def uninstall_repl_displayhook() -> None:
 
 # Ensure this appears in the pyplot docs.
 @_copy_docstring_and_deprecators(matplotlib.set_loglevel)
-def set_loglevel(*args, **kwargs) -> None:
-    return matplotlib.set_loglevel(*args, **kwargs)
+def set_loglevel(level: str) -> None:
+    return matplotlib.set_loglevel(level)
 
 
 @_copy_docstring_and_deprecators(Artist.findobj)
@@ -1251,11 +1251,11 @@ def draw() -> None:
 
 
 @_copy_docstring_and_deprecators(Figure.savefig)
-def savefig(*args, **kwargs) -> None:
+def savefig(fname: str | os.PathLike | IO, **kwargs) -> None:
     fig = gcf()
     # savefig default implementation has no return, so mypy is unhappy
     # presumably this is here because subclasses can return?
-    res = fig.savefig(*args, **kwargs)  # type: ignore[func-returns-value]
+    res = fig.savefig(fname, **kwargs)  # type: ignore[func-returns-value]
     fig.canvas.draw_idle()  # Need this if 'transparent=True', to reset colors.
     return res
 
@@ -2690,7 +2690,13 @@ def matshow(A: ArrayLike, fignum: None | int = None, **kwargs) -> AxesImage:
     return im
 
 
-def polar(*args, **kwargs) -> list[Line2D]:
+def polar(
+        *args: float | ArrayLike | str,
+        scalex: bool = True,
+        scaley: bool = True,
+        data=None,
+        **kwargs
+) -> list[Line2D]:
     """
     Make a polar plot.
 
@@ -2724,7 +2730,13 @@ def polar(*args, **kwargs) -> list[Line2D]:
             )
     else:
         ax = axes(projection="polar")
-    return ax.plot(*args, **kwargs)
+    return ax.plot(
+        *args,
+        scalex=scalex,
+        scaley=scaley,
+        data=data,
+        **kwargs
+    )
 
 
 # If rcParams['backend_fallback'] is true, and an interactive backend is


Code review comments:
diff_hunk: @@ -2690,7 +2690,13 @@ def matshow(A: ArrayLike, fignum: None | int = None, **kwargs) -> AxesImage:
     return im
 
 
-def polar(*args, **kwargs) -> list[Line2D]:
+def polar(
+        *args: float | ArrayLike | str,
+        scalex: bool = True,
+        scaley: bool = True,
+        data=None,
path: lib/matplotlib/pyplot.py
text: @user1:
 IMHO we could leave data out, because it's not typed anyway, so no advantage of writing it out.

diff_hunk: @@ -2690,7 +2690,13 @@ def matshow(A: ArrayLike, fignum: None | int = None, **kwargs) -> AxesImage:
     return im
 
 
-def polar(*args, **kwargs) -> list[Line2D]:
+def polar(
+        *args: float | ArrayLike | str,
+        scalex: bool = True,
path: lib/matplotlib/pyplot.py
text: @user1:
I would also leave `scalex`, `scaley` out. They are an odd wart of `plot`. No other plotting method has this and I'd rather not explicitly talk about it in `polar`, even if this means it's not type annotated.