Skip to content

[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

Merged
merged 39 commits into from
May 6, 2019

Conversation

glemaitre
Copy link
Member

Instead of using our own decorator to skip tests if matplotlib is not installed, we should use pytest.

@thomasjpfan
Copy link
Member

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.

@glemaitre
Copy link
Member Author

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?

@thomasjpfan
Copy link
Member

We could have a teardown at the level of the module which could clean-up all figures.

Would there be any memory issues if a module generates a bunch of figures?

@glemaitre
Copy link
Member Author

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.

@thomasjpfan
Copy link
Member

A "hacky" way of using teardown_function(function) would be to check to name:

def teardown_function(function):
    if 'plot' in function.__name__:
        close_figure()

WDYT?

@glemaitre
Copy link
Member Author

It would require to call all the function with plot within the name which could also be error-prone.

@thomasjpfan
Copy link
Member

within the name which could also be error-prone.

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 close_figure themselves.

@glemaitre
Copy link
Member Author

Regarding the fixture, this is the reason that I proposed directly the teardown because you don't have to pass anything to the function.

Copy link
Member

@thomasjpfan thomasjpfan left a 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.

else:
return func(*args, **kwargs)
return run_test
def has_matplotlib():
Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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')
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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()
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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

@NicolasHug
Copy link
Member

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 module_teardown() useless)

@glemaitre
Copy link
Member Author

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)

@thomasjpfan
Copy link
Member

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.

@glemaitre
Copy link
Member Author

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.

@thomasjpfan
Copy link
Member

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)

@NicolasHug
Copy link
Member

A few other points:

  • If we use the teardown, why do we still keep the close_figure() calls?

  • We should just remove the helper close_figure() because it's just a (bad) wrapper around plt.close: https://matplotlib.org/api/_as_gen/matplotlib.pyplot.close.html
    I'm actually going to open a PR for this so that it can be merged before 0.21.

  • I still don't understand why / whether the figures are closed at the end of a module / function

@NicolasHug
Copy link
Member

NicolasHug commented Apr 26, 2019

Opened #13730, can you guys look at it please

@NicolasHug
Copy link
Member

NicolasHug commented Apr 26, 2019

@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())
  • no warnings
  • list is empty

@thomasjpfan
Copy link
Member

@NicolasHug I am able to reproduce the warning with your script. It could be platform/matplotlib version specific?

@thomasjpfan
Copy link
Member

thomasjpfan commented Apr 26, 2019

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.

@NicolasHug
Copy link
Member

OK. I agree we should be doing whatever pandas or yellowbrick is doing. Let's get rid of the module teardown.

@glemaitre
Copy link
Member Author

OK I make the modifications

@glemaitre
Copy link
Member Author

We install in dev mode in linux so it finds the conftest.

I'll then add a conftest in sklearn and add an exception in the corresponding test.

@glemaitre glemaitre changed the title MAINT: use pytest skipif instead of our decorator for matplotlib [MRG] MAINT: use pytest skipif instead of our decorator for matplotlib May 3, 2019
@glemaitre
Copy link
Member Author

I added one of the windows build and it seems that in this case the combining is working.

@glemaitre glemaitre changed the title [MRG] MAINT: use pytest skipif instead of our decorator for matplotlib [MRG] MAINT: add fixture for init and clean-up with matplotlib May 3, 2019
@@ -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):
Copy link
Member

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

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes in another pr

NicolasHug and others added 6 commits May 4, 2019 21:35
Co-Authored-By: glemaitre <g.lemaitre58@gmail.com>
Co-Authored-By: glemaitre <g.lemaitre58@gmail.com>
Copy link
Member

@NicolasHug NicolasHug left a 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

Copy link
Member

@thomasjpfan thomasjpfan left a 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!

@glemaitre
Copy link
Member Author

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)

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>
@thomasjpfan
Copy link
Member

I think that we should investigate why the combining in linux does not work as expected.

Want to figure this out in another PR? I think it is important we remove close_figure from the release.

@glemaitre
Copy link
Member Author

Yes in another PR is fine.

@ogrisel ogrisel merged commit b34751b into scikit-learn:master May 6, 2019
@ogrisel ogrisel added this to the 0.21 milestone May 6, 2019
@ogrisel
Copy link
Member

ogrisel commented May 6, 2019

@jnothman I let you backport that to be included in the 0.21 release branch.

jnothman pushed a commit to jnothman/scikit-learn that referenced this pull request May 6, 2019
koenvandevelde pushed a commit to koenvandevelde/scikit-learn that referenced this pull request Jul 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants