-
-
Notifications
You must be signed in to change notification settings - Fork 26k
[MRG] MAINT: add fixture for init and clean-up with matplotlib #13708
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
Conversation
There are a bunch of “close_figures” that a dev needs to remember to call. It may be good to have a fixture that yields a figure and cleans it up. |
I think that sometimes the figure will be created within the function itself (at least for some part of the test). We could have a teardown at the level of the module which could clean-up all figures. We could also skip this teardown if matplotlib is not available. WDYT? |
Would there be any memory issues if a module generates a bunch of figures? |
I don't think that it would be a problem. If this is the case we could move from module to function. |
A "hacky" way of using def teardown_function(function):
if 'plot' in function.__name__:
close_figure() WDYT? |
It would require to call all the function with |
I see your point. The following is slightly better: def teardown_function(function):
if '_plot_' in function.__name__:
close_figure() What about it fixture? @pytest.fixture
def close_figures():
yield
if has_matplotlib():
close_figure()
def test_stuff(close_figures):
.... The fixture is not much better than having uses call |
Regarding the fixture, this is the reason that I proposed directly the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay lets go with module level teardowns.
sklearn/utils/testing.py
Outdated
else: | ||
return func(*args, **kwargs) | ||
return run_test | ||
def has_matplotlib(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have a check_matplotlib_support
util already
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check_matplotlib_support
raise an error while I only need a boolean test.
Do you mean to call check_matplotlib_support
within this helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok yeah they're not doing the same thing.
Can we make it private to this file then? It's only really useful for the decorator
def test_plot_partial_dependence(): | ||
# Test partial dependence plot function. | ||
import matplotlib.pyplot as plt # noqa | ||
plt = pytest.importorskip('matplotlib.pyplot') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO having a decorator makes it more obvious that the test is matplotlib related.
Also, it is strange to use this pattern in some places while the skip_if_no_matplotlib
decorator is used in some others
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm split. it was the suggestion of @ogrisel IRL. His point was to say that we have a decorator and then a lazily import while now you have only the import which skip the test if required.
I'll like the mark.skipif
since it can be used in different setups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just avoid the decorator and always use pytest.importorskip('matplotlib.pyplot')
then?
I'd rather keep things consistent.
But I'm OK too with decorator + lazy import.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the idiom:
plt = pytest.importorskip('matplotlib.pyplot')
good enough. I don't think we need a decorator for that.
@@ -45,6 +46,11 @@ | |||
multioutput_regression_data = (make_regression(n_targets=2, random_state=0), 2) | |||
|
|||
|
|||
def teardown_module(module): | |||
if has_matplotlib(): | |||
close_figure() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does that mean we don't need to call close_figure()
in all the tests of this module anymore?
If that's the case, I would suggest changing the defualt of close_figure()
from None
to 'all'
for it to be more obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does that mean we don't need to call close_figure() in all the tests of this module anymore?
This is the idea.
If that's the case, I would suggest changing the defualt of close_figure() from None to 'all' for it to be more obvious.
We can do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we do, we need this to be merged before the release
Also can you please explain why we need to clean the figures @glemaitre ? I would have expected the figures to be GCed at the end of each function, or at the very least at the end of the module (hence making |
I am afraid it does not. I tried the following test @pytest.mark.parametrize(
"x", list(range(30))
)
def test_fig(x):
import matplotlib.pyplot as plt
fig, ax = plt.subplots() and got the following warning: /home/glemaitre/miniconda3/envs/dev/lib/python3.7/site-packages/matplotlib/pyplot.py:514: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`).
max_open_warning, RuntimeWarning) |
Does that mean if a module creates more than 20 figures the tests will fail? If so, we should close revert back to calling close_figure at the end of each test. |
Nop this is just a warning, so everything will be fine, we will just have an increase of memory until we close all figure in the teardown. |
I suspect we will be adding more plotting functionality soon, and having increased memory usage would be unfavorable. I am in favor of cleaning up memory after each test. (Function level) |
A few other points:
|
Opened #13730, can you guys look at it please |
@glemaitre I cannot reproduce your warning import pytest
import matplotlib.pyplot as plt
@pytest.mark.parametrize(
"x", list(range(300))
)
def test_fig(x):
fig, ax = plt.subplots()
print(plt.get_fignums())
|
@NicolasHug I am able to reproduce the warning with your script. It could be platform/matplotlib version specific? |
Going through how yellowbrick and pandas test figures, closing the figure after each plotting test case is common amongst them. We need to make sure that plotting tests manually closes figures. |
OK. I agree we should be doing whatever pandas or yellowbrick is doing. Let's get rid of the module teardown. |
OK I make the modifications |
We install in dev mode in linux so it finds the conftest. I'll then add a conftest in |
I added one of the windows build and it seems that in this case the combining is working. |
@@ -1024,21 +1015,3 @@ def assert_run_python_script(source_code, timeout=60): | |||
% e.output.decode('utf-8')) | |||
finally: | |||
os.unlink(source_file) | |||
|
|||
|
|||
def close_figure(fig=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to see this go, but this is public and it's going to be in 0.21...
:s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could squeeze it in 0.21 ping @jnothman
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is only introduced in 0.21, let's make it private before final release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't code right now, but will be happy to approve a PR which does
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why no just merging this PR and remove this function directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is merged before release then fine. But in case it is not, a quick fix would do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that we converged toward a concensus on what to implement. I can quickly answer to review so if anyone have some additional changes to bring, I'll be happy to make them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes in another pr
Co-Authored-By: glemaitre <g.lemaitre58@gmail.com>
Co-Authored-By: glemaitre <g.lemaitre58@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM apart from this
Co-Authored-By: glemaitre <g.lemaitre58@gmail.com>
…rn into is/matplotlib_maint
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this PR, we will have matplotlib testing for 2 linux instances and 1 windows instances. (It looks like windows was added since we can report coverage correctly)
@ogrisel Your idea of using pyplot
as a fixture is great!
Yes. 2 linux for the minimum and latest and windows for the reporting. I think that we should investigate why the combining in linux does not work as expected. |
Co-Authored-By: glemaitre <g.lemaitre58@gmail.com>
Want to figure this out in another PR? I think it is important we remove |
Yes in another PR is fine. |
@jnothman I let you backport that to be included in the 0.21 release branch. |
Instead of using our own decorator to skip tests if matplotlib is not installed, we should use pytest.