-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Changed importError to ModuleNotFoundError #12220
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
Changes from 11 commits
3a26211
3f80fd7
fd288e1
1fe14e0
543262c
1456b71
9a9643d
3ca5036
7de869d
3ca329b
f14d78d
b59336a
76fd0e5
a55abeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| :func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`. | ||
|
|
||
| The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`. | ||
|
|
||
| See :ref:`import-or-skip-import-error` for details. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ | |
| from typing import Type | ||
| from typing import TypeVar | ||
|
|
||
| from .warning_types import PytestDeprecationWarning | ||
|
|
||
|
|
||
| class OutcomeException(BaseException): | ||
| """OutcomeException and its subclass instances indicate and contain info | ||
|
|
@@ -192,7 +194,11 @@ def xfail(reason: str = "") -> NoReturn: | |
|
|
||
|
|
||
| def importorskip( | ||
| modname: str, minversion: Optional[str] = None, reason: Optional[str] = None | ||
| modname: str, | ||
| minversion: Optional[str] = None, | ||
| reason: Optional[str] = None, | ||
| *, | ||
| exc_type: Optional[Type[ImportError]] = None, | ||
| ) -> Any: | ||
| """Import and return the requested module ``modname``, or skip the | ||
| current test if the module cannot be imported. | ||
|
|
@@ -205,30 +211,81 @@ def importorskip( | |
| :param reason: | ||
| If given, this reason is shown as the message when the module cannot | ||
| be imported. | ||
| :param exc_type: | ||
| The exception that should be captured in order to skip modules. | ||
| Must be :py:class:`ImportError` or a subclass. | ||
|
|
||
| If the module can be imported but raises :class:`ImportError`, pytest will | ||
| issue a warning to the user, as often users expect the module not to be | ||
| found (which would raise :class:`ModuleNotFoundError` instead). | ||
|
|
||
| This warning can be suppressed by passing ``exc_type=ImportError`` explicitly. | ||
|
|
||
| See :ref:`import-or-skip-import-error` for details. | ||
|
|
||
|
|
||
| :returns: | ||
| The imported module. This should be assigned to its canonical name. | ||
|
|
||
| Example:: | ||
|
|
||
| docutils = pytest.importorskip("docutils") | ||
|
|
||
| .. versionadded:: 8.2 | ||
|
|
||
| The ``exc_type`` parameter. | ||
| """ | ||
| import warnings | ||
|
|
||
| __tracebackhide__ = True | ||
| compile(modname, "", "eval") # to catch syntaxerrors | ||
|
|
||
| # Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError), | ||
| # as this might be hiding an installation/environment problem, which is not usually what is intended | ||
| # when using importorskip() (#11523). | ||
| # In 9.1, to keep the function signature compatible, we just change the code below to: | ||
| # 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given. | ||
| # 2. Remove `warn_on_import` and the warning handling. | ||
| if exc_type is None: | ||
| exc_type = ImportError | ||
| warn_on_import_error = True | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit confused by the Codecov annotations here. It looks to me like this (and the others) should be covered by tests just fine? Are the added tests not running or something? Or is codecov broken? Or am I just missing something?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure, but this is definitely being covered by tests (and normal tests, not through Not sure what's going on. |
||
| else: | ||
| warn_on_import_error = False | ||
|
|
||
| skipped: Optional[Skipped] = None | ||
| warning: Optional[Warning] = None | ||
|
|
||
| with warnings.catch_warnings(): | ||
| # Make sure to ignore ImportWarnings that might happen because | ||
| # of existing directories with the same name we're trying to | ||
| # import but without a __init__.py file. | ||
| warnings.simplefilter("ignore") | ||
|
|
||
| try: | ||
| __import__(modname) | ||
| except ImportError as exc: | ||
| except exc_type as exc: | ||
| # Do not raise or issue warnings inside the catch_warnings() block. | ||
| if reason is None: | ||
| reason = f"could not import {modname!r}: {exc}" | ||
| raise Skipped(reason, allow_module_level=True) from None | ||
| skipped = Skipped(reason, allow_module_level=True) | ||
|
|
||
| if warn_on_import_error and type(exc) is ImportError: | ||
nicoddemus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| lines = [ | ||
| "", | ||
| f"Module '{modname}' was found, but when imported by pytest it raised:", | ||
| f" {exc!r}", | ||
| "In pytest 9.1 this warning will become an error by default.", | ||
| "You can fix the underlying problem, or alternatively overwrite this behavior and silence this " | ||
| "warning by passing exc_type=ImportError explicitly.", | ||
| "See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror", | ||
| ] | ||
| warning = PytestDeprecationWarning("\n".join(lines)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not call
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because at this point we are inside the |
||
|
|
||
| if warning: | ||
| warnings.warn(warning, stacklevel=2) | ||
| if skipped: | ||
| raise skipped | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, couldn't this be simplified by moving the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per #12220 (comment), we need to raise this after the warning. 👍 |
||
|
|
||
| mod = sys.modules[modname] | ||
| if minversion is None: | ||
| return mod | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| from typing import List | ||
| from typing import Tuple | ||
| from typing import Type | ||
| import warnings | ||
|
|
||
| from _pytest import outcomes | ||
| from _pytest import reports | ||
|
|
@@ -762,6 +763,73 @@ def test_importorskip_imports_last_module_part() -> None: | |
| assert os.path == ospath | ||
|
|
||
|
|
||
| class TestImportOrSkipExcType: | ||
| """Tests for #11523.""" | ||
|
|
||
| def test_no_warning(self) -> None: | ||
| # An attempt on a module which does not exist will raise ModuleNotFoundError, so it will | ||
| # be skipped normally and no warning will be issued. | ||
| with warnings.catch_warnings(record=True) as captured: | ||
| warnings.simplefilter("always") | ||
|
|
||
| with pytest.raises(pytest.skip.Exception): | ||
| pytest.importorskip("TestImportOrSkipExcType_test_no_warning") | ||
|
|
||
| assert captured == [] | ||
|
|
||
| def test_import_error_with_warning(self, pytester: Pytester) -> None: | ||
| # Create a module which exists and can be imported, however it raises | ||
| # ImportError due to some other problem. In this case we will issue a warning | ||
| # about the future behavior change. | ||
| fn = pytester.makepyfile("raise ImportError('some specific problem')") | ||
| pytester.syspathinsert() | ||
|
|
||
| with warnings.catch_warnings(record=True) as captured: | ||
| warnings.simplefilter("always") | ||
|
|
||
| with pytest.raises(pytest.skip.Exception): | ||
| pytest.importorskip(fn.stem) | ||
|
|
||
| [warning] = captured | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love this! Never seen it before, I've always done: assert len(captured) == 1
warning = captured[0]for this kind of thing.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All credits go to https://www.cosmicpython.com, seen this pattern there and have been using it ever since. 😁 |
||
| assert warning.category is pytest.PytestDeprecationWarning | ||
|
|
||
| def test_import_error_suppress_warning(self, pytester: Pytester) -> None: | ||
| # Same as test_import_error_with_warning, but we can suppress the warning | ||
| # by passing ImportError as exc_type. | ||
| fn = pytester.makepyfile("raise ImportError('some specific problem')") | ||
| pytester.syspathinsert() | ||
|
|
||
| with warnings.catch_warnings(record=True) as captured: | ||
| warnings.simplefilter("always") | ||
|
|
||
| with pytest.raises(pytest.skip.Exception): | ||
| pytest.importorskip(fn.stem, exc_type=ImportError) | ||
|
|
||
| assert captured == [] | ||
|
|
||
| def test_warning_integration(self, pytester: Pytester) -> None: | ||
| pytester.makepyfile( | ||
| """ | ||
| import pytest | ||
| def test_foo(): | ||
| pytest.importorskip("warning_integration_module") | ||
| """ | ||
| ) | ||
| pytester.makepyfile( | ||
| warning_integration_module=""" | ||
| raise ImportError("required library foobar not compiled properly") | ||
| """ | ||
| ) | ||
| result = pytester.runpytest() | ||
| result.stdout.fnmatch_lines( | ||
| [ | ||
| "*Module 'warning_integration_module' was found, but when imported by pytest it raised:", | ||
| "* ImportError('required library foobar not compiled properly')", | ||
| "*1 skipped, 1 warning*", | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
| def test_importorskip_dev_module(monkeypatch) -> None: | ||
| try: | ||
| mod = types.ModuleType("mockmodule") | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.