-
-
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
Introduce --import-mode=importlib #7246
Introduce --import-mode=importlib #7246
Conversation
Unfortunately importing using Here's the code for Our own @asottile, which is our in-house importlib expert, do you have any thoughts/hints here? |
the difference in assertion rewriting comes down to the call to
|
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.
The changes plumb the importmode
into py's pyimport
. I didn't dig into what this does, but from the added docs it certainty sounds good. And since it's not on by default, there isn't any risk to experiment with it. So LGTM.
src/_pytest/config/__init__.py
Outdated
@@ -512,7 +512,7 @@ def _importconftest(self, conftestpath): | |||
_ensure_removed_sysmodule(conftestpath.purebasename) | |||
|
|||
try: | |||
mod = conftestpath.pyimport() | |||
mod = conftestpath.pyimport(ensuresyspath=importmode) |
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.
One question about this: previously this pyimport
always used the default, even if the importmode was append
(non-default). Now also append
is plumbed into here, which changes the existing behavior. Does that make any difference?
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.
It might change things indeed, but I'm inclined that this was actually an oversight.
What do you think @RonnyPfannschmidt?
Thanks, I was suspecting that. But passing our assertion rewriter as loader won't interfere with other import hooks installed in If I'm right, should we explicitly loop over |
I'll have to think about this some more 🤔 but I suspect you're right that we'd need to implement the |
How does this look like to you? import importlib.util
import sys
def test_foo():
print()
modname = "test_assert_foo"
path = "test_assert_foo.py"
for meta_hook in sys.meta_path:
print('meta_hook=', meta_hook)
x = meta_hook.find_module(modname, path)
if x is not None:
print('x=', x)
spec = importlib.util.spec_from_file_location(modname, path, loader=meta_hook)
print('spec=', spec)
break |
So I went ahead and my But I would need to change I suggest reviewing the individual commits, as there are a lot of changes I'm afraid. |
00f8f52
to
0ad23a7
Compare
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 get a couple of failures running pytest's own tests with --import-mode=importlib
:
FAILED testing/code/test_source.py::test_getfslineno - TypeError: <class 'test_source.test_getfslineno.<locals>.A'> is a built-in class
FAILED testing/test_assertrewrite.py::test_rewrite_infinite_recursion - assert None is not None
hmmm actually, if we're importing a test we always want to use the pytest loader right? then we don't need to search through |
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.
im a bit onflicted on this one, the changes needed to port to pathlib are bi enough to warrnt a folloup instead of a "now"
Not sure TBH... in I might be misunderstanding how
Ahh that's a great idea, I should have done that already, thanks. I will take a look at those later. Thanks about all the other comments regarding the So I understand I will continue on that route. 👍
I'm OK with doing them in this PR, actually. But of course I can do a follow up right after merging this if you folks prefer. |
Did some cleanups/refactorings:
|
725d3ce
to
c943991
Compare
src/_pytest/pathlib.py
Outdated
if str(pkgroot) not in sys.path: | ||
sys.path.append(str(pkgroot)) | ||
else: | ||
assert mode == ImportMode.prepend, "unexpected mode: {}".format(mode) |
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.
There's a nicer way to handle this, if you feel inclined: python/mypy#5818 (works for enums too, though might need to compare the enum values using is
, IIRC).
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.
Seems nice; I did implement it locally but I get:
src\_pytest\pathlib.py:495: error: Argument 1 to "assert_never" has incompatible type "ImportMode"; expected "NoReturn" [arg-type]
Found 1 error in 1 file (checked 2 source files)
I implemented the solution from your original post verbatim, didn't really read the whole thread though.
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.
Here's a diff that works for me. (Again, it's ok if you prefer not to apply this)
diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py
index 84f9609a7..c19beb505 100644
--- a/src/_pytest/compat.py
+++ b/src/_pytest/compat.py
@@ -33,6 +33,7 @@ else:
if TYPE_CHECKING:
+ from typing import NoReturn
from typing import Type
from typing_extensions import Final
@@ -401,3 +402,38 @@ else:
from collections import OrderedDict
order_preserving_dict = OrderedDict
+
+
+# Perform exhaustiveness checking.
+#
+# Consider this example:
+#
+# MyUnion = Union[int, str]
+#
+# def handle(x: MyUnion) -> int {
+# if isinstance(x, int):
+# return 1
+# elif isinstance(x, str):
+# return 2
+# else:
+# raise Exception('unreachable')
+#
+# Now suppose we add a new variant:
+#
+# MyUnion = Union[int, str, bytes]
+#
+# After doing this, we must remember ourselves to go and update the handle
+# function to handle the new variant.
+#
+# With `assert_never` we can do better:
+#
+# // throw new Error('unreachable');
+# return assert_never(x)
+#
+# Now, if we forget to handle the new variant, the type-checker will emit a
+# compile-time error, instead of the runtime error we would have gotten
+# previously.
+#
+# This also work for Enums (if you use `is` to compare) and Literals.
+def assert_never(value: NoReturn) -> NoReturn:
+ assert False, "Unhandled value: {} ({})".format(value, type(value).__name__)
diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py
index 907e9223d..da7f0221c 100644
--- a/src/_pytest/pathlib.py
+++ b/src/_pytest/pathlib.py
@@ -23,6 +23,7 @@ from typing import Union
import py
+from _pytest.compat import assert_never
from _pytest.warning_types import PytestWarning
if sys.version_info[:2] >= (3, 6):
@@ -453,7 +454,7 @@ def import_path(
if not path.exists():
raise ImportError(path)
- if mode == ImportMode.importlib:
+ if mode is ImportMode.importlib:
import importlib.util
module_name = path.stem
@@ -484,13 +485,14 @@ def import_path(
pkg_root = path.parent
module_name = path.stem
- if mode == ImportMode.append:
+ if mode is ImportMode.append:
if str(pkg_root) not in sys.path:
sys.path.append(str(pkg_root))
- else:
- assert mode == ImportMode.prepend, "unexpected mode: {}".format(mode)
+ elif mode is ImportMode.prepend:
if str(pkg_root) != sys.path[0]:
sys.path.insert(0, str(pkg_root))
+ else:
+ assert_never(mode)
__import__(module_name)
src/_pytest/pathlib.py
Outdated
if str(pkgroot) not in sys.path: | ||
sys.path.append(str(pkgroot)) | ||
else: | ||
assert mode == ImportMode.prepend, "unexpected mode: {}".format(mode) |
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.
Here's a diff that works for me. (Again, it's ok if you prefer not to apply this)
diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py
index 84f9609a7..c19beb505 100644
--- a/src/_pytest/compat.py
+++ b/src/_pytest/compat.py
@@ -33,6 +33,7 @@ else:
if TYPE_CHECKING:
+ from typing import NoReturn
from typing import Type
from typing_extensions import Final
@@ -401,3 +402,38 @@ else:
from collections import OrderedDict
order_preserving_dict = OrderedDict
+
+
+# Perform exhaustiveness checking.
+#
+# Consider this example:
+#
+# MyUnion = Union[int, str]
+#
+# def handle(x: MyUnion) -> int {
+# if isinstance(x, int):
+# return 1
+# elif isinstance(x, str):
+# return 2
+# else:
+# raise Exception('unreachable')
+#
+# Now suppose we add a new variant:
+#
+# MyUnion = Union[int, str, bytes]
+#
+# After doing this, we must remember ourselves to go and update the handle
+# function to handle the new variant.
+#
+# With `assert_never` we can do better:
+#
+# // throw new Error('unreachable');
+# return assert_never(x)
+#
+# Now, if we forget to handle the new variant, the type-checker will emit a
+# compile-time error, instead of the runtime error we would have gotten
+# previously.
+#
+# This also work for Enums (if you use `is` to compare) and Literals.
+def assert_never(value: NoReturn) -> NoReturn:
+ assert False, "Unhandled value: {} ({})".format(value, type(value).__name__)
diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py
index 907e9223d..da7f0221c 100644
--- a/src/_pytest/pathlib.py
+++ b/src/_pytest/pathlib.py
@@ -23,6 +23,7 @@ from typing import Union
import py
+from _pytest.compat import assert_never
from _pytest.warning_types import PytestWarning
if sys.version_info[:2] >= (3, 6):
@@ -453,7 +454,7 @@ def import_path(
if not path.exists():
raise ImportError(path)
- if mode == ImportMode.importlib:
+ if mode is ImportMode.importlib:
import importlib.util
module_name = path.stem
@@ -484,13 +485,14 @@ def import_path(
pkg_root = path.parent
module_name = path.stem
- if mode == ImportMode.append:
+ if mode is ImportMode.append:
if str(pkg_root) not in sys.path:
sys.path.append(str(pkg_root))
- else:
- assert mode == ImportMode.prepend, "unexpected mode: {}".format(mode)
+ elif mode is ImportMode.prepend:
if str(pkg_root) != sys.path[0]:
sys.path.insert(0, str(pkg_root))
+ else:
+ assert_never(mode)
__import__(module_name)
7009cad
to
29aa8eb
Compare
Thanks everyone! 👍 |
The initial implementation (in pytest-dev#7246) introduced the `importlib` mode, which never added the imported module to `sys.modules`, so it included a test to ensure calling `import_path` twice would yield different modules. That proved problematic, so we started adding the imported module to `sys.modules` in pytest-dev#7870, but failed to realize that given we are now changing `sys.modules`, we might as well avoid importing it more than once. Then pytest-dev#10088 came along, passing `importlib` also when importing application modules (as opposed to only test modules before), which caused problems due to imports having side-effects and the expectation being that they are imported only once. With this PR, `import_path` returns the module immediately if already in `sys.modules`. Fix pytest-dev#10811, pytest-dev#10341
The initial implementation (in pytest-dev#7246) introduced the `importlib` mode, which never added the imported module to `sys.modules`, so it included a test to ensure calling `import_path` twice would yield different modules. Not adding modules to `sys.modules` proved problematic, so we began to add the imported module to `sys.modules` in pytest-dev#7870, but failed to realize that given we are now changing `sys.modules`, we might as well avoid importing it more than once. Then pytest-dev#10088 came along, passing `importlib` also when importing application modules (as opposed to only test modules before), which caused problems due to imports having side-effects and the expectation being that they are imported only once. With this PR, `import_path` returns the module immediately if already in `sys.modules`. Fix pytest-dev#10811, pytest-dev#10341
The initial implementation (in pytest-dev#7246) introduced the `importlib` mode, which never added the imported module to `sys.modules`, so it included a test to ensure calling `import_path` twice would yield different modules. Not adding modules to `sys.modules` proved problematic, so we began to add the imported module to `sys.modules` in pytest-dev#7870, but failed to realize that given we are now changing `sys.modules`, we might as well avoid importing it more than once. Then pytest-dev#10088 came along, passing `importlib` also when importing application modules (as opposed to only test modules before), which caused problems due to imports having side-effects and the expectation being that they are imported only once. With this PR, `import_path` returns the module immediately if already in `sys.modules`. Fix pytest-dev#10811, pytest-dev#10341
The initial implementation (in pytest-dev#7246) introduced the `importlib` mode, which never added the imported module to `sys.modules`, so it included a test to ensure calling `import_path` twice would yield different modules. Not adding modules to `sys.modules` proved problematic, so we began to add the imported module to `sys.modules` in pytest-dev#7870, but failed to realize that given we are now changing `sys.modules`, we might as well avoid importing it more than once. Then pytest-dev#10088 came along, passing `importlib` also when importing application modules (as opposed to only test modules before), which caused problems due to imports having side-effects and the expectation being that they are imported only once. With this PR, `import_path` returns the module immediately if already in `sys.modules`. Fix pytest-dev#10811, pytest-dev#10341
The initial implementation (in #7246) introduced the `importlib` mode, which never added the imported module to `sys.modules`, so it included a test to ensure calling `import_path` twice would yield different modules. Not adding modules to `sys.modules` proved problematic, so we began to add the imported module to `sys.modules` in #7870, but failed to realize that given we are now changing `sys.modules`, we might as well avoid importing it more than once. Then #10088 came along, passing `importlib` also when importing application modules (as opposed to only test modules before), which caused problems due to imports having side-effects and the expectation being that they are imported only once. With this PR, `import_path` returns the module immediately if already in `sys.modules`. Fix #10811 Fix #10341
This introduces --import-mode=importlib, which uses fine-grained facilities
from importlib to import test modules and conftest files, bypassing
the need to change sys.path and sys.modules as side-effect of that.
I've also opened #7245 to gather feedback on the new import mode.
Fix #5821