Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Doc/c-api/import.rst
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,10 @@ Importing Modules

Sets the current lazy imports filter. The *filter* should be a callable that
will receive ``(importing_module_name, imported_module_name, [fromlist])``
when an import can potentially be lazy and that must return ``True`` if
the import should be lazy and ``False`` otherwise.
when an import can potentially be lazy. The ``imported_module_name`` value
is the resolved module name, so ``lazy from .spam import eggs`` passes
``package.spam``. The callable must return ``True`` if the import should be
lazy and ``False`` otherwise.

Return ``0`` on success and ``-1`` with an exception set otherwise.

Expand Down
4 changes: 3 additions & 1 deletion Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1788,7 +1788,9 @@ always available. Unless explicitly noted otherwise, all variables are read-only
Where:

* *importing_module* is the name of the module doing the import
* *imported_module* is the name of the module being imported
* *imported_module* is the resolved name of the module being imported
(for example, ``lazy from .spam import eggs`` passes
``package.spam``)
* *fromlist* is the tuple of names being imported (for ``from ... import``
statements), or ``None`` for regular imports

Expand Down
183 changes: 183 additions & 0 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,36 @@ def tearDown(self):
sys.set_lazy_imports_filter(None)
sys.set_lazy_imports("normal")

def _run_subprocess_with_modules(self, code, files):
with tempfile.TemporaryDirectory() as tmpdir:
for relpath, contents in files.items():
path = os.path.join(tmpdir, relpath)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as file:
file.write(textwrap.dedent(contents))

env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join(
entry for entry in (tmpdir, env.get("PYTHONPATH")) if entry
)
env["PYTHON_LAZY_IMPORTS"] = "normal"

result = subprocess.run(
[sys.executable, "-c", textwrap.dedent(code)],
capture_output=True,
cwd=tmpdir,
env=env,
text=True,
)
return result

def _assert_subprocess_ok(self, code, files):
result = self._run_subprocess_with_modules(code, files)
self.assertEqual(
result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}"
)
return result

def test_filter_receives_correct_arguments_for_import(self):
"""Filter should receive (importer, name, fromlist=None) for 'import x'."""
code = textwrap.dedent("""
Expand Down Expand Up @@ -1290,6 +1320,159 @@ def deny_filter(importer, name, fromlist):
self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
self.assertIn("EAGER", result.stdout)

def test_filter_distinguishes_absolute_and_relative_from_imports(self):
"""Relative imports should pass resolved module names to the filter."""
files = {
"target.py": """
VALUE = "absolute"
""",
"pkg/__init__.py": "",
"pkg/target.py": """
VALUE = "relative"
""",
"pkg/runner.py": """
import sys

seen = []

def my_filter(importer, name, fromlist):
seen.append((importer, name, fromlist))
return True

sys.set_lazy_imports_filter(my_filter)

lazy from target import VALUE as absolute_value
lazy from .target import VALUE as relative_value

assert seen == [
(__name__, "target", ("VALUE",)),
(__name__, "pkg.target", ("VALUE",)),
], seen
""",
}

result = self._assert_subprocess_ok(
"""
import pkg.runner
print("OK")
""",
files,
)
self.assertIn("OK", result.stdout)

def test_filter_receives_resolved_name_for_relative_package_import(self):
"""'lazy from . import x' should report the resolved package name."""
files = {
"pkg/__init__.py": "",
"pkg/sibling.py": """
VALUE = 1
""",
"pkg/runner.py": """
import sys

seen = []

def my_filter(importer, name, fromlist):
seen.append((importer, name, fromlist))
return True

sys.set_lazy_imports_filter(my_filter)

lazy from . import sibling

assert seen == [
(__name__, "pkg", ("sibling",)),
], seen
""",
}

result = self._assert_subprocess_ok(
"""
import pkg.runner
print("OK")
""",
files,
)
self.assertIn("OK", result.stdout)

def test_filter_receives_resolved_name_for_parent_relative_import(self):
"""Parent relative imports should also use the resolved module name."""
files = {
"pkg/__init__.py": "",
"pkg/target.py": """
VALUE = 1
""",
"pkg/sub/__init__.py": "",
"pkg/sub/runner.py": """
import sys

seen = []

def my_filter(importer, name, fromlist):
seen.append((importer, name, fromlist))
return True

sys.set_lazy_imports_filter(my_filter)

lazy from ..target import VALUE

assert seen == [
(__name__, "pkg.target", ("VALUE",)),
], seen
""",
}

result = self._assert_subprocess_ok(
"""
import pkg.sub.runner
print("OK")
""",
files,
)
self.assertIn("OK", result.stdout)

def test_filter_can_force_eager_only_for_resolved_relative_target(self):
"""Resolved names should let filters treat relative and absolute imports differently."""
files = {
"target.py": """
VALUE = "absolute"
""",
"pkg/__init__.py": "",
"pkg/target.py": """
VALUE = "relative"
""",
"pkg/runner.py": """
import sys

def my_filter(importer, name, fromlist):
return name != "pkg.target"

sys.set_lazy_imports_filter(my_filter)

lazy from target import VALUE as absolute_value
lazy from .target import VALUE as relative_value

assert "pkg.target" in sys.modules, sorted(
name for name in sys.modules
if name in {"target", "pkg.target"}
)
assert "target" not in sys.modules, sorted(
name for name in sys.modules
if name in {"target", "pkg.target"}
)
assert relative_value == "relative", relative_value
""",
}

result = self._assert_subprocess_ok(
"""
import pkg.runner
print("OK")
""",
files,
)
self.assertIn("OK", result.stdout)


class AdditionalSyntaxRestrictionTests(unittest.TestCase):
"""Additional syntax restriction tests per PEP 810."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :func:`sys.set_lazy_imports_filter` so relative lazy imports pass the
resolved imported module name to the filter callback. Patch by Pablo Galindo.
2 changes: 1 addition & 1 deletion Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -4523,7 +4523,7 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
assert(!PyErr_Occurred());
fromlist = Py_NewRef(Py_None);
}
PyObject *args[] = {modname, name, fromlist};
PyObject *args[] = {modname, abs_name, fromlist};
PyObject *res = PyObject_Vectorcall(filter, args, 3, NULL);

Py_DECREF(modname);
Expand Down
2 changes: 1 addition & 1 deletion Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2796,7 +2796,7 @@ The filter is a callable which disables lazy imports when they
would otherwise be enabled. Returns True if the import is still enabled
or False to disable it. The callable is called with:
(importing_module_name, imported_module_name, [fromlist])
(importing_module_name, resolved_imported_module_name, [fromlist])
Pass None to clear the filter.
[clinic start generated code]*/
Expand Down
Loading