importlib: set children as attribute of parent modules (#12208)
Now `importlib` mode will correctly set the imported modules as an attribute of their parent modules. As helpfully posted on #12194, that's how the Python import module works so we should follow suit. In addition, we also try to import the parent modules as part of the process of importing a child module, again mirroring how Python importing works. Fix #12194
This commit is contained in:
parent
ad95d59d61
commit
ff806b239e
|
@ -0,0 +1 @@
|
||||||
|
Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules.
|
|
@ -620,10 +620,6 @@ def _import_module_using_spec(
|
||||||
:param insert_modules:
|
:param insert_modules:
|
||||||
If True, will call insert_missing_modules to create empty intermediate modules
|
If True, will call insert_missing_modules to create empty intermediate modules
|
||||||
for made-up module names (when importing test files not reachable from sys.path).
|
for made-up module names (when importing test files not reachable from sys.path).
|
||||||
Note: we can probably drop insert_missing_modules altogether: instead of
|
|
||||||
generating module names such as "src.tests.test_foo", which require intermediate
|
|
||||||
empty modules, we might just as well generate unique module names like
|
|
||||||
"src_tests_test_foo".
|
|
||||||
"""
|
"""
|
||||||
# Checking with sys.meta_path first in case one of its hooks can import this module,
|
# Checking with sys.meta_path first in case one of its hooks can import this module,
|
||||||
# such as our own assertion-rewrite hook.
|
# such as our own assertion-rewrite hook.
|
||||||
|
@ -636,9 +632,41 @@ def _import_module_using_spec(
|
||||||
|
|
||||||
if spec_matches_module_path(spec, module_path):
|
if spec_matches_module_path(spec, module_path):
|
||||||
assert spec is not None
|
assert spec is not None
|
||||||
|
# Attempt to import the parent module, seems is our responsibility:
|
||||||
|
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
|
||||||
|
parent_module_name, _, name = module_name.rpartition(".")
|
||||||
|
parent_module: Optional[ModuleType] = None
|
||||||
|
if parent_module_name:
|
||||||
|
parent_module = sys.modules.get(parent_module_name)
|
||||||
|
if parent_module is None:
|
||||||
|
# Find the directory of this module's parent.
|
||||||
|
parent_dir = (
|
||||||
|
module_path.parent.parent
|
||||||
|
if module_path.name == "__init__.py"
|
||||||
|
else module_path.parent
|
||||||
|
)
|
||||||
|
# Consider the parent module path as its __init__.py file, if it has one.
|
||||||
|
parent_module_path = (
|
||||||
|
parent_dir / "__init__.py"
|
||||||
|
if (parent_dir / "__init__.py").is_file()
|
||||||
|
else parent_dir
|
||||||
|
)
|
||||||
|
parent_module = _import_module_using_spec(
|
||||||
|
parent_module_name,
|
||||||
|
parent_module_path,
|
||||||
|
parent_dir,
|
||||||
|
insert_modules=insert_modules,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find spec and import this module.
|
||||||
mod = importlib.util.module_from_spec(spec)
|
mod = importlib.util.module_from_spec(spec)
|
||||||
sys.modules[module_name] = mod
|
sys.modules[module_name] = mod
|
||||||
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||||
|
|
||||||
|
# Set this module as an attribute of the parent module (#12194).
|
||||||
|
if parent_module is not None:
|
||||||
|
setattr(parent_module, name, mod)
|
||||||
|
|
||||||
if insert_modules:
|
if insert_modules:
|
||||||
insert_missing_modules(sys.modules, module_name)
|
insert_missing_modules(sys.modules, module_name)
|
||||||
return mod
|
return mod
|
||||||
|
|
|
@ -1126,6 +1126,139 @@ def test_safe_exists(tmp_path: Path) -> None:
|
||||||
assert safe_exists(p) is False
|
assert safe_exists(p) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_sets_module_as_attribute(pytester: Pytester) -> None:
|
||||||
|
"""Unittest test for #12194."""
|
||||||
|
pytester.path.joinpath("foo/bar/baz").mkdir(parents=True)
|
||||||
|
pytester.path.joinpath("foo/__init__.py").touch()
|
||||||
|
pytester.path.joinpath("foo/bar/__init__.py").touch()
|
||||||
|
pytester.path.joinpath("foo/bar/baz/__init__.py").touch()
|
||||||
|
pytester.syspathinsert()
|
||||||
|
|
||||||
|
# Import foo.bar.baz and ensure parent modules also ended up imported.
|
||||||
|
baz = import_path(
|
||||||
|
pytester.path.joinpath("foo/bar/baz/__init__.py"),
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert baz.__name__ == "foo.bar.baz"
|
||||||
|
foo = sys.modules["foo"]
|
||||||
|
assert foo.__name__ == "foo"
|
||||||
|
bar = sys.modules["foo.bar"]
|
||||||
|
assert bar.__name__ == "foo.bar"
|
||||||
|
|
||||||
|
# Check parent modules have an attribute pointing to their children.
|
||||||
|
assert bar.baz is baz
|
||||||
|
assert foo.bar is bar
|
||||||
|
|
||||||
|
# Ensure we returned the "foo.bar" module cached in sys.modules.
|
||||||
|
bar_2 = import_path(
|
||||||
|
pytester.path.joinpath("foo/bar/__init__.py"),
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert bar_2 is bar
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_sets_module_as_attribute_without_init_files(pytester: Pytester) -> None:
|
||||||
|
"""Similar to test_import_sets_module_as_attribute, but without __init__.py files."""
|
||||||
|
pytester.path.joinpath("foo/bar").mkdir(parents=True)
|
||||||
|
pytester.path.joinpath("foo/bar/baz.py").touch()
|
||||||
|
pytester.syspathinsert()
|
||||||
|
|
||||||
|
# Import foo.bar.baz and ensure parent modules also ended up imported.
|
||||||
|
baz = import_path(
|
||||||
|
pytester.path.joinpath("foo/bar/baz.py"),
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert baz.__name__ == "foo.bar.baz"
|
||||||
|
foo = sys.modules["foo"]
|
||||||
|
assert foo.__name__ == "foo"
|
||||||
|
bar = sys.modules["foo.bar"]
|
||||||
|
assert bar.__name__ == "foo.bar"
|
||||||
|
|
||||||
|
# Check parent modules have an attribute pointing to their children.
|
||||||
|
assert bar.baz is baz
|
||||||
|
assert foo.bar is bar
|
||||||
|
|
||||||
|
# Ensure we returned the "foo.bar.baz" module cached in sys.modules.
|
||||||
|
baz_2 = import_path(
|
||||||
|
pytester.path.joinpath("foo/bar/baz.py"),
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert baz_2 is baz
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_sets_module_as_attribute_regression(pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #12194."""
|
||||||
|
pytester.path.joinpath("foo/bar/baz").mkdir(parents=True)
|
||||||
|
pytester.path.joinpath("foo/__init__.py").touch()
|
||||||
|
pytester.path.joinpath("foo/bar/__init__.py").touch()
|
||||||
|
pytester.path.joinpath("foo/bar/baz/__init__.py").touch()
|
||||||
|
f = pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import foo
|
||||||
|
from foo.bar import baz
|
||||||
|
foo.bar.baz
|
||||||
|
|
||||||
|
def test_foo() -> None:
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
pytester.syspathinsert()
|
||||||
|
result = pytester.runpython(f)
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
result = pytester.runpytest("--import-mode=importlib", "--doctest-modules")
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_submodule_not_namespace(pytester: Pytester) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for importing a submodule 'foo.bar' while there is a 'bar' directory
|
||||||
|
reachable from sys.path -- ensuring the top-level module does not end up imported as a namespace
|
||||||
|
package.
|
||||||
|
|
||||||
|
#12194
|
||||||
|
https://github.com/pytest-dev/pytest/pull/12208#issuecomment-2056458432
|
||||||
|
"""
|
||||||
|
pytester.syspathinsert()
|
||||||
|
# Create package 'foo' with a submodule 'bar'.
|
||||||
|
pytester.path.joinpath("foo").mkdir()
|
||||||
|
foo_path = pytester.path.joinpath("foo/__init__.py")
|
||||||
|
foo_path.touch()
|
||||||
|
bar_path = pytester.path.joinpath("foo/bar.py")
|
||||||
|
bar_path.touch()
|
||||||
|
# Create top-level directory in `sys.path` with the same name as that submodule.
|
||||||
|
pytester.path.joinpath("bar").mkdir()
|
||||||
|
|
||||||
|
# Import `foo`, then `foo.bar`, and check they were imported from the correct location.
|
||||||
|
foo = import_path(
|
||||||
|
foo_path,
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
bar = import_path(
|
||||||
|
bar_path,
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert foo.__name__ == "foo"
|
||||||
|
assert bar.__name__ == "foo.bar"
|
||||||
|
assert foo.__file__ is not None
|
||||||
|
assert bar.__file__ is not None
|
||||||
|
assert Path(foo.__file__) == foo_path
|
||||||
|
assert Path(bar.__file__) == bar_path
|
||||||
|
|
||||||
|
|
||||||
class TestNamespacePackages:
|
class TestNamespacePackages:
|
||||||
"""Test import_path support when importing from properly namespace packages."""
|
"""Test import_path support when importing from properly namespace packages."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue