Skip to content

Generate pyplot wrappers at runtime #25422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

QuLogic
Copy link
Member

@QuLogic QuLogic commented Mar 9, 2023

PR Summary

When looking at #24976, I was wondering if we could drop black for it, but that seemed to be fairly difficult. But then I wondered if we really needed to generate all this text that needed to pass flake8 in the first place.

So this removes the need for a boilerplate and various templates that need to be generated and pass flake8. Now that module-level __getattr__ exists, we don't need to pay the full penalty of generating everything on import, and can amortize that over every function call.

As a downside, the wrappers no longer have explicit parameters, so argument errors are raised from the original
methods, which may be confusing. There is currently one test that fails because of this, but it could be fixed if we are okay with this.

This also includes #12743, in order to more explicitly define what's in the file. I did not yet re-review everything in __all__, but I did need to add these symbols to __dir__ in order to get tests to pass.

There's also a small oddity that I need to explicitly call __getattr__ in the file itself. I'm not sure if there is a better way to fix that.

PR Checklist

Documentation and Tests

  • Has pytest style unit tests (and pytest passes)
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • [n/a] New plotting related features are documented with examples.

Release Notes

  • New features are marked with a .. versionadded:: directive in the docstring and documented in doc/users/next_whats_new/
  • API changes are marked with a .. versionchanged:: directive in the docstring and documented in doc/api/next_api_changes/
  • Release notes conform with instructions in next_whats_new/README.rst or next_api_changes/README.rst

timhoffm and others added 2 commits March 9, 2023 03:54
This removes the need for a boilerplate and various templates that need
to be generated and pass flake8. As a downside, the wrappers no longer
ave explicit parameters, so argument errors are raised from the original
methods, which may be confusing.
@tacaswell
Copy link
Member

When we talked about doing this before we rejected it because we wanted to be kind to static analysis so we would probably need to generate the pyi files still?

@anntzer
Copy link
Contributor

anntzer commented Mar 9, 2023

I don't think the pyi files necessarily need to follow pep8 (at least they may not need to be linewrapped)?

@QuLogic
Copy link
Member Author

QuLogic commented Mar 9, 2023

I thought we had an earlier discussion about this, but could not find it. I went through again and did a better search, and I believe the longterm goal was to get rid of boilerplate.py. Reading through the issues I've found that:

  1. I left a comment that @jklymak had an implementation for this, but the branch was deleted. But I don't actually know where I found the reference to that branch any more.
  2. Code generation improvements in Python 2.4 would enable dropping boilerplate.py, but 2.2 was still supported at the time: Pyplot update in setup.py #928 (comment)
  3. The previous major holdup was the signature being *args, **kwargs

At this point, we are far and away from still supporting Python 2.2, and this implementation correctly creates the signature:

$ ipython
In [1]: import matplotlib.pyplot as plt

In [2]: plt.acorr  # Axes wrapper
Out[2]: <function matplotlib.pyplot.acorr(x, *, data=None, **kwargs)>
In [3]: plt.Axes.acorr
Out[3]: <function matplotlib.axes._axes.Axes.acorr(self, x, *, data=None, **kwargs)>

In [4]: plt.figimage  # Figure wrapper
Out[4]: <function matplotlib.pyplot.figimage(X, xo=0, yo=0, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, origin=None, resize=False, **kwargs)>

In [5]: plt.Figure.figimage
Out[5]: <function matplotlib.figure.Figure.figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, origin=None, resize=False, **kwargs)>

The only inconsistency is the error raised for incorrect arguments as noted above, but we could fix that with inspect.bind also?

@QuLogic
Copy link
Member Author

QuLogic commented Mar 9, 2023

I also could've sworn that yesterday I could get ipython/python to display the type annotations in signature, and thus confirmed that the wrapped versions were correct, but today I can't seem to get it to display anything, even for the original functions.

But yes, it looks like running mypy on an example that uses a pyplot wrapper does fail now, so we'll have to do something about that.

@ksunden
Copy link
Member

ksunden commented Mar 10, 2023

This will not have type hints for the generated functions. Type hints from pyi stub files are not available at runtime, and thus the signatures for any dynamic generation will not have them until and unless we have inline type hints.

This behavior may also explain the "ipython sometimes has annotations". It will have them, I think, for pyplot in #24976, but not any of the underlying functions. I believe Ipython uses the runtime annotations.

We could make boilerplate generate the pyi file instead of the py file, which is arguably easier (though not by much, honestly) and could be less strict about flake8...

@timhoffm
Copy link
Member

timhoffm commented Mar 10, 2023

I’m feeling very uneasy about this. Dynamically generated code can behave like static code, but it is different, which can show up in a number of ways:

  • Only the executed code will reveal the added functions. But there are tools that statically analyze files.
  • What happens when you step through this in a debugger?
  • Is this correctly picked up in Sphinx? (Didn’t check because CI is broken). Maybe yes, but then likely without source links.
  • Some types of structural errors in the generated functions would be caught by the parser (we at least know that we create syntactically valid functions). For dynamic functions, everything is deferred to call-time. (Which we might not cover systematically in our tests).
  • Related: I assume it’s not possible to have a test coverage report on the dynamic functions.
  • It’s harder to see that changes in our generating code will do the right thing: The static generator produces code that I can investigate. The dynamic generator produces runtime objects/ behavior that is much harder to reason about.
  • For new contributors / readers of the code , it is much harder to understand what is going on.

Overall, I think this will work well in 99% of the cases, but I expect several edge cases where we’ll run into trouble. This is an essential part of the library. The static generator has proven to work well. Changing that is risky.

On the other hand, I do not fully understand the motivation. What are the benefits of this? Is this only because of flake8? If so, there have to be better ways to cope with it.

@ksunden
Copy link
Member

ksunden commented Mar 14, 2023

My understanding is that this is in response to my proposed change as part of the type hinting effort. See that discussion for my in depth reply.

The short version is that type hints made the auto generated code hit several edge cases on formatting that the existing naive textwrap implementation did not handle well and so rather than playing whack-a-mole with those edge cases, I used black, a tool designed to autoformat python code, to apply formatting to the autogenerated portions of pyplot.

I am not totally unwilling to rethink that, but the bar is pretty high, as outlined in my reply to the linked comment above.

@timhoffm
Copy link
Member

I'm fine with running black on the autogenerated code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants