-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Changed importError to ModuleNotFoundError #12220
Conversation
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.
Thanks @shekhuverma.
Can you please add a test that shows importorskip
will only skip on an explicit ModuleNotFoundError
, but produce a failure for an ImportError
exception?
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 at the very least, we need an easy escape hatch to get the former behavior, no? I'm somewhat ambivalent on adding a warning (after all, it's a loud failure - the only difference would be a warning vs. failing collection).
However, if someone really wants to catch ImportError
here, they should be able to do pytest.importorskip("mymodule", exc=ImportError)
or somesuch (with exc=
, we might want to ensure that issubclass(exc, ImportError)
maybe, at least on the type annotation level).
Right now, if someone wants to keep the existing behavior, they'd need to go from pytest.importorskip("mymodule")
to something like (untested):
try:
import mymodule
except ImportError:
pytest.skip("some good reason here", allow_module_level=True)
or so, which seems quite burdensome.
testing/test_runner.py
Outdated
@@ -762,6 +765,27 @@ def test_importorskip_imports_last_module_part() -> None: | |||
assert os.path == ospath | |||
|
|||
|
|||
def test_importorskip_importError_Exception() -> None: | |||
## Mocking the import function to raise a importError | |||
realimport = builtins.__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.
Please use monkeypatch instead of hand-rolling this.
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.
Besides the changes requested, please also add a new changelog entry, as improvement.
Actually. I think we probably should not introduce the new behavior right away, instead capture ImportError
and show a warning stating that the default behavior will change in future versions to catch ModuleNotFoundError
instead.
src/_pytest/outcomes.py
Outdated
@@ -225,7 +225,7 @@ def importorskip( | |||
warnings.simplefilter("ignore") | |||
try: | |||
__import__(modname) | |||
except ImportError as exc: | |||
except ModuleNotFoundError as exc: |
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.
Let's add a new parameter to this function, *, exc_type: Type[ImportError] = ModuleNotFoundError
and use it here:
except ModuleNotFoundError as exc: | |
except exc_type as exc: |
This way we provide a escape hatch to users which rely on the old behavior.
Also please update the docs, describing the parameters and explicitly showing an example for users that want the old behavior.
@The-Compiler and I left similar reviews almost at the same time hehehe. I think we should actually not change the default behavior right away, for better or worse this is a breaking change. I think introducing a new parameter
In 9.0, we turn the warning into an error, and 9.1 remove (2) from the code and let the The benefits of this is that users would be able to use |
Thanks for the review. |
src/_pytest/outcomes.py
Outdated
@@ -223,12 +230,24 @@ def importorskip( | |||
# of existing directories with the same name we're trying to | |||
# import but without a __init__.py file. | |||
warnings.simplefilter("ignore") | |||
|
|||
if exc_type is None: | |||
exc_type = ModuleNotFoundError |
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.
Sorry I was not clearer.
I meant that we should only issue the warning if:
- The user is not passing an explicit
exc_type
. - We actually capture
ImportError
.
So something like:
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")
if exc_type is None:
exc_type = ImportError
warn_on_import_error = True
else:
warn_on_import_error = False
try:
__import__(modname)
except exc_type as exc:
if reason is None:
reason = f"could not import {modname!r}: {exc}"
if warn_on_import_error and type(exc) is ImportError:
warnings.warn(
PytestDeprecationWarning(
f"pytest.importorskip() caught {exc}, but this will change in a future pytest release to only capture ModuleNotFoundError exceptions by default.\nTo overwrite the future behavior and silence this warning, pass exc_type=ImportError explicitly.",
)
)
raise Skipped(reason, allow_module_level=True) from None
So we will not be changing the behavior right now, only in the future.
Do you agree @The-Compiler?
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.
Yep, that sounds like a solid approach. We'll also need tests for the warning and the different argument behaviours.
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.
def importorskip(
modname: str,
minversion: Optional[str] = None,
reason: Optional[str] = None,
exc_type: Optional[Type[ImportError,ModuleNotFoundError]] = None,
) -> Any:
.
.
.
if exc_type is None:
#to keep the old behavior
exc_type = ImportError
warn_on_import_error = True
else:
warn_on_import_error = False
if warn_on_import_error or type(exc) is ImportError:
warnings.warn()
I think the function should be like this, showing a warning if the user doesn't pass any exc_type or when there is ImportError
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 do not want to issue the warning unless we actually caught ImportError
.
Most of the time the exception being raised is ModuleNotFoundError
, which is OK and what users expect.
The problem is when the library is found, but ImportError
is raised because of a related problem (for example failure to load a required a shared library). In this case we want to issue a warning now, and in pytest 9.0, let the exception propagate upwards.
The exc_type
parameter will let users be explicit about what they want to capture.
@shekhuverma thanks for following up! Took the liberty of adding more tests, docs, and change the implementation a bit. @The-Compiler would appreciate another review. |
After this gets merged I will create an issue to track changing the default in |
Thanks for the help @The-Compiler and @nicoddemus. |
Hello there, what are the next steps? |
The ball is on our side now @shekhuverma, waiting on @The-Compiler's review. |
(Note to ourselves, we need to squash merge this) |
Sorry for the delay! I plan to look at this today, now that PyConDE is over. |
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.
Thanks @shekhuverma and @nicoddemus. I have a couple of remaining questions and comments (most of them minor, but one semantic thing that I think we should aim to get correct).
"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)) |
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 call warning.warn(...)
here directly, given that warning = ...
is only set from here?
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.
Because at this point we are inside the with warnings.catch_warnings():
block, which would catch our own warning.
if warning: | ||
warnings.warn(warning, stacklevel=2) | ||
if skipped: | ||
raise skipped |
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.
Same here, couldn't this be simplified by moving the raise Skipped(...)
to directly after the warning?
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.
As per #12220 (comment), we need to raise this after the warning. 👍
with pytest.raises(pytest.skip.Exception): | ||
pytest.importorskip(fn.stem) | ||
|
||
[warning] = captured |
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 love this! Never seen it before, I've always done:
assert len(captured) == 1
warning = captured[0]
for this kind of thing.
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.
All credits go to https://www.cosmicpython.com, seen this pattern there and have been using it ever since. 😁
# 2. Remove `warn_on_import` and the warning handling. | ||
if exc_type is None: | ||
exc_type = ImportError | ||
warn_on_import_error = True |
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 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?
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.
Not sure, but this is definitely being covered by tests (and normal tests, not through pytester
either). I even tested this locally by adding a hard assert 0
in this line to ensure it is hit by the tests, and it is (as expected).
Not sure what's going on.
Co-authored-by: Florian Bruhin <[email protected]>
Can you please mark more related issues/improvements that I can try? |
Thanks again! 👍 We have the good first issue label, though indeed I think we haven't really been doing a good job at applying it to issues recently... |
Yes, there is only one issue with that label now |
With pytest 8.2, pytest.importorskip(...) now only considers ModuleNotFoundError rather than all ImportErrors, and warns otherwise: pytest-dev/pytest#12220 While we could override this via pytest.importorskip(..., exc_type=machinery.Unavailable) this is a simpler solution, and it also makes more sense semantically: We only raise Unavailable when an import is being done that would otherwise result in a ModuleNotFoundError anyways (e.g. trying to import QtWebKit on Qt 6).
Closes #11523
Changed ImportError to ModuleNotFoundError in importorskip()``