mirror of
https://github.com/python/cpython.git
synced 2026-04-16 00:31:03 +00:00
gh-148110: Resolve lazy import filter names for relative imports (#148111)
This commit is contained in:
parent
a0c57a8d17
commit
ca960b6f38
7 changed files with 197 additions and 8 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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("""
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
4
Python/clinic/sysmodule.c.h
generated
4
Python/clinic/sysmodule.c.h
generated
|
|
@ -1830,7 +1830,7 @@ PyDoc_STRVAR(sys_set_lazy_imports_filter__doc__,
|
|||
"would otherwise be enabled. Returns True if the import is still enabled\n"
|
||||
"or False to disable it. The callable is called with:\n"
|
||||
"\n"
|
||||
"(importing_module_name, imported_module_name, [fromlist])\n"
|
||||
"(importing_module_name, resolved_imported_module_name, [fromlist])\n"
|
||||
"\n"
|
||||
"Pass None to clear the filter.");
|
||||
|
||||
|
|
@ -2121,4 +2121,4 @@ exit:
|
|||
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
|
||||
#define SYS_GETANDROIDAPILEVEL_METHODDEF
|
||||
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
|
||||
/*[clinic end generated code: output=adbadb629b98eabf input=a9049054013a1b77]*/
|
||||
/*[clinic end generated code: output=e8333fe10c01ae66 input=a9049054013a1b77]*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -2796,14 +2796,14 @@ 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]*/
|
||||
|
||||
static PyObject *
|
||||
sys_set_lazy_imports_filter_impl(PyObject *module, PyObject *filter)
|
||||
/*[clinic end generated code: output=10251d49469c278c input=2eb48786bdd4ee42]*/
|
||||
/*[clinic end generated code: output=10251d49469c278c input=fd51ed8df6ab54b7]*/
|
||||
{
|
||||
if (PyImport_SetLazyImportsFilter(filter) < 0) {
|
||||
return NULL;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue