From 5eb4ab5a91d5ded26ab6c2e58667bfb2f5ade249 Mon Sep 17 00:00:00 2001 From: Danishk2445 <143554875+Danishk2445@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:51:22 -0500 Subject: [PATCH 1/3] gh-57491: Reduce the EPUB table of contents depth to 2 (#158105) --- Doc/conf.py | 4 ++++ .../2026-09-24-10-10-15.gh-issue-57491.UA-2uN.rst | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2026-09-24-10-10-15.gh-issue-57491.UA-2uN.rst diff --git a/Doc/conf.py b/Doc/conf.py index 0fb8b5cd800cfb0..8791aba435f49ea 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -469,6 +469,10 @@ # https://github.com/sphinx-doc/sphinx/issues/12359 epub_use_index = False +# The default depth of 3 lists over a thousand entries, which makes the +# table of contents hard to navigate in e-book readers +epub_tocdepth = 2 + # translation tag # --------------- diff --git a/Misc/NEWS.d/next/Documentation/2026-09-24-10-10-15.gh-issue-57491.UA-2uN.rst b/Misc/NEWS.d/next/Documentation/2026-09-24-10-10-15.gh-issue-57491.UA-2uN.rst new file mode 100644 index 000000000000000..c7c8057776bec4a --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2026-09-24-10-10-15.gh-issue-57491.UA-2uN.rst @@ -0,0 +1,3 @@ +Reduce the depth of the table of contents in the EPUB documentation from +three levels to two, making it much shorter and easier to navigate in e-book +readers. From 972cfaae3f1349a2b822a0003e96e980c59a402f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:00:10 +0200 Subject: [PATCH 2/3] gh-138912: Add fast path for match class patterns without sub-patterns (#144820) Co-authored-by: Pieter Eendebak --- Doc/library/dis.rst | 52 +++++++++++-------- Include/internal/pycore_intrinsics.h | 3 +- Lib/test/test_capi/test_opt.py | 5 +- ...-02-14-16-19-22.gh-issue-138912.TKMC9K.rst | 2 + Python/codegen.c | 7 +++ Python/intrinsics.c | 20 +++++++ 6 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-16-19-22.gh-issue-138912.TKMC9K.rst diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index ced19268dea2364..73e77f4707cf92a 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -1899,32 +1899,38 @@ iterations of the loop. The operand determines which intrinsic function is called: - +------------------------------------------+-----------------------------------+ - | Operand | Description | - +==========================================+===================================+ - | ``INTRINSIC_2_INVALID`` | Not valid | - +------------------------------------------+-----------------------------------+ - | ``INTRINSIC_PREP_RERAISE_STAR`` | Calculates the | - | | :exc:`ExceptionGroup` to raise | - | | from a ``try-except*``. | - +------------------------------------------+-----------------------------------+ - | ``INTRINSIC_TYPEVAR_WITH_BOUND`` | Creates a :class:`typing.TypeVar` | - | | with a bound. | - +------------------------------------------+-----------------------------------+ - | ``INTRINSIC_TYPEVAR_WITH_CONSTRAINTS`` | Creates a | - | | :class:`typing.TypeVar` with | - | | constraints. | - +------------------------------------------+-----------------------------------+ - | ``INTRINSIC_SET_FUNCTION_TYPE_PARAMS`` | Sets the ``__type_params__`` | - | | attribute of a function. | - +------------------------------------------+-----------------------------------+ - | ``INTRINSIC_ADD_CONDITIONAL_ANNOTATION`` | Adds an annotation index to the | - | | ``__conditional_annotations__`` | - | | set. | - +------------------------------------------+-----------------------------------+ + +------------------------------------------+-----------------------------------------+ + | Operand | Description | + +==========================================+=========================================+ + | ``INTRINSIC_2_INVALID`` | Not valid | + +------------------------------------------+-----------------------------------------+ + | ``INTRINSIC_PREP_RERAISE_STAR`` | Calculates the | + | | :exc:`ExceptionGroup` to raise | + | | from a ``try-except*``. | + +------------------------------------------+-----------------------------------------+ + | ``INTRINSIC_TYPEVAR_WITH_BOUND`` | Creates a :class:`typing.TypeVar` | + | | with a bound. | + +------------------------------------------+-----------------------------------------+ + | ``INTRINSIC_TYPEVAR_WITH_CONSTRAINTS`` | Creates a | + | | :class:`typing.TypeVar` with | + | | constraints. | + +------------------------------------------+-----------------------------------------+ + | ``INTRINSIC_SET_FUNCTION_TYPE_PARAMS`` | Sets the ``__type_params__`` | + | | attribute of a function. | + +------------------------------------------+-----------------------------------------+ + | ``INTRINSIC_ADD_CONDITIONAL_ANNOTATION`` | Adds an annotation index to the | + | | ``__conditional_annotations__`` | + | | set. | + +------------------------------------------+-----------------------------------------+ + | ``INTRINSIC_MATCH_CLASS_ISINSTANCE`` | Do :func:`isinstance` checks for | + | | :ref:`Class patterns `. | + +------------------------------------------+-----------------------------------------+ .. versionadded:: 3.12 + .. versionchanged:: 3.16 + Added ``INTRINSIC_MATCH_CLASS_ISINSTANCE``. + .. opcode:: LOAD_SPECIAL diff --git a/Include/internal/pycore_intrinsics.h b/Include/internal/pycore_intrinsics.h index 447cea91716eca9..e8ddafed9123409 100644 --- a/Include/internal/pycore_intrinsics.h +++ b/Include/internal/pycore_intrinsics.h @@ -31,8 +31,9 @@ #define INTRINSIC_SET_FUNCTION_TYPE_PARAMS 4 #define INTRINSIC_SET_TYPEPARAM_DEFAULT 5 #define INTRINSIC_ADD_CONDITIONAL_ANNOTATION 6 +#define INTRINSIC_MATCH_CLASS_ISINSTANCE 7 -#define MAX_INTRINSIC_2 6 +#define MAX_INTRINSIC_2 7 typedef PyObject *(*intrinsic_func1)(PyThreadState* tstate, PyObject *value); typedef PyObject *(*intrinsic_func2)(PyThreadState* tstate, PyObject *value1, PyObject *value2); diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 36efab518781410..ca0545dba264f2a 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -5752,12 +5752,13 @@ def testfunc(n): def test_match_class(self): def testfunc(n): class A: + __match_args__ = ("val",) val = 1 x = A() ret = 0 for _ in range(n): match x: - case A(): + case A(1): ret += x.val return ret @@ -5766,7 +5767,7 @@ class A: uops = get_opnames(ex) self.assertIn("_MATCH_CLASS", uops) - self.assertEqual(count_ops(ex, "_POP_TOP_NOP"), 4) + self.assertEqual(count_ops(ex, "_POP_TOP_NOP"), 5) def test_dict_update(self): def testfunc(n): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-16-19-22.gh-issue-138912.TKMC9K.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-16-19-22.gh-issue-138912.TKMC9K.rst new file mode 100644 index 000000000000000..6236c6335ceca3c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-14-16-19-22.gh-issue-138912.TKMC9K.rst @@ -0,0 +1,2 @@ +Add fast path for :keyword:`match` class patterns without any sub-patterns +to improve performance by ~15%. diff --git a/Python/codegen.c b/Python/codegen.c index 71ec33724684dc6..7143d9abc4255e8 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -6245,6 +6245,13 @@ codegen_pattern_class(compiler *c, pattern_ty p, pattern_context *pc) if (nattrs) { RETURN_IF_ERROR(validate_kwd_attrs(c, kwd_attrs, kwd_patterns)); } + if (nargs + nattrs == 0) { + // Fast path if there are no sub-patterns + VISIT(c, expr, p->v.MatchClass.cls); + ADDOP_I(c, LOC(p), CALL_INTRINSIC_2, INTRINSIC_MATCH_CLASS_ISINSTANCE); + RETURN_IF_ERROR(jump_to_fail_pop(c, LOC(p), pc, POP_JUMP_IF_FALSE)); + return SUCCESS; + } VISIT(c, expr, p->v.MatchClass.cls); PyObject *attr_names = PyTuple_New(nattrs); if (attr_names == NULL) { diff --git a/Python/intrinsics.c b/Python/intrinsics.c index 52e2dc8a99d8640..dfa81baa654e123 100644 --- a/Python/intrinsics.c +++ b/Python/intrinsics.c @@ -287,6 +287,25 @@ add_conditional_annotation(PyThreadState* tstate, PyObject *conditional_annotati Py_RETURN_NONE; } +static PyObject * +match_class_isinstance(PyThreadState* tstate, PyObject *subject, PyObject *type) +{ + /* Fast path for class patterns with no sub-patterns, e.g. `case C():` + Equivalent to the isinstance check performed by _PyEval_MatchClass, + including the same TypeError when the pattern does not refer to a + class. */ + if (!PyType_Check(type)) { + _PyErr_SetString(tstate, PyExc_TypeError, + "class pattern must refer to a class"); + return NULL; + } + int res = PyObject_IsInstance(subject, type); + if (res < 0) { + return NULL; + } + return res ? Py_True : Py_False; +} + const intrinsic_func2_info _PyIntrinsics_BinaryFunctions[] = { INTRINSIC_FUNC_ENTRY(INTRINSIC_2_INVALID, no_intrinsic2) @@ -296,6 +315,7 @@ _PyIntrinsics_BinaryFunctions[] = { INTRINSIC_FUNC_ENTRY(INTRINSIC_SET_FUNCTION_TYPE_PARAMS, _Py_set_function_type_params) INTRINSIC_FUNC_ENTRY(INTRINSIC_SET_TYPEPARAM_DEFAULT, _Py_set_typeparam_default) INTRINSIC_FUNC_ENTRY(INTRINSIC_ADD_CONDITIONAL_ANNOTATION, add_conditional_annotation) + INTRINSIC_FUNC_ENTRY(INTRINSIC_MATCH_CLASS_ISINSTANCE, match_class_isinstance) }; #undef INTRINSIC_FUNC_ENTRY From 1e8ff18a1215b711136b4377282e392583847d62 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sat, 26 Sep 2026 16:19:25 -0400 Subject: [PATCH 3/3] gh-157757: Fix to make `lazy import a.b as c` import the module `a.b` (#158092) * Import the module a lazy `import a.b as c` names `import a.b as c` compiles to `IMPORT_NAME a.b` followed by `IMPORT_FROM b`. Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM rewrote it into the placeholder `lazy from a import b` produces. Reification then imported `a` alone and read `b` off it, so the module `a.b` was never imported under its own name: an attribute of the package shadowing it answered instead, and `math.pi`, which no module backs, bound the float where the eager statement raises ModuleNotFoundError. Mark the dotted import on the placeholder and keep the whole name on it. Reification imports that name and then walks its components with IMPORT_FROM, which is what the eager statement does. The test pinning `lazy import math.pi as pi` as working is inverted, since the eager statement raises. * Stop excluding test_trace from the lazy-imports-all run It passes now that a lazy `import a.b as c` imports the module: the KeyError on 'test.tracedmodules.testmod' came from the submodule never being imported under its own name. * Rename lz_submodule to lz_dotted_as and trim the comments The flag means "bind the whole dotted name, not the root", which the old name did not say, and import.c already has unrelated lazy_pending_submodules machinery to be confused with. * Chain lazy IMPORT_FROM placeholders instead of flagging dotted imports Each deferred IMPORT_FROM off a placeholder without a fromlist now keeps the previous placeholder in lz_from and the attribute name in lz_attr. Reification walks back to the placeholder IMPORT_NAME left, runs that import, and replays the lookups in order with _PyEval_ImportFrom, which is what the eager bytecode does. This drops the lz_dotted_as flag and also follows a custom __lazy_import__ that returns a placeholder for a different module name. * Chain every deferred IMPORT_FROM onto the previous placeholder `lazy from a import b` now records its lookup the same way as `import a.b as c`, so a placeholder holds either the module name and fromlist or the previous placeholder and an attribute name, and reification has a single path. The import passes only the name being resolved as the fromlist, so accessing b still does not import the other names' submodules. * Treat an empty fromlist on a lazy import placeholder as no fromlist __import__("a.b", fromlist=()) returns the top-level package `a`, the same as fromlist=None, but the placeholder kept the empty tuple. Every consumer of lz_attr then read it as a real fromlist: reification narrowed it to the chained attribute and replayed the lookups on `a.b`, _PyEval_LazyImportFrom took the attribute off sys.modules["a.b"], and the repr named `a.b.attr`. _PyLazyImport_New already collapses None to NULL for exactly this reason, so collapse an empty tuple there too and every site follows. * gh-157757: Preserve empty fromlists for custom import hooks --------- Co-authored-by: Pablo Galindo Salgado --- Lib/test/lazy_imports_all_exclude.txt | 1 - Lib/test/test_lazy_import/__init__.py | 143 +++++++++++++++++- Lib/test/test_lazy_import/data/pkg/broken.py | 2 + ...-09-18-15-20-00.gh-issue-157757.Kq3Lm7.rst | 3 + Objects/lazyimportobject.c | 47 ++++-- Python/ceval.c | 37 +---- Python/import.c | 64 +++++--- 7 files changed, 234 insertions(+), 63 deletions(-) create mode 100644 Lib/test/test_lazy_import/data/pkg/broken.py create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-15-20-00.gh-issue-157757.Kq3Lm7.rst diff --git a/Lib/test/lazy_imports_all_exclude.txt b/Lib/test/lazy_imports_all_exclude.txt index 2680d3b1e4357b7..45796d1db9f0a58 100644 --- a/Lib/test/lazy_imports_all_exclude.txt +++ b/Lib/test/lazy_imports_all_exclude.txt @@ -35,6 +35,5 @@ test_pyrepl test_subprocess test_symtable test_tools -test_trace test_type_annotations test_unittest diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 9147e788d7a81f2..5026c9670d81d23 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -724,10 +724,17 @@ def test_non_package_lazily_imported(self): assert_python_ok("-c", code) def test_non_package_lazily_imported_as(self): - """Doing a dotted lazy import as still works""" + """A dotted lazy import as raises when the name is not a module.""" + # gh-157757: the eager statement raises, so the lazy one raises too. code = textwrap.dedent(""" lazy import math.pi as pi - pi + + try: + pi + except ModuleNotFoundError: + pass + else: + raise AssertionError("ModuleNotFoundError was not raised") """) assert_python_ok("-c", code) @@ -1170,6 +1177,22 @@ def test_accessing_one_name_leaves_others_as_proxies(self): self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}") self.assertIn("OK", result.stdout) + def test_accessing_one_name_imports_only_its_submodule(self): + """Accessing one name should not import the other names' submodules.""" + code = textwrap.dedent(""" + import sys + + lazy from test.test_lazy_import.data.pkg import b, bar, broken + + # Importing bar prints, and importing broken raises. + b.foo() + + assert "test.test_lazy_import.data.pkg.bar" not in sys.modules + assert "test.test_lazy_import.data.pkg.broken" not in sys.modules + """) + rc, out, err = assert_python_ok("-c", code) + self.assertEqual(out, b"") + def test_all_names_reified_after_all_accessed(self): """All names should be reified after each is accessed.""" code = textwrap.dedent(""" @@ -2209,6 +2232,122 @@ def test_import_after_variable_wins(self): ] self.assertIs(module_same_name_var_order2.bar, bar_mod) + def test_lazy_import_as_wins_over_variable(self): + """A dotted lazy import as imports the submodule the variable hides.""" + # gh-157757: importing pkg.b rebinds pkg.b from the variable to the + # module, eagerly and lazily alike. + code = textwrap.dedent(""" + import sys + import test.test_lazy_import.data.pkg as pkg + pkg.b = "hides the b submodule" + + lazy import test.test_lazy_import.data.pkg.b as b + lazy import test.test_lazy_import.data.metasyntactic.foo.bar as bar + + assert b is sys.modules["test.test_lazy_import.data.pkg.b"], b + assert bar is sys.modules[ + "test.test_lazy_import.data.metasyntactic.foo.bar"], bar + """) + assert_python_ok("-c", code) + + def test_dotted_as_of_loaded_module(self): + """A dotted lazy import as binds the module, not a same-named attribute.""" + # importlib.metadata is already loaded and has a `metadata` attribute. + code = textwrap.dedent(""" + import importlib.metadata + import importlib.metadata as eager + + lazy import importlib.metadata as lazily + + assert lazily is eager, lazily + """) + assert_python_ok("-c", code) + + def test_dotted_as_replays_lookups_on_custom_placeholder(self): + """A dotted lazy import as looks up its names on what the hook returned.""" + code = textwrap.dedent(""" + import builtins + import xml.dom + + # In a list, so the hook reading it does not resolve it. + placeholder = [__lazy_import__("xml")] + default = builtins.__lazy_import__ + builtins.__lazy_import__ = lambda *args: placeholder[0] + lazy import fake.dom as dom + builtins.__lazy_import__ = default + + assert dom is xml.dom, dom + """) + assert_python_ok("-c", code) + + def test_empty_fromlist_placeholder_matches_no_fromlist(self): + """An empty fromlist behaves like None.""" + code = textwrap.dedent(""" + expected = "" + # In lists, so reading them does not resolve them. + for fromlist in (None, ()): + same = [__lazy_import__("xml.dom", fromlist=fromlist)] + assert repr(same[0]) == expected, (fromlist, repr(same[0])) + bare = [__lazy_import__("xml.dom")] + assert repr(bare[0]) == expected, repr(bare[0]) + """) + assert_python_ok("-c", code) + + def test_empty_fromlist_preserved_for_custom_import(self): + code = textwrap.dedent(""" + import builtins + import types + + value = object() + module = types.SimpleNamespace(dom=value) + placeholder = [__lazy_import__("xml.dom", fromlist=())] + default_import = builtins.__import__ + default_lazy_import = builtins.__lazy_import__ + calls = [] + + def import_hook(name, globals, locals, fromlist, level): + assert name == "xml.dom", name + assert fromlist == (), fromlist + calls.append(fromlist) + return module + + builtins.__import__ = import_hook + assert placeholder[0].resolve() is module + builtins.__lazy_import__ = lambda *args: placeholder[0] + lazy import fake.dom as dom + assert dom is value + builtins.__import__ = default_import + builtins.__lazy_import__ = default_lazy_import + + assert calls == [(), ()], calls + """) + assert_python_ok("-c", code) + + def test_dotted_as_replays_lookups_on_dotted_placeholder(self): + """A dotted lazy import as replays its names on the hook's package.""" + # importlib.metadata has a `metadata` attribute of its own, which the + # placeholder for importlib must not answer with. + for target in ("xml.dom", "importlib.metadata"): + with self.subTest(target=target): + leaf = target.rpartition(".")[2] + code = textwrap.dedent(f""" + import builtins + import sys + import {target} + + # In a list, so the hook reading it does not resolve it. + placeholder = [__lazy_import__("{target}", fromlist=())] + default = builtins.__lazy_import__ + builtins.__lazy_import__ = lambda *args: placeholder[0] + lazy import fake.{leaf} as {leaf} + builtins.__lazy_import__ = default + + name = repr(globals()["{leaf}"]) + assert name == "", name + assert {leaf} is sys.modules["{target}"], {leaf} + """) + assert_python_ok("-c", code) + class DeletedModuleReimportTests(unittest.TestCase): """Tests for reimporting after module deletion from sys.modules.""" diff --git a/Lib/test/test_lazy_import/data/pkg/broken.py b/Lib/test/test_lazy_import/data/pkg/broken.py new file mode 100644 index 000000000000000..6499aa9e15030a6 --- /dev/null +++ b/Lib/test/test_lazy_import/data/pkg/broken.py @@ -0,0 +1,2 @@ +# Submodule that raises an error during import +raise ValueError("This module always fails to import") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-15-20-00.gh-issue-157757.Kq3Lm7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-15-20-00.gh-issue-157757.Kq3Lm7.rst new file mode 100644 index 000000000000000..bb5377a53cab566 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-18-15-20-00.gh-issue-157757.Kq3Lm7.rst @@ -0,0 +1,3 @@ +Fix a lazy ``import a.b as c`` reading ``b`` off ``a`` instead of importing +the module ``a.b``. It now binds the submodule, and raises +:exc:`ModuleNotFoundError` when no module backs the name. diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 8f7f3f98c291289..e008c076352ebbe 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -14,8 +14,8 @@ PyObject * _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins, PyObject *name, PyObject *fromlist) { PyLazyImportObject *m; - if (!name || !PyUnicode_Check(name)) { - PyErr_SetString(PyExc_TypeError, "expected str for name"); + if (!name || !(PyUnicode_Check(name) || PyLazyImport_CheckExact(name))) { + PyErr_SetString(PyExc_TypeError, "expected str or lazy_import for name"); return NULL; } if (fromlist == Py_None || fromlist == NULL) { @@ -104,16 +104,45 @@ lazy_import_getattro(PyObject *op, PyObject *name) return value; } +// The dotted name of the object that resolving the placeholder returns. static PyObject * -lazy_import_name(PyLazyImportObject *m) +lazy_import_path(PyLazyImportObject *m) { - if (m->lz_attr != NULL) { - if (PyUnicode_Check(m->lz_attr)) { - return PyUnicode_FromFormat("%U.%U", m->lz_from, m->lz_attr); - } - else { - return PyUnicode_FromFormat("%U...", m->lz_from); + if (PyLazyImport_CheckExact(m->lz_from)) { + PyObject *base = lazy_import_path((PyLazyImportObject *)m->lz_from); + if (base == NULL) { + return NULL; } + PyObject *res = PyUnicode_FromFormat("%U.%U", base, m->lz_attr); + Py_DECREF(base); + return res; + } + if (m->lz_attr != NULL && + (!PyTuple_Check(m->lz_attr) || PyTuple_GET_SIZE(m->lz_attr) > 0)) { + return Py_NewRef(m->lz_from); + } + // __import__("a.b") returns the top-level package `a`. + Py_ssize_t dot = PyUnicode_FindChar( + m->lz_from, '.', 0, PyUnicode_GET_LENGTH(m->lz_from), 1 + ); + if (dot == -2) { + return NULL; + } + if (dot < 0) { + return Py_NewRef(m->lz_from); + } + return PyUnicode_Substring(m->lz_from, 0, dot); +} + +static PyObject * +lazy_import_name(PyLazyImportObject *m) +{ + if (PyLazyImport_CheckExact(m->lz_from)) { + return lazy_import_path(m); + } + if (m->lz_attr != NULL && + (!PyTuple_Check(m->lz_attr) || PyTuple_GET_SIZE(m->lz_attr) > 0)) { + return PyUnicode_FromFormat("%U...", m->lz_from); } return Py_NewRef(m->lz_from); } diff --git a/Python/ceval.c b/Python/ceval.c index 8cf02651d9a408f..c1fbcb0611384c5 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3333,7 +3333,13 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje assert(PyUnicode_Check(name)); PyObject *ret; PyLazyImportObject *d = (PyLazyImportObject *)v; - PyObject *mod = PyImport_GetModule(d->lz_from); + PyObject *mod = NULL; + // Only `from a import b` can take b off an already imported a; + // `import a.b as c` has to import a.b first. + if (d->lz_attr != NULL && PyTuple_Check(d->lz_attr) && + PyTuple_GET_SIZE(d->lz_attr) > 0) { + mod = PyImport_GetModule(d->lz_from); + } if (mod != NULL) { // Check if the module already has the attribute, if so, resolve it // eagerly. @@ -3353,34 +3359,7 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje Py_DECREF(mod); } - if (d->lz_attr != NULL) { - if (PyUnicode_Check(d->lz_attr)) { - PyObject *from = PyUnicode_FromFormat( - "%U.%U", d->lz_from, d->lz_attr); - if (from == NULL) { - return NULL; - } - ret = _PyLazyImport_New(frame, d->lz_builtins, from, name); - Py_DECREF(from); - return ret; - } - } - else { - Py_ssize_t dot = PyUnicode_FindChar( - d->lz_from, '.', 0, PyUnicode_GET_LENGTH(d->lz_from), 1 - ); - if (dot >= 0) { - PyObject *from = PyUnicode_Substring(d->lz_from, 0, dot); - if (from == NULL) { - return NULL; - } - ret = _PyLazyImport_New(frame, d->lz_builtins, from, name); - Py_DECREF(from); - return ret; - } - } - ret = _PyLazyImport_New(frame, d->lz_builtins, d->lz_from, name); - return ret; + return _PyLazyImport_New(frame, d->lz_builtins, v, name); } #define CANNOT_CATCH_MSG "catching classes that do not inherit from "\ diff --git a/Python/import.c b/Python/import.c index 037f15d4ca2bafa..2fa63dd01833a64 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3898,6 +3898,25 @@ _PyImport_ResolveName(PyThreadState *tstate, PyObject *name, return resolve_name(tstate, name, globals, level); } +// Look up, in order, the attributes recorded from the root placeholder to lz +// on the module the root's import returned. +static PyObject * +lazy_import_replay_from(PyThreadState *tstate, PyObject *mod, + PyLazyImportObject *lz) +{ + if (!PyLazyImport_CheckExact(lz->lz_from)) { + return Py_NewRef(mod); + } + PyObject *from = lazy_import_replay_from( + tstate, mod, (PyLazyImportObject *)lz->lz_from); + if (from == NULL) { + return NULL; + } + PyObject *obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr); + Py_DECREF(from); + return obj; +} + PyObject * _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) { @@ -3910,6 +3929,13 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) PyLazyImportObject *lz = (PyLazyImportObject *)lazy_import; PyInterpreterState *interp = tstate->interp; + // Walk back to the placeholder IMPORT_NAME left, and the first lookup on it. + PyLazyImportObject *root = lz, *first = NULL; + while (PyLazyImport_CheckExact(root->lz_from)) { + first = root; + root = (PyLazyImportObject *)root->lz_from; + } + // Acquire the global import lock to serialize reification _PyImport_AcquireLock(interp); @@ -3946,7 +3972,7 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) return NULL; } PyErr_SetImportErrorSubclass(PyExc_ImportCycleError, errmsg, - lz->lz_from, NULL); + root->lz_from, NULL); Py_DECREF(errmsg); Py_DECREF(name); _PyImport_ReleaseLock(interp); @@ -3956,24 +3982,20 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) goto error; } - if (lz->lz_attr != NULL) { - if (PyUnicode_Check(lz->lz_attr)) { - fromlist = PyTuple_New(1); - if (fromlist == NULL) { - goto error; - } - Py_INCREF(lz->lz_attr); - PyTuple_SET_ITEM(fromlist, 0, lz->lz_attr); - } - else { - Py_INCREF(lz->lz_attr); - fromlist = lz->lz_attr; + if (root->lz_attr != NULL) { + // `from a import b, c`: import only the name being resolved. + // Keep an empty tuple intact for custom __import__ hooks. + fromlist = first && PyTuple_GET_SIZE(root->lz_attr) > 0 + ? PyTuple_Pack(1, first->lz_attr) + : Py_NewRef(root->lz_attr); + if (fromlist == NULL) { + goto error; } } PyObject *globals = PyEval_GetGlobals(); - if (PyMapping_GetOptionalItem(lz->lz_builtins, &_Py_ID(__import__), + if (PyMapping_GetOptionalItem(root->lz_builtins, &_Py_ID(__import__), &import_func) < 0) { goto error; } @@ -3983,19 +4005,17 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import) } obj = _PyEval_ImportNameWithImport( tstate, import_func, globals, globals, - lz->lz_from, fromlist, _PyLong_GetZero() + root->lz_from, fromlist, _PyLong_GetZero() ); if (obj == NULL) { goto error; } - if (lz->lz_attr != NULL && PyUnicode_Check(lz->lz_attr)) { - PyObject *from = obj; - obj = _PyEval_ImportFrom(tstate, from, lz->lz_attr); - Py_DECREF(from); - if (obj == NULL) { - goto error; - } + PyObject *from = obj; + obj = lazy_import_replay_from(tstate, from, lz); + Py_DECREF(from); + if (obj == NULL) { + goto error; } assert(!PyLazyImport_CheckExact(obj));