Skip to content
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

Skip tests for optional/extra dependencies when not installed #1113

Merged
merged 26 commits into from
Mar 31, 2020

Conversation

hectormz
Copy link
Contributor

@hectormz hectormz commented Mar 9, 2020

Description

When running all tests locally ImportErrors are thrown if optional/extra dependencies are not installed. By skipping these tests when not installed, users can still run the entire test suite if desired. The entire test suite should still be run via CI.

This behavior is largely achieved by changing

import xx

to

xx = pytest.importorskip(xx)

which will try to import module xx, and will skip all tests in file if the module is not installed.

Individual tests may also be skipped using the decorator:

@pytest.mark.skipif(importlib.util.find_spec("xx") is None, reason="xx tests only required for CI")

The last remaining issue is numba, which is used in 20+ tests, used in a variety of files. One option is to make numba an entry in requirements-dev.txt instead of requirements-optional.txt, or add the skip decorators to each of the 20+ tests.

This PR addresses Issue #1112 .

Checklist

  • Follows official PR format
  • New features are properly documented (with an example if appropriate)?
  • Code style correct (follows pylint and black guidelines)
  • Changes are listed in changelog

Copy link
Member

@OriolAbril OriolAbril left a comment

Choose a reason for hiding this comment

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

Very general and minor comments.

To get pylint off you back you can put the emcee = pytest.importorskip("emcee") like lines below the imports, or set the disable wrong import error at the beginning of the file to avoid the check throughout all the file.

I'd also change the message in the skipifs to "xx not installed" or "test requires xx which is not installed"

Regarding numba, maybe we could move all numba tests to a specific file? Numba is an optional requirement for users (like bokeh) whereas the dependencies in requirements dev are not.

@hectormz
Copy link
Contributor Author

hectormz commented Mar 9, 2020

Hi @OriolAbril thanks!

I'll fix the pylint issues, and move the imports down.

I'll update the skipif messages to: "test requires xx which is not installed"

I'll look into moving all the numba files to their own file(s). If it looks a little more complicated organizing them in a clear way, I might move that to a new Issue/PR. Here are the failing numba tests currently:

FAILED tests/base_tests/test_diagnostics.py::test_numba_bfmi - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_rhat[rank] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_rhat[split] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_rhat[folded] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_rhat[z_scale] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_rhat[identity] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_mcse[mean] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_mcse[sd] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_numba_mcse[quantile] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_ks_summary_numba - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_geweke_numba - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_mcse_error_numba[True-1] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_mcse_error_numba[True-20] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_mcse_error_numba[False-1] - ValueError: Numba is not installed
FAILED tests/base_tests/test_diagnostics.py::test_mcse_error_numba[False-20] - ValueError: Numba is not installed
FAILED tests/base_tests/test_stats.py::test_summary_include_circ[True] - ValueError: Numba is not installed
FAILED tests/base_tests/test_stats.py::test_summary_include_circ[False] - ValueError: Numba is not installed
FAILED tests/base_tests/test_stats.py::test_numba_stats - ValueError: Numba is not installed
FAILED tests/base_tests/test_utils.py::test_utils_fixture - ModuleNotFoundError: No module named 'numba'
FAILED tests/base_tests/test_utils.py::test_conditional_jit_numba_decorator_keyword - ValueError: too many values to unpack (expected 2)
FAILED tests/base_tests/test_utils.py::test_numba_utils - ValueError: Numba is not installed
FAILED tests/base_tests/test_utils.py::test_numba_var[0-0] - ValueError: Numba is not installed
FAILED tests/base_tests/test_utils.py::test_numba_var[0-1] - ValueError: Numba is not installed
FAILED tests/base_tests/test_utils.py::test_numba_var[1-0] - ValueError: Numba is not installed
FAILED tests/base_tests/test_utils.py::test_numba_var[1-1] - ValueError: Numba is not installed

The easiest/laziest solution might be to create new files with numba tests:

test_utils_numba.py
test_stats_numba.py
test_diagnostis_numba.py

This would preserve the clarity of the original goal that numba is helping achieve.

@hectormz hectormz force-pushed the optional-tests branch 2 times, most recently from 2f43704 to ea585c9 Compare March 9, 2020 19:15
@hectormz
Copy link
Contributor Author

Hi @OriolAbril I ended up moving the numba tests as I proposed. I tested locally with and without numba to verify passed or skipped tests

import stan as pystan # pylint: disable=import-error
return int(pystan.__version__[0])
try:
import stan as pystan # pylint: disable=import-error
Copy link
Member

Choose a reason for hiding this comment

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

Curious, why is this extra block of logic needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@canyon289 In the event that neither pystan or stan are installed, there would be no imported pystan, which would then have no version attribute. I believe that this function is called via test_data_pystan.py, and will throw an error just while collecting the tests. Even though this file would eventually be skipped because neither libraries are installed via:

pytestmark = pytest.mark.skipif(
    ~pystan_installed, reason="test requires stan/pystan which is not installed"
)

This was my way to handle potentially two exceptions, and also indicate that there is no form of stan installed by returning None. (I have neither stan/pystan package, so this was a solution that I could get to work for me)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this sounds fine, maybe this could be import stan --> stan.__version__

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @ahartikainen is that just regarding line 580? Still try to import pystan, and if not present try to import stan, but no need to import stan as pystan?

@canyon289
Copy link
Member

This pr is great. It does mean that the maintainers need to be extra vigilant with CI and ensure that no tests are skipped before merging. Do you know if theres a way with pytest to fail ir any tests are skipped?

@hectormz
Copy link
Contributor Author

Valid point @canyon289 ! There may be an option that is builtin to pytest (that I haven't found yet), but I came across pytest-error-for-skips :

Pytest plugin to treat skipped tests as test failures.

This is nice if you want to ensure that your CI tests really run all tests and don't skip tests because of missing dependencies.

Simply execute your tests via pytest --error-for-skips ... and all skipped tests become test failures.

But I must imagine that there might be something completely integrated into pytest

@hectormz
Copy link
Contributor Author

@ahartikainen
Copy link
Contributor

Could we have env variable which is also tested with the import func?

@canyon289
Copy link
Member

Could we have env variable which is also tested with the import func?

Curious what this would do

@OriolAbril
Copy link
Member

I have to point out that some tests are skipped in CI, some features require the latest version of a given library which means that test can be executed in "external_latest" but not in "external_especial"

@hectormz
Copy link
Contributor Author

I'm guessing @ahartikainen is proposing to create an env variable on the CI machine like ARVIZ_CI_MACHINE. Instead of the importorskip calls at the top, we would do something like:

if os.environ.get('ARVIZ_CI_MACHINE')=='True':
    ARVIZ_CI_MACHINE = True
else:
    ARVIZ_CI_MACHINE = False

try:
    import pystan
    pystan_installed = True
except ImportError:
    pystan_installed = False

pytestmark = pytest.mark.skipif(
    ~pystan_installed & ~ARVIZ_CI_MACHINE, reason="test requires stan/pystan which is not installed"
)

@hectormz
Copy link
Contributor Author

@OriolAbril so it would be a good idea to separate acceptable and unacceptable CI skips?

@ahartikainen
Copy link
Contributor

Maybe we could monkeypatch pytest.importskip function?

@OriolAbril
Copy link
Member

I have seen this example in pytest docs about importorskip usage: http://doc.pytest.org/en/latest/example/parametrize.html#indirect-parametrization-of-optional-implementations-imports, maybe we could base the monkeypatch on something similar?

If CI import and return module, if not in CI use importorskip?

@hectormz
Copy link
Contributor Author

hectormz commented Mar 22, 2020

I've been looking at monkeypatching pytest.importskip (and _pytest.outcomes.importorskip) and haven't been able to do so. I'm not sure pytest can monkeypatch itself. I've done this in the context of conftest.py for all tests, or individual tests. I started with just trying to delete importorskip entirely:

@pytest.fixture(autouse=True)
def no_importorskip(monkeypatch):
    """Remove pytest.importorskip for all tests."""
    monkeypatch.delattr("pytest.importorskip")

I think if we want to go down this route, we can implement our own importorskip() (take original code and add ability to check for env variable, and stick it in arviz.tests.utils etc).

@ahartikainen
Copy link
Contributor

yeah, that sounds like the correct option

@hectormz
Copy link
Contributor Author

@ahartikainen let me know of this implementation. I also added tests for the custom importorskip to test local and CI experience

Copy link
Contributor

@ahartikainen ahartikainen left a comment

Choose a reason for hiding this comment

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

Looks good. I added some minor comments / questions.

LGTM

arviz/tests/external_tests/test_data_pystan.py Outdated Show resolved Hide resolved
import stan as pystan # pylint: disable=import-error
return int(pystan.__version__[0])
try:
import stan as pystan # pylint: disable=import-error
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this sounds fine, maybe this could be import stan --> stan.__version__

# of existing directories with the same name we're trying to
# import but without a __init__.py file
warnings.simplefilter("ignore")
__import__(modname)
Copy link
Contributor

Choose a reason for hiding this comment

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

is there any difference between these

__import__(modname)
mod = sys.modules[modname]

vs

mod = importlib.import_module(modname)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think they should be mostly similar for our uses. I used the former as I copied the existing code in pytest.importorksip.

But I can use importlib.import_module. It might clear up the failures in CI to import jax.random.PRNGKey and pyro.infer.Predictive.

I can't seem to easily install jaxlib on Windows, so I can't test this out locally.

@hectormz
Copy link
Contributor Author

In addition to the points raised by @ahartikainen , I'm looking to see why these CI imports failed:

ModuleNotFoundError: No module named 'jax.random.PRNGKey'; 'jax.random' is not a package

ModuleNotFoundError: No module named 'pyro.infer.Predictive'

@hectormz
Copy link
Contributor Author

@ahartikainen I don't think test_precompile_models() in helpers.py is being discovered and run with pytest arviz\tests (I searched through it in verbose log). It might be better to move it to the test_helpers.py file I created.

@ahartikainen
Copy link
Contributor

@ahartikainen I don't think test_precompile_models() in helpers.py is being discovered and run with pytest arviz\tests (I searched through it in verbose log). It might be better to move it to the test_helpers.py file I created.

tbh, it is not a test, but a way to precompile models for CI, so at some point, we can start gathering performance data for our tests and they should be reproducible, in most cases.

I cheated with test_ so pytest would work with it.

So users should not run it (without manually calling it)

@ahartikainen
Copy link
Contributor

ahartikainen commented Mar 23, 2020

In addition to the points raised by @ahartikainen , I'm looking to see why these CI imports failed:

ModuleNotFoundError: No module named 'jax.random.PRNGKey'; 'jax.random' is not a package

ModuleNotFoundError: No module named 'pyro.infer.Predictive'

edit.
I think we probably need to change the import for the classes

class = getattr(import_module('my_module'), 'my_class')

https://stackoverflow.com/questions/41678073/import-class-from-module-dynamically

@hectormz
Copy link
Contributor Author

tbh, it is not a test, but a way to precompile models for CI, so at some point, we can start gathering performance data for our tests and they should be reproducible, in most cases.

I cheated with test_ so pytest would work with it.

So users should not run it (without manually calling it)

okay got it @ahartikainen

edit.
I think we probably need to change the import for the classes

class = getattr(import_module('my_module'), 'my_class')

https://stackoverflow.com/questions/41678073/import-class-from-module-dynamically

I'll give that shot!

@codecov
Copy link

codecov bot commented Mar 30, 2020

Codecov Report

Merging #1113 into master will increase coverage by 0.06%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1113      +/-   ##
==========================================
+ Coverage   92.61%   92.68%   +0.06%     
==========================================
  Files          93       93              
  Lines        8991     9073      +82     
==========================================
+ Hits         8327     8409      +82     
  Misses        664      664              
Impacted Files Coverage Δ
arviz/data/io_cmdstanpy.py 100.00% <0.00%> (ø)
arviz/plots/backends/matplotlib/forestplot.py 96.53% <0.00%> (+0.01%) ⬆️
arviz/plots/plot_utils.py 94.66% <0.00%> (+0.01%) ⬆️
arviz/plots/backends/bokeh/forestplot.py 92.88% <0.00%> (+0.02%) ⬆️
arviz/stats/stats_utils.py 91.66% <0.00%> (+0.02%) ⬆️
arviz/data/io_pystan.py 99.07% <0.00%> (+0.04%) ⬆️
arviz/data/inference_data.py 83.13% <0.00%> (+0.06%) ⬆️
arviz/utils.py 90.95% <0.00%> (+0.14%) ⬆️
arviz/data/io_cmdstan.py 94.84% <0.00%> (+0.14%) ⬆️
arviz/data/io_pyro.py 95.86% <0.00%> (+0.24%) ⬆️
... and 3 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f76da26...3edc64e. Read the comment docs.

@ahartikainen
Copy link
Contributor

Is this now skipping pystan tests?

@hectormz
Copy link
Contributor Author

Seems that way. I was going to investigate why. To sort this out, I setup a virtual machine to mimic the CI, but pystan tests ran locally.

@ahartikainen
Copy link
Contributor

Is it trying to import once and then just fails?

importlib.util.find_spec("stan") is not None
)
pytestmark = pytest.mark.skipif(
not pystan_installed & (not running_on_ci()),
Copy link
Contributor

Choose a reason for hiding this comment

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

not pystan_installed & (not running_on_ci())

not True & (not True) == True

(not True) & (not True) == False

Copy link
Contributor

Choose a reason for hiding this comment

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

Or you can use

not ( pystan_installed |  running_on_ci() )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that logic is cleaner. But shouldn't the current logic work because it equates to False, so it shouldn't get skipped

Copy link
Contributor

Choose a reason for hiding this comment

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

It is currently true (missing parenthesis)

not True & (not True) == True

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes okay. I updated that, will see if it does the trick. And I see that there are some built-in emcee skips looking for emcee3, so that should still be okay

@hectormz
Copy link
Contributor Author

Thanks @ahartikainen for getting this through!

@canyon289
Copy link
Member

Looked it over. It looks like it works, there some sections I can't read but I don't want to hold this up :)

@canyon289
Copy link
Member

Also this is great work. Looks well implemented. thank you so much for persevering and getting it through

@hectormz
Copy link
Contributor Author

@canyon289 there are sections you physically can't access in order to inspect, or some sections that don't make sense?

If there's something I could clarify or improve to avoid future headache, I'd be happy to update it now that it's working.

Happy to contribute 🙂

@ahartikainen ahartikainen merged commit 9909aab into arviz-devs:master Mar 31, 2020
@canyon289
Copy link
Member

canyon289 commented Mar 31, 2020

@hectormz It's likely less about your code and more about me not understanding the depth of the pytest internals. I didn't mean that as a criticism of your work!

@hectormz
Copy link
Contributor Author

No offense taken!

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.

4 participants