]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142349: Implement PEP 810 - Explicit lazy imports (#142351)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Thu, 12 Feb 2026 00:15:33 +0000 (00:15 +0000)
committerGitHub <noreply@github.com>
Thu, 12 Feb 2026 00:15:33 +0000 (00:15 +0000)
Co-authored-by: T. Wouters <twouters@meta.com >
Co-authored-by: Brittany Reynoso <breynoso@meta.com>
Co-authored-by: Dino Viehland <dinoviehland@meta.com>
138 files changed:
Doc/c-api/exceptions.rst
Doc/c-api/import.rst
Doc/library/ast.rst
Doc/library/exceptions.rst
Doc/library/sys.rst
Doc/library/types.rst
Doc/reference/lexical_analysis.rst
Doc/reference/simple_stmts.rst
Doc/using/cmdline.rst
Doc/whatsnew/3.15.rst
Grammar/python.gram
Include/cpython/initconfig.h
Include/import.h
Include/internal/pycore_ast.h
Include/internal/pycore_ast_state.h
Include/internal/pycore_ceval.h
Include/internal/pycore_compile.h
Include/internal/pycore_dict.h
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_import.h
Include/internal/pycore_interp_structs.h
Include/internal/pycore_lazyimportobject.h [new file with mode: 0644]
Include/internal/pycore_magic_number.h
Include/internal/pycore_moduleobject.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_symtable.h
Include/internal/pycore_unicodeobject_generated.h
Include/pyerrors.h
Lib/_compat_pickle.py
Lib/_pyrepl/utils.py
Lib/dis.py
Lib/idlelib/colorizer.py
Lib/idlelib/idle_test/test_colorizer.py
Lib/importlib/_bootstrap.py
Lib/keyword.py
Lib/rlcompleter.py
Lib/test/.ruff.toml
Lib/test/exception_hierarchy.txt
Lib/test/test_ast/data/ast_repr.txt
Lib/test/test_ast/snippets.py
Lib/test/test_ast/test_ast.py
Lib/test/test_capi/test_config.py
Lib/test/test_dis.py
Lib/test/test_embed.py
Lib/test/test_import/data/lazy_imports/basic2.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_compatibility_mode.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_compatibility_mode_relative.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_compatibility_mode_used.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_dir.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_from_unused.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_unused.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/basic_used.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/broken_attr_module.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/broken_module.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/compatibility_mode_func.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/compatibility_mode_try_except.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/dunder_lazy_import.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/dunder_lazy_import_builtins.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/dunder_lazy_import_used.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/eager_import_func.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/global_filter.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/global_filter_from.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/global_filter_from_true.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/global_filter_true.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/global_off.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/global_on.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/globals_access.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_class_body.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_compat_from.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_future_import.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_get_value.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_import_func.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_import_pkg.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_try_except.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_try_except_from.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_try_except_from_star.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_with.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/lazy_with_from.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/modules_dict.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/modules_getattr.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/modules_getattr_other.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/multi_from_import.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/pkg/__init__.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/pkg/b.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/pkg/bar.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/pkg/c.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/relative_lazy.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/relative_lazy_from.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/try_except_eager.py [new file with mode: 0644]
Lib/test/test_import/data/lazy_imports/try_except_eager_from.py [new file with mode: 0644]
Lib/test/test_import/test_lazy_imports.py [new file with mode: 0644]
Lib/test/test_pyrepl/test_utils.py
Lib/test/test_syntax.py
Lib/test/test_sys.py
Lib/test/test_traceback.py
Lib/test/test_types.py
Lib/traceback.py
Lib/types.py
Makefile.pre.in
Misc/NEWS.d/next/Core_and_Builtins/2025-12-06-15-46-32.gh-issue-142349.IdTuYL.rst [new file with mode: 0644]
Modules/_testcapi/import.c
Modules/_testinternalcapi/test_cases.c.h
Modules/_typesmodule.c
Objects/dictobject.c
Objects/exceptions.c
Objects/lazyimportobject.c [new file with mode: 0644]
Objects/moduleobject.c
PCbuild/_freeze_module.vcxproj
PCbuild/_freeze_module.vcxproj.filters
PCbuild/pythoncore.vcxproj
PCbuild/pythoncore.vcxproj.filters
Parser/Python.asdl
Parser/action_helpers.c
Parser/parser.c
Parser/pegen.h
Programs/test_frozenmain.h
Python/Python-ast.c
Python/bltinmodule.c
Python/bytecodes.c
Python/ceval.c
Python/ceval.h
Python/clinic/bltinmodule.c.h
Python/clinic/import.c.h
Python/clinic/sysmodule.c.h
Python/codegen.c
Python/compile.c
Python/executor_cases.c.h
Python/generated_cases.c.h
Python/import.c
Python/initconfig.c
Python/jit.c
Python/pylifecycle.c
Python/specialize.c
Python/symtable.c
Python/sysmodule.c
Tools/c-analyzer/cpython/globals-to-fix.tsv
Tools/jit/template.c

index 59af470f59ff345df93d1209587e3eea4e18e266..a1cfb8872345cf9393703a264a5ae2bd45e155d1 100644 (file)
@@ -1119,6 +1119,8 @@ Exception types
      * :exc:`FloatingPointError`
    * * .. c:var:: PyObject *PyExc_GeneratorExit
      * :exc:`GeneratorExit`
+   * * .. c:var:: PyObject *PyExc_ImportCycleError
+     * :exc:`ImportCycleError`
    * * .. c:var:: PyObject *PyExc_ImportError
      * :exc:`ImportError`
    * * .. c:var:: PyObject *PyExc_IndentationError
index a28c0713dd3b2fe8ed2b05595776af2f1b49fa0c..04b5adb9a8f43d80eb5db6961e5d3fb66c673130 100644 (file)
@@ -346,6 +346,58 @@ Importing Modules
 
    .. versionadded:: 3.14
 
+.. c:function:: PyImport_LazyImportsMode PyImport_GetLazyImportsMode()
+
+   Gets the current lazy imports mode.
+
+   .. versionadded:: next
+
+.. c:function:: PyObject* PyImport_GetLazyImportsFilter()
+
+   Return a :term:`strong reference` to the current lazy imports filter,
+   or ``NULL`` if none exists. This function always succeeds.
+
+   .. versionadded:: next
+
+.. c:function:: int PyImport_SetLazyImportsMode(PyImport_LazyImportsMode mode)
+
+   Similar to :c:func:`PyImport_ImportModuleAttr`, but names are UTF-8 encoded
+   strings instead of Python :class:`str` objects.
+
+   This function always returns ``0``.
+
+   .. versionadded:: next
+
+.. c:function:: int PyImport_SetLazyImportsFilter(PyObject *filter)
+
+   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.
+
+   Return ``0`` on success and ``-1`` with an exception set otherwise.
+
+   .. versionadded:: next
+
+.. c:type:: PyImport_LazyImportsMode
+
+   Enumeration of possible lazy import modes.
+
+   .. c:enumerator:: PyImport_LAZY_NORMAL
+
+      Respect the ``lazy`` keyword in source code. This is the default mode.
+
+   .. c:enumerator:: PyImport_LAZY_ALL
+
+      Make all imports lazy by default.
+
+   .. c:enumerator:: PyImport_LAZY_NONE
+
+      Disable lazy imports entirely. Even explicit ``lazy`` statements become
+      eager imports.
+
+   .. versionadded:: next
+
 .. c:function:: PyObject* PyImport_CreateModuleFromInitfunc(PyObject *spec, PyObject* (*initfunc)(void))
 
    This function is a building block that enables embedders to implement
index ee7ce15c48b589380d12fb37cf67c5f13ac8379d..8815187ea8c8844e9ff4a8f75d69fe3380693fa1 100644 (file)
@@ -1114,7 +1114,8 @@ Imports
                     names=[
                         alias(name='x'),
                         alias(name='y'),
-                        alias(name='z')])])
+                        alias(name='z')],
+                    is_lazy=0)])
 
 
 .. class:: ImportFrom(module, names, level)
@@ -1135,7 +1136,8 @@ Imports
                         alias(name='x'),
                         alias(name='y'),
                         alias(name='z')],
-                    level=0)])
+                    level=0,
+                    is_lazy=0)])
 
 
 .. class:: alias(name, asname)
@@ -1153,7 +1155,8 @@ Imports
                     names=[
                         alias(name='a', asname='b'),
                         alias(name='c')],
-                    level=2)])
+                    level=2,
+                    is_lazy=0)])
 
 Control flow
 ^^^^^^^^^^^^
index f3aca1ba49257b74a8ff296a550a47fb34e4ac15..33f37bdf1fc1cd6e02ffe982e0dc89d402f7d7e9 100644 (file)
@@ -266,6 +266,12 @@ The following exceptions are the exceptions that are usually raised.
 
    .. versionadded:: 3.6
 
+.. exception:: ImportCycleError
+
+   A subclass of :exc:`ImportError` which is raised when a lazy import fails
+   because it (directly or indirectly) tries to import itself.
+
+   .. versionadded:: next
 
 .. exception:: IndexError
 
index 76b205691bfeb4cfc54e7c6776054694503b3cc9..4c76feafc9b492268b31f8b0a5b01ea4d95cf961 100644 (file)
@@ -911,6 +911,35 @@ always available. Unless explicitly noted otherwise, all variables are read-only
 
    .. versionadded:: 3.11
 
+
+.. function:: get_lazy_imports()
+
+   Returns the current lazy imports mode as a string.
+
+   * ``"normal"``: Only imports explicitly marked with the ``lazy`` keyword
+     are lazy
+   * ``"all"``: All top-level imports are potentially lazy
+   * ``"none"``: All lazy imports are suppressed (even explicitly marked
+     ones)
+
+   See also :func:`set_lazy_imports` and :pep:`810`.
+
+   .. versionadded:: next
+
+
+.. function:: get_lazy_imports_filter()
+
+   Returns the current lazy imports filter function, or ``None`` if no
+   filter is set.
+
+   The filter function is called for every potentially lazy import to
+   determine whether it should actually be lazy. See
+   :func:`set_lazy_imports_filter` for details on the filter function
+   signature.
+
+   .. versionadded:: next
+
+
 .. function:: getrefcount(object)
 
    Return the reference count of the *object*.  The count returned is generally one
@@ -1719,6 +1748,61 @@ always available. Unless explicitly noted otherwise, all variables are read-only
 
    .. versionadded:: 3.11
 
+
+.. function:: set_lazy_imports(mode)
+
+   Sets the global lazy imports mode. The *mode* parameter must be one of
+   the following strings:
+
+   * ``"normal"``: Only imports explicitly marked with the ``lazy`` keyword
+     are lazy
+   * ``"all"``: All top-level imports become potentially lazy
+   * ``"none"``: All lazy imports are suppressed (even explicitly marked
+     ones)
+
+   This function is intended for advanced users who need to control lazy
+   imports across their entire application. Library developers should
+   generally not use this function as it affects the runtime execution of
+   applications.
+
+   In addition to the mode, lazy imports can be controlled via the filter
+   provided by :func:`set_lazy_imports_filter`.
+
+   See also :func:`get_lazy_imports` and :pep:`810`.
+
+   .. versionadded:: next
+
+
+.. function:: set_lazy_imports_filter(filter)
+
+   Sets the lazy imports filter callback. The *filter* parameter must be a
+   callable or ``None`` to clear the filter.
+
+   The filter function is called for every potentially lazy import to
+   determine whether it should actually be lazy. It must have the following
+   signature::
+
+      def filter(importing_module: str, imported_module: str,
+                 fromlist: tuple[str, ...] | None) -> bool
+
+   Where:
+
+   * *importing_module* is the name of the module doing the import
+   * *imported_module* is the name of the module being imported
+   * *fromlist* is the tuple of names being imported (for ``from ... import``
+     statements), or ``None`` for regular imports
+
+   The filter should return ``True`` to allow the import to be lazy, or
+   ``False`` to force an eager import.
+
+   This is an advanced feature intended for specialized users who need
+   fine-grained control over lazy import behavior.
+
+   See also :func:`get_lazy_imports_filter` and :pep:`810`.
+
+   .. versionadded:: next
+
+
 .. function:: setprofile(profilefunc)
 
    .. index::
index 40b5f3db13de5f05945feec05f41f26255047f3b..01f4df3c89091f13ec6937e24694c57bf62fda94 100644 (file)
@@ -343,6 +343,18 @@ Standard names are defined for the following types:
    .. seealso:: :pep:`667`
 
 
+.. data:: LazyImportType
+
+   The type of lazy import proxy objects. These objects are created when a
+   module is lazily imported and serve as placeholders until the module is
+   actually accessed. This type can be used to detect lazy imports
+   programmatically.
+
+   .. versionadded:: next
+
+   .. seealso:: :pep:`810`
+
+
 .. data:: GetSetDescriptorType
 
    The type of objects defined in extension modules with ``PyGetSetDef``, such
index 046c759854c4dfa6708ebe3bcb7cd54d8476149b..5c931683db100ac7d4777c1241b524e9f9f02cd5 100644 (file)
@@ -457,6 +457,7 @@ Some names are only reserved under specific contexts. These are known as
 
 - ``match``, ``case``, and ``_``, when used in the :keyword:`match` statement.
 - ``type``, when used in the :keyword:`type` statement.
+- ``lazy``, when used before an :keyword:`import` statement.
 
 These syntactically act as keywords in their specific contexts,
 but this distinction is done at the parser level, not when tokenizing.
@@ -468,6 +469,9 @@ identifier names.
 .. versionchanged:: 3.12
    ``type`` is now a soft keyword.
 
+.. versionchanged:: next
+   ``lazy`` is now a soft keyword.
+
 .. index::
    single: _, identifiers
    single: __, identifiers
index 36b30c9b16b0dbc1193947801c31ad43006ea542..9ada6f047843b4993af4e7a0ed98e8f342cd5dc1 100644 (file)
@@ -743,14 +743,15 @@ The :keyword:`!import` statement
    pair: name; binding
    pair: keyword; from
    pair: keyword; as
+   pair: keyword; lazy
    pair: exception; ImportError
    single: , (comma); import statement
 
 .. productionlist:: python-grammar
-   import_stmt: "import" `module` ["as" `identifier`] ("," `module` ["as" `identifier`])*
-              : | "from" `relative_module` "import" `identifier` ["as" `identifier`]
+   import_stmt: ["lazy"] "import" `module` ["as" `identifier`] ("," `module` ["as" `identifier`])*
+              : | ["lazy"] "from" `relative_module` "import" `identifier` ["as" `identifier`]
               : ("," `identifier` ["as" `identifier`])*
-              : | "from" `relative_module` "import" "(" `identifier` ["as" `identifier`]
+              : | ["lazy"] "from" `relative_module` "import" "(" `identifier` ["as" `identifier`]
               : ("," `identifier` ["as" `identifier`])* [","] ")"
               : | "from" `relative_module` "import" "*"
    module: (`identifier` ".")* `identifier`
@@ -869,6 +870,56 @@ determine dynamically the modules to be loaded.
 
 .. _normalization form: https://www.unicode.org/reports/tr15/#Norm_Forms
 
+.. _lazy-imports:
+.. _lazy:
+
+Lazy imports
+------------
+
+.. index::
+   pair: lazy; import
+   single: lazy import
+
+The :keyword:`lazy` keyword is a :ref:`soft keyword <soft-keywords>` that
+only has special meaning when it appears immediately before an
+:keyword:`import` or :keyword:`from` statement. When an import statement is
+preceded by the :keyword:`lazy` keyword, the import becomes *lazy*: the
+module is not loaded immediately at the import statement. Instead, a lazy
+proxy object is created and bound to the name. The actual module is loaded
+on first use of that name.
+
+Lazy imports are only permitted at module scope. Using :keyword:`lazy`
+inside a function, class body, or
+:keyword:`try`/:keyword:`except`/:keyword:`finally` block raises a
+:exc:`SyntaxError`. Star imports cannot be lazy (``lazy from module import
+*`` is a syntax error), and :ref:`future statements <future>` cannot be
+lazy.
+
+When using ``lazy from ... import``, each imported name is bound to a lazy
+proxy object. The first access to any of these names triggers loading of the
+entire module and resolves only that specific name to its actual value.
+Other names remain as lazy proxies until they are accessed.
+
+Example::
+
+   lazy import json
+   import sys
+
+   print('json' in sys.modules)  # False - json module not yet loaded
+
+   # First use triggers loading
+   result = json.dumps({"hello": "world"})
+
+   print('json' in sys.modules)  # True - now loaded
+
+If an error occurs during module loading (such as :exc:`ImportError` or
+:exc:`SyntaxError`), it is raised at the point where the lazy import is first
+used, not at the import statement itself.
+
+See :pep:`810` for the full specification of lazy imports.
+
+.. versionadded:: next
+
 .. _future:
 
 Future statements
index 515424424c17732de58c9ff24c3616dd523214d0..2e7ea7b4fc4cbac68d663f729d44c7f09c8e9d76 100644 (file)
@@ -694,6 +694,14 @@ Miscellaneous options
 
      .. versionadded:: 3.14
 
+   * :samp:`-X lazy_imports={all,none,normal}` controls lazy import behavior.
+     ``all`` makes all imports lazy by default, ``none`` disables lazy imports
+     entirely (even explicit ``lazy`` statements become eager), and ``normal``
+     (the default) respects the ``lazy`` keyword in source code.
+     See also :envvar:`PYTHON_LAZY_IMPORTS`.
+
+     .. versionadded:: next
+
    It also allows passing arbitrary values and retrieving them through the
    :data:`sys._xoptions` dictionary.
 
@@ -1360,6 +1368,17 @@ conflict.
 
    .. versionadded:: 3.14
 
+.. envvar:: PYTHON_LAZY_IMPORTS
+
+   Controls lazy import behavior. Accepts three values: ``all`` makes all
+   imports lazy by default, ``none`` disables lazy imports entirely (even
+   explicit ``lazy`` statements become eager), and ``normal`` (the default)
+   respects the ``lazy`` keyword in source code.
+
+   See also the :option:`-X lazy_imports <-X>` command-line option.
+
+   .. versionadded:: next
+
 Debug-mode variables
 ~~~~~~~~~~~~~~~~~~~~
 
index 7ab549475152a9fc6f5120c97f0007f4510b71a3..0e440ccfd011f07c7efb690ecc5bba4a08244068 100644 (file)
@@ -65,6 +65,8 @@ Summary -- Release highlights
 
 .. PEP-sized items next.
 
+* :pep:`810`: :ref:`Explicit lazy imports for faster startup times
+  <whatsnew315-pep810>`
 * :pep:`799`: :ref:`A dedicated profiling package for organizing Python
   profiling tools <whatsnew315-profiling-package>`
 * :pep:`799`: :ref:`Tachyon: High frequency statistical sampling profiler
@@ -82,6 +84,102 @@ Summary -- Release highlights
 New features
 ============
 
+.. _whatsnew315-pep810:
+
+:pep:`810`: Explicit lazy imports
+---------------------------------
+
+Large Python applications often suffer from slow startup times. A
+significant contributor to this problem is the import system: when a module
+is imported, Python must locate the file, read it from disk, compile it to
+bytecode, and execute all top-level code. For applications with deep
+dependency trees, this process can take seconds, even when most of the
+imported code is never actually used during a particular run.
+
+Developers have worked around this by moving imports inside functions, using
+:mod:`importlib` to load modules on demand, or restructuring code to avoid
+unnecessary dependencies. These approaches work but make code harder to read
+and maintain, scatter import statements throughout the codebase, and require
+discipline to apply consistently.
+
+Python now provides a cleaner solution through explicit :keyword:`lazy`
+imports using the new ``lazy`` soft keyword. When you mark an import as
+lazy, Python defers the actual module loading until the imported name is
+first used. This gives you the organizational benefits of declaring all
+imports at the top of the file while only paying the loading cost for
+modules you actually use.
+
+The ``lazy`` keyword works with both ``import`` and ``from ... import``
+statements. When you write ``lazy import heavy_module``, Python does not
+immediately load the module. Instead, it creates a lightweight proxy object.
+The actual module loading happens transparently when you first access the
+name:
+
+.. code-block:: python
+
+   lazy import json
+   lazy from datetime import datetime
+
+   print("Starting up...")  # json and datetime not loaded yet
+
+   data = json.loads('{"key": "value"}')  # json gets loads here
+   now = datetime()  # datetime loads here
+
+This mechanism is particularly useful for applications that import many
+modules at the top level but may only use a subset of them in any given run.
+The deferred loading reduces startup latency without requiring code
+restructuring or conditional imports scattered throughout the codebase.
+
+In the case where loading a lazily imported module fails (for example, if
+the module does not exist), Python raises the exception at the point of
+first use rather than at import time. The associated traceback includes both
+the location where the name was accessed and the original import statement,
+making it straightforward to diagnose & debug the failure.
+
+For cases where you want to enable lazy loading globally without modifying
+source code, Python provides the :option:`-X lazy_imports <-X>` command-line
+option and the :envvar:`PYTHON_LAZY_IMPORTS` environment variable. Both
+accept three values: ``all`` makes all imports lazy by default, ``none``
+disables lazy imports entirely (even explicit ``lazy`` statements become
+eager), and ``normal`` (the default) respects the ``lazy`` keyword in source
+code. The :func:`sys.set_lazy_imports` and :func:`sys.get_lazy_imports`
+functions allow changing and querying this mode at runtime.
+
+For more selective control, :func:`sys.set_lazy_imports_filter` accepts a
+callable that determines whether a specific module should be loaded lazily.
+The filter receives three arguments: the importing module's name (or
+``None``), the imported module's name, and the fromlist (or ``None`` for
+regular imports). It should return ``True`` to allow the import to be lazy,
+or ``False`` to force eager loading. This allows patterns like making only
+your own application's modules lazy while keeping third-party dependencies
+eager:
+
+.. code-block:: python
+
+   import sys
+
+   def myapp_filter(importing, imported, fromlist):
+       return imported.startswith("myapp.")
+   sys.set_lazy_imports_filter(myapp_filter)
+   sys.set_lazy_imports("all")
+
+   import myapp.slow_module  # lazy (matches filter)
+   import json               # eager (does not match filter)
+
+The proxy type itself is available as :data:`types.LazyImportType` for code
+that needs to detect lazy imports programmatically.
+
+There are some restrictions on where the ``lazy`` keyword can be used. Lazy
+imports are only permitted at module scope; using ``lazy`` inside a
+function, class body, or ``try``/``except``/``finally`` block raises a
+:exc:`SyntaxError`. Neither star imports nor future imports can be lazy
+(``lazy from module import *`` and ``lazy from __future__ import ...`` both
+raise :exc:`SyntaxError`).
+
+.. seealso:: :pep:`810` for the full specification and rationale.
+
+(Contributed by Pablo Galindo Salgado and Dino Viehland in :gh:`142349`.)
+
 .. _whatsnew315-profiling-package:
 
 :pep:`799`: A dedicated profiling package
@@ -840,6 +938,13 @@ symtable
   (Contributed by Yashp002 in :gh:`143504`.)
 
 
+symtable
+--------
+
+* Add :meth:`symtable.Function.get_cells` and :meth:`symtable.Symbol.is_cell` methods.
+  (Contributed by Yashp002 in :gh:`143504`.)
+
+
 sys
 ---
 
index 9698d6e20100c621847ef9a6daa30e4f448303cc..1212e8640a1a9ca267c17d4d24eb3a21c0a92cf2 100644 (file)
@@ -121,9 +121,9 @@ simple_stmts[asdl_stmt_seq*]:
 simple_stmt[stmt_ty] (memo):
     | assignment
     | &"type" type_alias
+    | &('import' | 'from' | "lazy") import_stmt
     | e=star_expressions { _PyAST_Expr(e, EXTRA) }
     | &'return' return_stmt
-    | &('import' | 'from') import_stmt
     | &'raise' raise_stmt
     | &'pass' pass_stmt
     | &'del' del_stmt
@@ -216,7 +216,7 @@ assert_stmt[stmt_ty]:
     | invalid_assert_stmt
     | 'assert' a=expression b=[',' z=expression { z }] { _PyAST_Assert(a, b, EXTRA) }
 
-import_stmt[stmt_ty]:
+import_stmt[stmt_ty](memo):
     | invalid_import
     | import_name
     | import_from
@@ -224,13 +224,15 @@ import_stmt[stmt_ty]:
 # Import statements
 # -----------------
 
-import_name[stmt_ty]: 'import' a=dotted_as_names { _PyAST_Import(a, EXTRA) }
+import_name[stmt_ty]:
+    | lazy="lazy"? 'import' a=dotted_as_names { _PyAST_Import(a, lazy ? 1 : 0, EXTRA) }
+
 # note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
 import_from[stmt_ty]:
-    | 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
-        _PyPegen_checked_future_import(p, b->v.Name.id, c, _PyPegen_seq_count_dots(a), EXTRA) }
-    | 'from' a=('.' | '...')+ 'import' b=import_from_targets {
-        _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), EXTRA) }
+    | lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
+        _PyPegen_checked_future_import(p, b->v.Name.id, c, _PyPegen_seq_count_dots(a), lazy, EXTRA) }
+    | lazy="lazy"? 'from' a=('.' | '...')+ 'import' b=import_from_targets {
+        _PyAST_ImportFrom(NULL, b, _PyPegen_seq_count_dots(a), lazy ? 1 : 0, EXTRA) }
 import_from_targets[asdl_alias_seq*]:
     | '(' a=import_from_as_names [','] ')' { a }
     | import_from_as_names !','
index 5606ebeb7c95e04e313eceaead47ce2b92154dbc..1ccc496c63ac78026b048ac480cf13a6b9f5140f 100644 (file)
@@ -191,6 +191,7 @@ typedef struct PyConfig {
     int enable_gil;
     int tlbc_enabled;
 #endif
+    int lazy_imports;
 
     /* --- Path configuration inputs ------------ */
     int pathconfig_warnings;
index d91ebe96ca868d139168c3a0c1333fbba001dee9..cc7ad71f2676a224def1216c4c659ec219a8c131 100644 (file)
@@ -88,6 +88,20 @@ PyAPI_FUNC(int) PyImport_AppendInittab(
     PyObject* (*initfunc)(void)
     );
 
+typedef enum {
+    PyImport_LAZY_NORMAL,
+    PyImport_LAZY_ALL,
+    PyImport_LAZY_NONE,
+} PyImport_LazyImportsMode;
+
+#ifndef Py_LIMITED_API
+PyAPI_FUNC(int) PyImport_SetLazyImportsMode(PyImport_LazyImportsMode mode);
+PyAPI_FUNC(int) PyImport_SetLazyImportsFilter(PyObject *filter);
+
+PyAPI_FUNC(PyImport_LazyImportsMode) PyImport_GetLazyImportsMode(void);
+PyAPI_FUNC(PyObject *) PyImport_GetLazyImportsFilter(void);
+#endif
+
 #ifndef Py_LIMITED_API
 #  define Py_CPYTHON_IMPORT_H
 #  include "cpython/import.h"
index 60367202bab6370a027998275208863939fad654..b47398669bbe513464e3887c45a9f154ba4c45f4 100644 (file)
@@ -329,12 +329,14 @@ struct _stmt {
 
         struct {
             asdl_alias_seq *names;
+            int is_lazy;
         } Import;
 
         struct {
             identifier module;
             asdl_alias_seq *names;
             int level;
+            int is_lazy;
         } ImportFrom;
 
         struct {
@@ -764,11 +766,12 @@ stmt_ty _PyAST_TryStar(asdl_stmt_seq * body, asdl_excepthandler_seq * handlers,
                        end_col_offset, PyArena *arena);
 stmt_ty _PyAST_Assert(expr_ty test, expr_ty msg, int lineno, int col_offset,
                       int end_lineno, int end_col_offset, PyArena *arena);
-stmt_ty _PyAST_Import(asdl_alias_seq * names, int lineno, int col_offset, int
-                      end_lineno, int end_col_offset, PyArena *arena);
+stmt_ty _PyAST_Import(asdl_alias_seq * names, int is_lazy, int lineno, int
+                      col_offset, int end_lineno, int end_col_offset, PyArena
+                      *arena);
 stmt_ty _PyAST_ImportFrom(identifier module, asdl_alias_seq * names, int level,
-                          int lineno, int col_offset, int end_lineno, int
-                          end_col_offset, PyArena *arena);
+                          int is_lazy, int lineno, int col_offset, int
+                          end_lineno, int end_col_offset, PyArena *arena);
 stmt_ty _PyAST_Global(asdl_identifier_seq * names, int lineno, int col_offset,
                       int end_lineno, int end_col_offset, PyArena *arena);
 stmt_ty _PyAST_Nonlocal(asdl_identifier_seq * names, int lineno, int
index d4ac419f51d6b2d2c46de6f7b6aa89a8027a9f09..1caf200ee34b2a48aecb70b636ae08bfbd1bcff4 100644 (file)
@@ -205,6 +205,7 @@ struct ast_state {
     PyObject *id;
     PyObject *ifs;
     PyObject *is_async;
+    PyObject *is_lazy;
     PyObject *items;
     PyObject *iter;
     PyObject *key;
index e9f1f65e53cec12ce8fd78a0befce8606cb27440..1ee1f83082757672d4852812a66634b31c44f4b5 100644 (file)
@@ -310,7 +310,19 @@ PyAPI_FUNC(void) _PyEval_FormatExcCheckArg(PyThreadState *tstate, PyObject *exc,
 PyAPI_FUNC(void) _PyEval_FormatExcUnbound(PyThreadState *tstate, PyCodeObject *co, int oparg);
 PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs);
 PyAPI_FUNC(PyObject *) _PyEval_ImportFrom(PyThreadState *, PyObject *, PyObject *);
-PyAPI_FUNC(PyObject *) _PyEval_ImportName(PyThreadState *, _PyInterpreterFrame *, PyObject *, PyObject *, PyObject *);
+
+PyAPI_FUNC(PyObject *) _PyEval_LazyImportName(
+    PyThreadState *tstate, PyObject *builtins, PyObject *globals,
+    PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level,
+    int lazy);
+PyAPI_FUNC(PyObject *) _PyEval_LazyImportFrom(
+    PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *v, PyObject *name);
+PyAPI_FUNC(PyObject *) _PyEval_ImportName(
+    PyThreadState *tstate, PyObject *builtins, PyObject *globals,
+    PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level);
+PyObject * _PyEval_ImportNameWithImport(
+    PyThreadState *tstate, PyObject *import_func, PyObject *globals,
+    PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level);
 PyAPI_FUNC(PyObject *)_PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, Py_ssize_t nargs, PyObject *kwargs);
 PyAPI_FUNC(PyObject *)_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys);
 PyAPI_FUNC(void) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr);
index 527141b54d0dca2c85c317156ff8476dfaa589c5..911cc1f10f1513164c70540e1888d3bd2bbf1047 100644 (file)
@@ -131,6 +131,7 @@ int _PyCompile_PushFBlock(struct _PyCompiler *c, _Py_SourceLocation loc,
 void _PyCompile_PopFBlock(struct _PyCompiler *c, enum _PyCompile_FBlockType t,
                           _PyJumpTargetLabel block_label);
 _PyCompile_FBlockInfo *_PyCompile_TopFBlock(struct _PyCompiler *c);
+bool _PyCompile_InExceptionHandler(struct _PyCompiler *c);
 
 int _PyCompile_EnterScope(struct _PyCompiler *c, identifier name, int scope_type,
                           void *key, int lineno, PyObject *private,
index 950547cb002f4c346a9e96d4fb34cf790ff0266f..379bf6a81784b0801dd43e4d1d7feff63f79d376 100644 (file)
@@ -36,6 +36,8 @@ extern int _PyDict_DelItem_KnownHash_LockHeld(PyObject *mp, PyObject *key,
 
 extern int _PyDict_Contains_KnownHash(PyObject *, PyObject *, Py_hash_t);
 
+extern void _PyDict_ClearKeysVersionLockHeld(PyObject *mp);
+
 extern int _PyDict_Next(
     PyObject *mp, Py_ssize_t *pos, PyObject **key, PyObject **value, Py_hash_t *hash);
 
@@ -264,6 +266,13 @@ static inline PyDictUnicodeEntry* DK_UNICODE_ENTRIES(PyDictKeysObject *dk) {
 #define DICT_UNIQUE_ID_SHIFT (32)
 #define DICT_UNIQUE_ID_MAX ((UINT64_C(1) << (64 - DICT_UNIQUE_ID_SHIFT)) - 1)
 
+/* The first three dict watcher IDs are reserved for CPython,
+ * so we don't need to check that they haven't been used */
+#define BUILTINS_WATCHER_ID     0
+#define GLOBALS_WATCHER_ID      1
+#define MODULE_WATCHER_ID       2
+#define FIRST_AVAILABLE_WATCHER 3
+
 
 PyAPI_FUNC(void)
 _PyDict_SendEvent(int watcher_bits,
index 09b8762ee2de35b4bf116b31c4976ab045deddfa..64e3438f9157fe698d50ea477702f158dee1114d 100644 (file)
@@ -1446,6 +1446,8 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__iter__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__itruediv__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__ixor__));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__lazy_import__));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__lazy_modules__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__le__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__len__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__length_hint__));
index 040f79afeebb1172bb65c4ba6098d13a3117adda..78ed30dd7f62a214895fcfeaca4805fc5732a607 100644 (file)
@@ -169,6 +169,8 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(__iter__)
         STRUCT_FOR_ID(__itruediv__)
         STRUCT_FOR_ID(__ixor__)
+        STRUCT_FOR_ID(__lazy_import__)
+        STRUCT_FOR_ID(__lazy_modules__)
         STRUCT_FOR_ID(__le__)
         STRUCT_FOR_ID(__len__)
         STRUCT_FOR_ID(__length_hint__)
index 4c8b8c0ed868d6c1958db5b483fc804b2082d555..32ed3a62b2b4a7ce3bd097699e874a7c3d82fba5 100644 (file)
@@ -32,6 +32,18 @@ extern int _PyImport_FixupBuiltin(
     PyObject *modules
     );
 
+extern PyObject * _PyImport_ResolveName(
+    PyThreadState *tstate, PyObject *name, PyObject *globals, int level);
+extern PyObject * _PyImport_GetAbsName(
+    PyThreadState *tstate, PyObject *name, PyObject *globals, int level);
+// Symbol is exported for the JIT on Windows builds.
+PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
+    PyThreadState *tstate, PyObject *lazy_import);
+extern PyObject * _PyImport_LazyImportModuleLevelObject(
+    PyThreadState *tstate, PyObject *name, PyObject *builtins,
+    PyObject *globals, PyObject *locals, PyObject *fromlist, int level);
+
+
 #ifdef HAVE_DLOPEN
 #  include <dlfcn.h>              // RTLD_NOW, RTLD_LAZY
 #  if HAVE_DECL_RTLD_NOW
@@ -69,11 +81,19 @@ extern void _PyImport_ClearModules(PyInterpreterState *interp);
 
 extern void _PyImport_ClearModulesByIndex(PyInterpreterState *interp);
 
+extern PyObject * _PyImport_InitLazyModules(
+    PyInterpreterState *interp);
+extern void _PyImport_ClearLazyModules(PyInterpreterState *interp);
+
 extern int _PyImport_InitDefaultImportFunc(PyInterpreterState *interp);
 extern int _PyImport_IsDefaultImportFunc(
         PyInterpreterState *interp,
         PyObject *func);
 
+extern int _PyImport_IsDefaultLazyImportFunc(
+        PyInterpreterState *interp,
+        PyObject *func);
+
 extern PyObject * _PyImport_GetImportlibLoader(
         PyInterpreterState *interp,
         const char *loader_name);
index 723657e4cef10db99d0e088793550a10c4377ebb..3ebc8967c3dc7febb3bf323fea7b84d0d5975790 100644 (file)
@@ -324,6 +324,14 @@ struct _import_state {
     int dlopenflags;
 #endif
     PyObject *import_func;
+    PyObject *lazy_import_func;
+    int lazy_imports_mode;
+    PyObject *lazy_imports_filter;
+    PyObject *lazy_importing_modules;
+    PyObject *lazy_modules;
+#ifdef Py_GIL_DISABLED
+    PyMutex lazy_mutex;
+#endif
     /* The global import lock. */
     _PyRecursiveMutex lock;
     /* diagnostic info in PyImport_ImportModuleLevelObject() */
diff --git a/Include/internal/pycore_lazyimportobject.h b/Include/internal/pycore_lazyimportobject.h
new file mode 100644 (file)
index 0000000..b81e421
--- /dev/null
@@ -0,0 +1,35 @@
+// Lazy object interface.
+
+#ifndef Py_INTERNAL_LAZYIMPORTOBJECT_H
+#define Py_INTERNAL_LAZYIMPORTOBJECT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef Py_BUILD_CORE
+#  error "this header requires Py_BUILD_CORE define"
+#endif
+
+PyAPI_DATA(PyTypeObject) PyLazyImport_Type;
+#define PyLazyImport_CheckExact(op) Py_IS_TYPE((op), &PyLazyImport_Type)
+
+typedef struct {
+    PyObject_HEAD
+    PyObject *lz_builtins;
+    PyObject *lz_from;
+    PyObject *lz_attr;
+    // Frame information for the original import location.
+    PyCodeObject *lz_code;     // Code object where the lazy import was created.
+    int lz_instr_offset;       // Instruction offset where the lazy import was created.
+} PyLazyImportObject;
+
+
+PyAPI_FUNC(PyObject *) _PyLazyImport_GetName(PyObject *lazy_import);
+PyAPI_FUNC(PyObject *) _PyLazyImport_New(
+    struct _PyInterpreterFrame *frame, PyObject *import_func, PyObject *from, PyObject *attr);
+
+#ifdef __cplusplus
+}
+#endif
+#endif // !Py_INTERNAL_LAZYIMPORTOBJECT_H
index 919a2ec0d1aa9e7ba2f48594ae35f2e7bd1ec043..3fcf650426d36d0dbe76d27214d74c44c4e092d3 100644 (file)
@@ -286,11 +286,12 @@ Known values:
     Python 3.15a1 3653 (Fix handling of opcodes that may leave operands on the stack when optimizing LOAD_FAST)
     Python 3.15a1 3654 (Fix missing exception handlers in logical expression)
     Python 3.15a1 3655 (Fix miscompilation of some module-level annotations)
-    Python 3.15a2 3656 (Add TRACE_RECORD instruction, for platforms with switch based interpreter)
+    Python 3.15a1 3656 (Add TRACE_RECORD instruction, for platforms with switch based interpreter)
     Python 3.15a4 3657 (Add BINARY_OP_SUBSCR_USTR_INT)
     Python 3.15a4 3658 (Optimize bytecode for list/set called on genexp)
     Python 3.15a4 3659 (Add CALL_FUNCTION_EX specialization)
     Python 3.15a4 3660 (Change generator preamble code)
+    Python 3.15a4 3661 (Lazy imports IMPORT_NAME opcode changes)
 
 
     Python 3.16 will start with 3700
@@ -304,7 +305,7 @@ PC/launcher.c must also be updated.
 
 */
 
-#define PYC_MAGIC_NUMBER 3660
+#define PYC_MAGIC_NUMBER 3661
 /* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
    (little-endian) and then appending b'\r\n'. */
 #define PYC_MAGIC_NUMBER_TOKEN \
index 9a62daf6621ca2955bb3a89de70e0e844afb824c..7882ce0332356115e958108401b0df730246bf08 100644 (file)
@@ -19,6 +19,8 @@ extern int _PyModule_IsPossiblyShadowing(PyObject *);
 
 extern int _PyModule_IsExtension(PyObject *obj);
 
+extern int _PyModule_InitModuleDictWatcher(PyInterpreterState *interp);
+
 typedef int (*_Py_modexecfunc)(PyObject *);
 
 typedef struct {
index f4ea7b7954bb84a684444c44263c75243bb60d5d..d4b7b090f93f317cc3427e7e8337cc3599160d8a 100644 (file)
@@ -1444,6 +1444,8 @@ extern "C" {
     INIT_ID(__iter__), \
     INIT_ID(__itruediv__), \
     INIT_ID(__ixor__), \
+    INIT_ID(__lazy_import__), \
+    INIT_ID(__lazy_modules__), \
     INIT_ID(__le__), \
     INIT_ID(__len__), \
     INIT_ID(__length_hint__), \
index c0164507ea033e800a69af4e05442468b8565467..e1176c65c5ca9e619bb1ad1d89b10653c7eb1c1f 100644 (file)
@@ -126,6 +126,7 @@ typedef struct _symtable_entry {
     unsigned ste_method : 1; /* true if block is a function block defined in class scope */
     unsigned ste_has_conditional_annotations : 1; /* true if block has conditionally executed annotations */
     unsigned ste_in_conditional_block : 1; /* set while we are inside a conditionally executed block */
+    unsigned ste_in_try_block : 1; /* set while we are inside a try/except block */
     unsigned ste_in_unevaluated_annotation : 1; /* set while we are processing an annotation that will not be evaluated */
     int ste_comp_iter_expr; /* non-zero if visiting a comprehension range expression */
     _Py_SourceLocation ste_loc; /* source location of block */
index f9677d01228dce988bdffdbeb5fc0cbbed4fc501..d843674f1809023b5649365ea7a0977f43be9f62 100644 (file)
@@ -456,6 +456,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(__lazy_import__);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(__lazy_modules__);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(__le__);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
index 5d0028c116e2d862948042746734e252e4aed0f6..cfabbc5fe8d5da0632b62a53d5ae252416a05ea2 100644 (file)
@@ -91,6 +91,9 @@ PyAPI_DATA(PyObject *) PyExc_EOFError;
 PyAPI_DATA(PyObject *) PyExc_FloatingPointError;
 PyAPI_DATA(PyObject *) PyExc_OSError;
 PyAPI_DATA(PyObject *) PyExc_ImportError;
+#if !defined(Py_LIMITED_API)
+PyAPI_DATA(PyObject *) PyExc_ImportCycleError;
+#endif
 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03060000
 PyAPI_DATA(PyObject *) PyExc_ModuleNotFoundError;
 #endif
index a981326432429b315b602d66491d7824ac07fc6c..928db663b446ba22a168076b5d28572d9eb651b9 100644 (file)
@@ -240,6 +240,7 @@ for excname in PYTHON3_OSERROR_EXCEPTIONS:
     REVERSE_NAME_MAPPING[('builtins', excname)] = ('exceptions', 'OSError')
 
 PYTHON3_IMPORTERROR_EXCEPTIONS = (
+    'ImportCycleError',
     'ModuleNotFoundError',
 )
 
index 06cddef851bb4042549f3d26a3d6fe50e70db77c..25d7ac1bd0b14e5c0954b54cb7764e8c0feb0566 100644 (file)
@@ -276,6 +276,12 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool:
             TI(T.NAME, string=s)
         ):
             return not keyword.iskeyword(s)
+        case (
+            None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":" | ";"),
+            TI(string="lazy"),
+            TI(string="import") | TI(string="from")
+        ):
+            return True
         case _:
             return False
 
index 8c257d118fb23be9db22c52e8806a63c1e632661..58c7f6419032c6c697b52474cf6bdb30fa195a01 100644 (file)
@@ -35,6 +35,7 @@ SET_FUNCTION_ATTRIBUTE = opmap['SET_FUNCTION_ATTRIBUTE']
 FUNCTION_ATTR_FLAGS = ('defaults', 'kwdefaults', 'annotations', 'closure', 'annotate')
 
 ENTER_EXECUTOR = opmap['ENTER_EXECUTOR']
+IMPORT_NAME = opmap['IMPORT_NAME']
 LOAD_GLOBAL = opmap['LOAD_GLOBAL']
 LOAD_SMALL_INT = opmap['LOAD_SMALL_INT']
 BINARY_OP = opmap['BINARY_OP']
@@ -600,6 +601,12 @@ class ArgResolver:
                     argval, argrepr = _get_name_info(arg//4, get_name)
                     if (arg & 1) and argrepr:
                         argrepr = f"{argrepr} + NULL|self"
+                elif deop == IMPORT_NAME:
+                    argval, argrepr = _get_name_info(arg//4, get_name)
+                    if (arg & 1) and argrepr:
+                        argrepr = f"{argrepr} + lazy"
+                    elif (arg & 2) and argrepr:
+                        argrepr = f"{argrepr} + eager"
                 else:
                     argval, argrepr = _get_name_info(arg, get_name)
             elif deop in hasjump or deop in hasexc:
@@ -1012,7 +1019,9 @@ def _find_imports(co):
                 (level_op[0] in hasconst or level_op[0] == LOAD_SMALL_INT)):
                 level = _get_const_value(level_op[0], level_op[1], consts)
                 fromlist = _get_const_value(from_op[0], from_op[1], consts)
-                yield (names[oparg], level, fromlist)
+                # IMPORT_NAME encodes lazy/eager flags in bits 0-1,
+                # name index in bits 2+.
+                yield (names[oparg >> 2], level, fromlist)
 
 def _find_store_names(co):
     """Find names of variables which are written in the code
index bffa2ddd3cd9cd2cb6c33143cc9bf0f0e137c704..6db38de3aa6cb9cd922f24af731f43886648d53c 100644 (file)
@@ -42,6 +42,11 @@ def make_pat():
         ]) +
         r"))"
     )
+    lazy_softkw = (
+        r"^[ \t]*" +  # at beginning of line + possible indentation
+        r"(?P<LAZY_SOFTKW>lazy)" +
+        r"(?=[ \t]+(?:import|from)\b)"  # followed by 'import' or 'from'
+    )
     builtinlist = [str(name) for name in dir(builtins)
                    if not name.startswith('_') and
                    name not in keyword.kwlist]
@@ -56,7 +61,7 @@ def make_pat():
     prog = re.compile("|".join([
                                 builtin, comment, string, kw,
                                 match_softkw, case_default,
-                                case_softkw_and_pattern,
+                                case_softkw_and_pattern, lazy_softkw,
                                 any("SYNC", [r"\n"]),
                                ]),
                       re.DOTALL | re.MULTILINE)
@@ -70,6 +75,7 @@ prog_group_name_to_tag = {
     "CASE_SOFTKW": "KEYWORD",
     "CASE_DEFAULT_UNDERSCORE": "KEYWORD",
     "CASE_SOFTKW2": "KEYWORD",
+    "LAZY_SOFTKW": "KEYWORD",
 }
 
 
index 40800df97b0bd38e6dfcb1b06d80cacb3ff73916..fb6ee8250867502dbf97da6b97d14e0756bda4fa 100644 (file)
@@ -542,6 +542,24 @@ class ColorDelegatorTest(unittest.TestCase):
         self._assert_highlighting('case _:', {'KEYWORD': [('1.0', '1.4'),
                                                           ('1.5', '1.6')]})
 
+    def test_lazy_soft_keyword(self):
+        # lazy followed by import
+        self._assert_highlighting('lazy import foo',
+                                  {'KEYWORD': [('1.0', '1.4'),
+                                               ('1.5', '1.11')]})
+        self._assert_highlighting('    lazy import foo',
+                                  {'KEYWORD': [('1.4', '1.8'),
+                                               ('1.9', '1.15')]})
+
+        # lazy followed by from
+        self._assert_highlighting('lazy from foo import bar',
+                                  {'KEYWORD': [('1.0', '1.4'), ('1.5', '1.9'),
+                                               ('1.14', '1.20')]})
+
+        # lazy not followed by import/from (not highlighted)
+        self._assert_highlighting('lazy = 1', {})
+        self._assert_highlighting('lazy foo', {})
+
     def test_long_multiline_string(self):
         source = textwrap.dedent('''\
             """a
index cfcebb7309803c7158d700f80b64d5c02a8b226e..45beb51659f5b70f6d6de7f1afd82af4dbfd5c7b 100644 (file)
@@ -1256,6 +1256,12 @@ def _find_and_load_unlocked(name, import_):
         except AttributeError:
             msg = f"Cannot set an attribute on {parent!r} for child module {child!r}"
             _warnings.warn(msg, ImportWarning)
+    # Set attributes to lazy submodules on the module.
+    try:
+        _imp._set_lazy_attributes(module, name)
+    except Exception as e:
+        msg = f"Cannot set lazy attributes on {name!r}: {e!r}"
+        _warnings.warn(msg, ImportWarning)
     return module
 
 
index e22c837835e740aa9be9ffed805757836a06b0ea..98ffe2de28b1a1b9c04f298e4b6d833520e0ac77 100644 (file)
@@ -56,6 +56,7 @@ kwlist = [
 softkwlist = [
     '_',
     'case',
+    'lazy',
     'match',
     'type'
 ]
index 23eb0020f42e8a01486d03cd980f490e15ef3a52..e8cef29d00467f7010b4677798b498c614e560e5 100644 (file)
@@ -36,6 +36,7 @@ import keyword
 import re
 import __main__
 import warnings
+import types
 
 __all__ = ["Completer"]
 
@@ -188,7 +189,17 @@ class Completer:
                         # property method, which is not desirable.
                         matches.append(match)
                         continue
-                    if (value := getattr(thisobject, word, None)) is not None:
+
+                    if (isinstance(thisobject, types.ModuleType)
+                        and
+                        isinstance(thisobject.__dict__.get(word),
+                                   types.LazyImportType)
+                    ):
+                        value = thisobject.__dict__.get(word)
+                    else:
+                        value = getattr(thisobject, word, None)
+
+                    if value is not None:
                         matches.append(self._callable_postfix(value, match))
                     else:
                         matches.append(match)
index 7b46139f786cf77733d217d2da9b45792a634aa6..b5be4c3afaf9580e7bd34936a1b9a80fd6970a85 100644 (file)
@@ -14,6 +14,9 @@ extend-exclude = [
     # New grammar constructions may not yet be recognized by Ruff,
     # and tests re-use the same names as only the grammar is being checked.
     "test_grammar.py",
+    # Lazy import syntax (PEP 810) is not yet supported by Ruff
+    "test_import/data/lazy_imports/*.py",
+    "test_import/data/lazy_imports/**/*.py",
 ]
 
 [lint]
index f2649aa2d41fefd53dcba4bbe9361a8ab8f3b6b1..98a5e950602eafcdb76e20233041c1d4ed96d1d3 100644 (file)
@@ -14,6 +14,7 @@ BaseException
       â”œâ”€â”€ EOFError
       â”œâ”€â”€ ExceptionGroup [BaseExceptionGroup]
       â”œâ”€â”€ ImportError
+      â”‚    â””── ImportCycleError
       â”‚    â””── ModuleNotFoundError
       â”œâ”€â”€ LookupError
       â”‚    â”œâ”€â”€ IndexError
index 1c1985519cd8b452628f698d39ff6e32fb6ff8e1..cc6accd766b78add1a1382b3cc8ce674ac7a3853 100644 (file)
@@ -69,10 +69,14 @@ Module(body=[Try(body=[Pass()], handlers=[ExceptHandler(type=Name(...), name='ex
 Module(body=[TryStar(body=[Pass()], handlers=[ExceptHandler(type=Name(...), name='exc', body=[Pass(...)])], orelse=[Pass()], finalbody=[Pass()])], type_ignores=[])
 Module(body=[Assert(test=Name(id='v', ctx=Load(...)), msg=None)], type_ignores=[])
 Module(body=[Assert(test=Name(id='v', ctx=Load(...)), msg=Constant(value='message', kind=None))], type_ignores=[])
-Module(body=[Import(names=[alias(name='sys', asname=None)])], type_ignores=[])
-Module(body=[Import(names=[alias(name='foo', asname='bar')])], type_ignores=[])
-Module(body=[ImportFrom(module='sys', names=[alias(name='x', asname='y')], level=0)], type_ignores=[])
-Module(body=[ImportFrom(module='sys', names=[alias(name='v', asname=None)], level=0)], type_ignores=[])
+Module(body=[Import(names=[alias(name='sys', asname=None)], is_lazy=0)], type_ignores=[])
+Module(body=[Import(names=[alias(name='foo', asname='bar')], is_lazy=0)], type_ignores=[])
+Module(body=[ImportFrom(module='sys', names=[alias(name='x', asname='y')], level=0, is_lazy=0)], type_ignores=[])
+Module(body=[ImportFrom(module='sys', names=[alias(name='v', asname=None)], level=0, is_lazy=0)], type_ignores=[])
+Module(body=[Import(names=[alias(name='sys', asname=None)], is_lazy=1)], type_ignores=[])
+Module(body=[Import(names=[alias(name='foo', asname='bar')], is_lazy=1)], type_ignores=[])
+Module(body=[ImportFrom(module='sys', names=[alias(name='x', asname='y')], level=0, is_lazy=1)], type_ignores=[])
+Module(body=[ImportFrom(module='sys', names=[alias(name='v', asname=None)], level=0, is_lazy=1)], type_ignores=[])
 Module(body=[Global(names=['v'])], type_ignores=[])
 Module(body=[Expr(value=Constant(value=1, kind=None))], type_ignores=[])
 Module(body=[Pass()], type_ignores=[])
index b76f98901d2ad8b064d9499ed4098ea72c8fe224..a565ed10f8b434b81fb12eaeab62bc1225eea13f 100644 (file)
@@ -118,6 +118,12 @@ exec_tests = [
     # ImportFrom
     "from sys import x as y",
     "from sys import v",
+    # Lazy Import
+    "lazy import sys",
+    "lazy import foo as bar",
+    # Lazy ImportFrom
+    "lazy from sys import x as y",
+    "lazy from sys import v",
     # Global
     "global v",
     # Expr
@@ -460,10 +466,14 @@ exec_results = [
 ('Module', [('TryStar', (1, 0, 7, 6), [('Pass', (2, 2, 2, 6))], [('ExceptHandler', (3, 0, 4, 6), ('Name', (3, 8, 3, 17), 'Exception', ('Load',)), 'exc', [('Pass', (4, 2, 4, 6))])], [('Pass', (5, 7, 5, 11))], [('Pass', (7, 2, 7, 6))])], []),
 ('Module', [('Assert', (1, 0, 1, 8), ('Name', (1, 7, 1, 8), 'v', ('Load',)), None)], []),
 ('Module', [('Assert', (1, 0, 1, 19), ('Name', (1, 7, 1, 8), 'v', ('Load',)), ('Constant', (1, 10, 1, 19), 'message', None))], []),
-('Module', [('Import', (1, 0, 1, 10), [('alias', (1, 7, 1, 10), 'sys', None)])], []),
-('Module', [('Import', (1, 0, 1, 17), [('alias', (1, 7, 1, 17), 'foo', 'bar')])], []),
-('Module', [('ImportFrom', (1, 0, 1, 22), 'sys', [('alias', (1, 16, 1, 22), 'x', 'y')], 0)], []),
-('Module', [('ImportFrom', (1, 0, 1, 17), 'sys', [('alias', (1, 16, 1, 17), 'v', None)], 0)], []),
+('Module', [('Import', (1, 0, 1, 10), [('alias', (1, 7, 1, 10), 'sys', None)], 0)], []),
+('Module', [('Import', (1, 0, 1, 17), [('alias', (1, 7, 1, 17), 'foo', 'bar')], 0)], []),
+('Module', [('ImportFrom', (1, 0, 1, 22), 'sys', [('alias', (1, 16, 1, 22), 'x', 'y')], 0, 0)], []),
+('Module', [('ImportFrom', (1, 0, 1, 17), 'sys', [('alias', (1, 16, 1, 17), 'v', None)], 0, 0)], []),
+('Module', [('Import', (1, 0, 1, 15), [('alias', (1, 12, 1, 15), 'sys', None)], 1)], []),
+('Module', [('Import', (1, 0, 1, 22), [('alias', (1, 12, 1, 22), 'foo', 'bar')], 1)], []),
+('Module', [('ImportFrom', (1, 0, 1, 27), 'sys', [('alias', (1, 21, 1, 27), 'x', 'y')], 0, 1)], []),
+('Module', [('ImportFrom', (1, 0, 1, 22), 'sys', [('alias', (1, 21, 1, 22), 'v', None)], 0, 1)], []),
 ('Module', [('Global', (1, 0, 1, 8), ['v'])], []),
 ('Module', [('Expr', (1, 0, 1, 1), ('Constant', (1, 0, 1, 1), 1, None))], []),
 ('Module', [('Pass', (1, 0, 1, 4))], []),
index c5f42cb7888c085685f76768e9e50ba15a407913..f29f98beb2d04849632f9938cd2477d996d023d6 100644 (file)
@@ -1701,8 +1701,8 @@ Module(
 
         check_text(
             "import _ast as ast; from module import sub",
-            empty="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)])",
-            full="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)], type_ignores=[])",
+            empty="Module(body=[Import(names=[alias(name='_ast', asname='ast')], is_lazy=0), ImportFrom(module='module', names=[alias(name='sub')], level=0, is_lazy=0)])",
+            full="Module(body=[Import(names=[alias(name='_ast', asname='ast')], is_lazy=0), ImportFrom(module='module', names=[alias(name='sub')], level=0, is_lazy=0)], type_ignores=[])",
         )
 
     def test_copy_location(self):
index b04d0923926ded4ef7bf47054c19909203267fd7..f10ad50d3bea7e98eba254a562fb94ca33366ba5 100644 (file)
@@ -62,6 +62,7 @@ class CAPITests(unittest.TestCase):
             ("int_max_str_digits", int, None),
             ("interactive", bool, None),
             ("isolated", bool, None),
+            ("lazy_imports", int, None),
             ("malloc_stats", bool, None),
             ("pymalloc_hugepages", bool, None),
             ("module_search_paths", list[str], "path"),
index 8529afaa3f537064b1c95ef181e7c560f89cbf75..cefd64ddfe84173e1f95e6b1606b69f805e949ca 100644 (file)
@@ -292,7 +292,7 @@ dis_intrinsic_1_2 = """\
 
   1           LOAD_SMALL_INT           0
               LOAD_CONST               1 (('*',))
-              IMPORT_NAME              0 (math)
+              IMPORT_NAME              2 (math + eager)
               CALL_INTRINSIC_1         2 (INTRINSIC_IMPORT_STAR)
               POP_TOP
               LOAD_CONST               2 (None)
index 29b1249b10dfc86b981cc9f78aa22a7e7138cf9a..35246d7c4844396d8360cffaabb07375634a71a4 100644 (file)
@@ -635,6 +635,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         'tracemalloc': 0,
         'perf_profiling': 0,
         'import_time': 0,
+        'lazy_imports': -1,
         'thread_inherit_context': DEFAULT_THREAD_INHERIT_CONTEXT,
         'context_aware_warnings': DEFAULT_CONTEXT_AWARE_WARNINGS,
         'code_debug_ranges': True,
diff --git a/Lib/test/test_import/data/lazy_imports/basic2.py b/Lib/test/test_import/data/lazy_imports/basic2.py
new file mode 100644 (file)
index 0000000..f93ec89
--- /dev/null
@@ -0,0 +1,4 @@
+def f():
+   pass
+
+x = 42
diff --git a/Lib/test/test_import/data/lazy_imports/basic_compatibility_mode.py b/Lib/test/test_import/data/lazy_imports/basic_compatibility_mode.py
new file mode 100644 (file)
index 0000000..5076fa4
--- /dev/null
@@ -0,0 +1,2 @@
+__lazy_modules__ = ['test.test_import.data.lazy_imports.basic2']
+import test.test_import.data.lazy_imports.basic2
diff --git a/Lib/test/test_import/data/lazy_imports/basic_compatibility_mode_relative.py b/Lib/test/test_import/data/lazy_imports/basic_compatibility_mode_relative.py
new file mode 100644 (file)
index 0000000..e377593
--- /dev/null
@@ -0,0 +1,2 @@
+__lazy_modules__ = ['test.test_import.data.lazy_imports.basic2']
+lazy from .basic2 import f
diff --git a/Lib/test/test_import/data/lazy_imports/basic_compatibility_mode_used.py b/Lib/test/test_import/data/lazy_imports/basic_compatibility_mode_used.py
new file mode 100644 (file)
index 0000000..64f3664
--- /dev/null
@@ -0,0 +1,3 @@
+__lazy_modules__ = ['test.test_import.data.lazy_imports.basic2']
+import test.test_import.data.lazy_imports.basic2
+test.test_import.data.lazy_imports.basic2.f()
diff --git a/Lib/test/test_import/data/lazy_imports/basic_dir.py b/Lib/test/test_import/data/lazy_imports/basic_dir.py
new file mode 100644 (file)
index 0000000..ca9e29d
--- /dev/null
@@ -0,0 +1,2 @@
+lazy import test.test_import.data.lazy_imports.basic2
+x = dir()
diff --git a/Lib/test/test_import/data/lazy_imports/basic_from_unused.py b/Lib/test/test_import/data/lazy_imports/basic_from_unused.py
new file mode 100644 (file)
index 0000000..686caa8
--- /dev/null
@@ -0,0 +1 @@
+lazy from test.test_import.data.lazy_imports import basic2
diff --git a/Lib/test/test_import/data/lazy_imports/basic_unused.py b/Lib/test/test_import/data/lazy_imports/basic_unused.py
new file mode 100644 (file)
index 0000000..bf8ae46
--- /dev/null
@@ -0,0 +1 @@
+lazy import test.test_import.data.lazy_imports.basic2
diff --git a/Lib/test/test_import/data/lazy_imports/basic_used.py b/Lib/test/test_import/data/lazy_imports/basic_used.py
new file mode 100644 (file)
index 0000000..84e3547
--- /dev/null
@@ -0,0 +1,3 @@
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
+
+basic2.f()
diff --git a/Lib/test/test_import/data/lazy_imports/broken_attr_module.py b/Lib/test/test_import/data/lazy_imports/broken_attr_module.py
new file mode 100644 (file)
index 0000000..a60bca2
--- /dev/null
@@ -0,0 +1,3 @@
+# Module that exists but doesn't have expected attributes
+x = 42
+# No 'nonexistent_attr' here
diff --git a/Lib/test/test_import/data/lazy_imports/broken_module.py b/Lib/test/test_import/data/lazy_imports/broken_module.py
new file mode 100644 (file)
index 0000000..b49d4a4
--- /dev/null
@@ -0,0 +1,2 @@
+# Module that raises an error during import
+raise ValueError("This module always fails to import")
diff --git a/Lib/test/test_import/data/lazy_imports/compatibility_mode_func.py b/Lib/test/test_import/data/lazy_imports/compatibility_mode_func.py
new file mode 100644 (file)
index 0000000..307338a
--- /dev/null
@@ -0,0 +1,5 @@
+__lazy_modules__ = ['test.test_import.data.lazy_imports.basic2']
+def f():
+    import test.test_import.data.lazy_imports.basic2
+
+f()
diff --git a/Lib/test/test_import/data/lazy_imports/compatibility_mode_try_except.py b/Lib/test/test_import/data/lazy_imports/compatibility_mode_try_except.py
new file mode 100644 (file)
index 0000000..6d54e69
--- /dev/null
@@ -0,0 +1,5 @@
+__lazy_modules__ = ['test.test_import.data.lazy_imports.basic2']
+try:
+    import test.test_import.data.lazy_imports.basic2
+except:
+    pass
diff --git a/Lib/test/test_import/data/lazy_imports/dunder_lazy_import.py b/Lib/test/test_import/data/lazy_imports/dunder_lazy_import.py
new file mode 100644 (file)
index 0000000..1a8a19c
--- /dev/null
@@ -0,0 +1 @@
+basic = __lazy_import__('test.test_import.data.lazy_imports.basic2')
diff --git a/Lib/test/test_import/data/lazy_imports/dunder_lazy_import_builtins.py b/Lib/test/test_import/data/lazy_imports/dunder_lazy_import_builtins.py
new file mode 100644 (file)
index 0000000..f1fed0f
--- /dev/null
@@ -0,0 +1,14 @@
+import sys
+
+def myimport(*args):
+    return sys.modules[__name__]
+
+
+new_globals = dict(globals())
+new_globals["__builtins__"] = {
+    "__import__": myimport,
+}
+basic2 = 42
+basic = __lazy_import__("test.test_import.data.lazy_imports.basic2",
+                        globals=new_globals)
+basic
diff --git a/Lib/test/test_import/data/lazy_imports/dunder_lazy_import_used.py b/Lib/test/test_import/data/lazy_imports/dunder_lazy_import_used.py
new file mode 100644 (file)
index 0000000..2432ca1
--- /dev/null
@@ -0,0 +1,3 @@
+basic = __lazy_import__('test.test_import.data.lazy_imports',
+                        fromlist=("basic2", ))
+basic
diff --git a/Lib/test/test_import/data/lazy_imports/eager_import_func.py b/Lib/test/test_import/data/lazy_imports/eager_import_func.py
new file mode 100644 (file)
index 0000000..89e643a
--- /dev/null
@@ -0,0 +1,3 @@
+def f():
+    import test.test_import.data.lazy_imports.basic2 as basic2
+    return basic2
diff --git a/Lib/test/test_import/data/lazy_imports/global_filter.py b/Lib/test/test_import/data/lazy_imports/global_filter.py
new file mode 100644 (file)
index 0000000..72cb5f2
--- /dev/null
@@ -0,0 +1,10 @@
+import sys
+
+def filter(module_name, imported_name, from_list):
+    assert module_name == __name__
+    assert imported_name == "test.test_import.data.lazy_imports.basic2"
+    return False
+
+sys.set_lazy_imports_filter(filter)
+
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
diff --git a/Lib/test/test_import/data/lazy_imports/global_filter_from.py b/Lib/test/test_import/data/lazy_imports/global_filter_from.py
new file mode 100644 (file)
index 0000000..733839d
--- /dev/null
@@ -0,0 +1,11 @@
+import importlib
+
+def filter(module_name, imported_name, from_list):
+    assert module_name == __name__
+    assert imported_name == "test.test_import.data.lazy_imports.basic2"
+    assert from_list == ['f']
+    return False
+
+importlib.set_lazy_imports(None, filter)
+
+lazy from import test.test_import.data.lazy_imports.basic2 import f
diff --git a/Lib/test/test_import/data/lazy_imports/global_filter_from_true.py b/Lib/test/test_import/data/lazy_imports/global_filter_from_true.py
new file mode 100644 (file)
index 0000000..c019f1a
--- /dev/null
@@ -0,0 +1,11 @@
+import importlib
+
+def filter(module_name, imported_name, from_list):
+    assert module_name == __name__
+    assert imported_name == "test.test_import.data.lazy_imports.basic2"
+    assert from_list == ['f']
+    return True
+
+importlib.set_lazy_imports(None, filter)
+
+lazy from import test.test_import.data.lazy_imports.basic2 import f
diff --git a/Lib/test/test_import/data/lazy_imports/global_filter_true.py b/Lib/test/test_import/data/lazy_imports/global_filter_true.py
new file mode 100644 (file)
index 0000000..4881b30
--- /dev/null
@@ -0,0 +1,11 @@
+import sys
+
+def filter(module_name, imported_name, from_list):
+    assert module_name == __name__
+    assert imported_name == "test.test_import.data.lazy_imports.basic2"
+    return True
+
+sys.set_lazy_imports("normal")
+sys.set_lazy_imports_filter(filter)
+
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
diff --git a/Lib/test/test_import/data/lazy_imports/global_off.py b/Lib/test/test_import/data/lazy_imports/global_off.py
new file mode 100644 (file)
index 0000000..4f20274
--- /dev/null
@@ -0,0 +1,5 @@
+import sys
+
+sys.set_lazy_imports("none")
+
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
diff --git a/Lib/test/test_import/data/lazy_imports/global_on.py b/Lib/test/test_import/data/lazy_imports/global_on.py
new file mode 100644 (file)
index 0000000..3f8e1d2
--- /dev/null
@@ -0,0 +1,5 @@
+import sys
+
+sys.set_lazy_imports("all")
+
+import test.test_import.data.lazy_imports.basic2 as basic2
diff --git a/Lib/test/test_import/data/lazy_imports/globals_access.py b/Lib/test/test_import/data/lazy_imports/globals_access.py
new file mode 100644 (file)
index 0000000..c12c6a0
--- /dev/null
@@ -0,0 +1,9 @@
+# Test that globals() returns lazy proxy objects without reifying
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
+
+def get_from_globals():
+    g = globals()
+    return g['basic2']
+
+def get_direct():
+    return basic2
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_class_body.py b/Lib/test/test_import/data/lazy_imports/lazy_class_body.py
new file mode 100644 (file)
index 0000000..e154b78
--- /dev/null
@@ -0,0 +1,3 @@
+# SyntaxError: lazy import inside class body is not allowed
+class Foo:
+    lazy import json
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_compat_from.py b/Lib/test/test_import/data/lazy_imports/lazy_compat_from.py
new file mode 100644 (file)
index 0000000..f887f47
--- /dev/null
@@ -0,0 +1,6 @@
+# Test __lazy_modules__ with from imports
+__lazy_modules__ = ['test.test_import.data.lazy_imports.basic2']
+from test.test_import.data.lazy_imports.basic2 import x, f
+
+def get_x():
+    return x
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_future_import.py b/Lib/test/test_import/data/lazy_imports/lazy_future_import.py
new file mode 100644 (file)
index 0000000..8bd258b
--- /dev/null
@@ -0,0 +1 @@
+lazy from __future__ import annotations
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_get_value.py b/Lib/test/test_import/data/lazy_imports/lazy_get_value.py
new file mode 100644 (file)
index 0000000..0ff572f
--- /dev/null
@@ -0,0 +1,7 @@
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
+
+def f():
+    x = globals()
+    return x['basic2'].resolve()
+
+f()
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_import_func.py b/Lib/test/test_import/data/lazy_imports/lazy_import_func.py
new file mode 100644 (file)
index 0000000..af758b5
--- /dev/null
@@ -0,0 +1,2 @@
+def f():
+    lazy import foo
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_import_pkg.py b/Lib/test/test_import/data/lazy_imports/lazy_import_pkg.py
new file mode 100644 (file)
index 0000000..79aa9a5
--- /dev/null
@@ -0,0 +1,2 @@
+lazy import test.test_import.data.lazy_imports.pkg.bar
+x = test.test_import.data.lazy_imports.pkg.bar.f
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_try_except.py b/Lib/test/test_import/data/lazy_imports/lazy_try_except.py
new file mode 100644 (file)
index 0000000..e58d1f9
--- /dev/null
@@ -0,0 +1,4 @@
+try:
+    lazy import foo
+except:
+    pass
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_try_except_from.py b/Lib/test/test_import/data/lazy_imports/lazy_try_except_from.py
new file mode 100644 (file)
index 0000000..8c97553
--- /dev/null
@@ -0,0 +1,4 @@
+try:
+    lazy from foo import bar
+except:
+    pass
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_try_except_from_star.py b/Lib/test/test_import/data/lazy_imports/lazy_try_except_from_star.py
new file mode 100644 (file)
index 0000000..b2370ea
--- /dev/null
@@ -0,0 +1 @@
+lazy from foo import *
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_with.py b/Lib/test/test_import/data/lazy_imports/lazy_with.py
new file mode 100644 (file)
index 0000000..b383879
--- /dev/null
@@ -0,0 +1,3 @@
+import contextlib
+with contextlib.nullcontext():
+    lazy import test.test_import.data.lazy_imports.basic2
diff --git a/Lib/test/test_import/data/lazy_imports/lazy_with_from.py b/Lib/test/test_import/data/lazy_imports/lazy_with_from.py
new file mode 100644 (file)
index 0000000..7936326
--- /dev/null
@@ -0,0 +1,3 @@
+import contextlib
+with contextlib.nullcontext():
+    lazy import test.test_import.data.lazy_imports.basic2 as basic2
diff --git a/Lib/test/test_import/data/lazy_imports/modules_dict.py b/Lib/test/test_import/data/lazy_imports/modules_dict.py
new file mode 100644 (file)
index 0000000..327f866
--- /dev/null
@@ -0,0 +1,5 @@
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
+
+import sys
+mod = sys.modules[__name__]
+x = mod.__dict__
diff --git a/Lib/test/test_import/data/lazy_imports/modules_getattr.py b/Lib/test/test_import/data/lazy_imports/modules_getattr.py
new file mode 100644 (file)
index 0000000..ae1d4bb
--- /dev/null
@@ -0,0 +1,5 @@
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
+
+import sys
+mod = sys.modules[__name__]
+x = mod.basic2
diff --git a/Lib/test/test_import/data/lazy_imports/modules_getattr_other.py b/Lib/test/test_import/data/lazy_imports/modules_getattr_other.py
new file mode 100644 (file)
index 0000000..e4d83e6
--- /dev/null
@@ -0,0 +1,5 @@
+lazy import test.test_import.data.lazy_imports.basic2 as basic2
+
+import sys
+mod = sys.modules[__name__]
+x = mod.__name__
diff --git a/Lib/test/test_import/data/lazy_imports/multi_from_import.py b/Lib/test/test_import/data/lazy_imports/multi_from_import.py
new file mode 100644 (file)
index 0000000..96dc975
--- /dev/null
@@ -0,0 +1,5 @@
+# Test that lazy from import with multiple names only reifies accessed names
+lazy from test.test_import.data.lazy_imports.basic2 import f, x
+
+def get_globals():
+    return globals()
diff --git a/Lib/test/test_import/data/lazy_imports/pkg/__init__.py b/Lib/test/test_import/data/lazy_imports/pkg/__init__.py
new file mode 100644 (file)
index 0000000..2d76aba
--- /dev/null
@@ -0,0 +1 @@
+x = 42
diff --git a/Lib/test/test_import/data/lazy_imports/pkg/b.py b/Lib/test/test_import/data/lazy_imports/pkg/b.py
new file mode 100644 (file)
index 0000000..a266b7c
--- /dev/null
@@ -0,0 +1,2 @@
+def foo():
+    return 'foo'
diff --git a/Lib/test/test_import/data/lazy_imports/pkg/bar.py b/Lib/test/test_import/data/lazy_imports/pkg/bar.py
new file mode 100644 (file)
index 0000000..b8d8b60
--- /dev/null
@@ -0,0 +1,2 @@
+print("BAR_MODULE_LOADED")
+def f(): pass
diff --git a/Lib/test/test_import/data/lazy_imports/pkg/c.py b/Lib/test/test_import/data/lazy_imports/pkg/c.py
new file mode 100644 (file)
index 0000000..0bb0311
--- /dev/null
@@ -0,0 +1,4 @@
+lazy from . import b, x
+
+def get_globals():
+    return globals()
diff --git a/Lib/test/test_import/data/lazy_imports/relative_lazy.py b/Lib/test/test_import/data/lazy_imports/relative_lazy.py
new file mode 100644 (file)
index 0000000..6273d38
--- /dev/null
@@ -0,0 +1,5 @@
+# Test relative imports with lazy keyword
+lazy from . import basic2
+
+def get_basic2():
+    return basic2
diff --git a/Lib/test/test_import/data/lazy_imports/relative_lazy_from.py b/Lib/test/test_import/data/lazy_imports/relative_lazy_from.py
new file mode 100644 (file)
index 0000000..1bae2d6
--- /dev/null
@@ -0,0 +1,8 @@
+# Test relative from imports with lazy keyword
+lazy from .basic2 import x, f
+
+def get_x():
+    return x
+
+def get_f():
+    return f
diff --git a/Lib/test/test_import/data/lazy_imports/try_except_eager.py b/Lib/test/test_import/data/lazy_imports/try_except_eager.py
new file mode 100644 (file)
index 0000000..4cdaa9a
--- /dev/null
@@ -0,0 +1,4 @@
+try:
+    import test.test_import.data.lazy_imports.basic2
+except:
+    pass
diff --git a/Lib/test/test_import/data/lazy_imports/try_except_eager_from.py b/Lib/test/test_import/data/lazy_imports/try_except_eager_from.py
new file mode 100644 (file)
index 0000000..6eadaaa
--- /dev/null
@@ -0,0 +1,4 @@
+try:
+    from test.test_import.data.lazy_imports.basic2 import f
+except:
+    pass
diff --git a/Lib/test/test_import/test_lazy_imports.py b/Lib/test/test_import/test_lazy_imports.py
new file mode 100644 (file)
index 0000000..1193af9
--- /dev/null
@@ -0,0 +1,1662 @@
+"""Tests for PEP 810 lazy imports."""
+
+import io
+import dis
+import subprocess
+import sys
+import textwrap
+import threading
+import types
+import unittest
+
+try:
+    import _testcapi
+except ImportError:
+    _testcapi = None
+
+
+class LazyImportTests(unittest.TestCase):
+    """Tests for basic lazy import functionality."""
+
+    def tearDown(self):
+        """Clean up any test modules from sys.modules."""
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+        sys.lazy_modules.clear()
+
+    def test_basic_unused(self):
+        """Lazy imported module should not be loaded if never accessed."""
+        import test.test_import.data.lazy_imports.basic_unused
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+        self.assertIn("test.test_import.data.lazy_imports", sys.lazy_modules)
+        self.assertEqual(sys.lazy_modules["test.test_import.data.lazy_imports"], {"basic2"})
+
+    def test_sys_lazy_modules(self):
+        try:
+            import test.test_import.data.lazy_imports.basic_from_unused
+        except ImportError as e:
+            self.fail('lazy import failed')
+
+        self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules)
+        self.assertIn("test.test_import.data.lazy_imports", sys.lazy_modules)
+        self.assertEqual(sys.lazy_modules["test.test_import.data.lazy_imports"], {"basic2"})
+        test.test_import.data.lazy_imports.basic_from_unused.basic2
+        self.assertNotIn("test.test_import.data", sys.lazy_modules)
+
+    def test_basic_unused_use_externally(self):
+        """Lazy import should load module when accessed from outside."""
+        from test.test_import.data.lazy_imports import basic_unused
+
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+        x = basic_unused.test.test_import.data.lazy_imports.basic2
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_basic_from_unused_use_externally(self):
+        """Lazy 'from' import should load when accessed from outside."""
+        from test.test_import.data.lazy_imports import basic_from_unused
+
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+        x = basic_from_unused.basic2
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_basic_unused_dir(self):
+        """dir() on module should not trigger lazy import reification."""
+        import test.test_import.data.lazy_imports.basic_unused
+
+        x = dir(test.test_import.data.lazy_imports.basic_unused)
+        self.assertIn("test", x)
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_basic_dir(self):
+        """dir() at module scope should not trigger lazy import reification."""
+        from test.test_import.data.lazy_imports import basic_dir
+
+        self.assertIn("test", basic_dir.x)
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_basic_used(self):
+        """Lazy import should load when accessed within the module."""
+        import test.test_import.data.lazy_imports.basic_used
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class GlobalLazyImportModeTests(unittest.TestCase):
+    """Tests for sys.set_lazy_imports() global mode control."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_global_off(self):
+        """Mode 'none' should disable lazy imports entirely."""
+        import test.test_import.data.lazy_imports.global_off
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_global_on(self):
+        """Mode 'all' should make regular imports lazy."""
+        import test.test_import.data.lazy_imports.global_on
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_global_filter(self):
+        """Filter returning False should prevent lazy loading."""
+        import test.test_import.data.lazy_imports.global_filter
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_global_filter_true(self):
+        """Filter returning True should allow lazy loading."""
+        import test.test_import.data.lazy_imports.global_filter_true
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_global_filter_from(self):
+        """Filter should work with 'from' imports."""
+        import test.test_import.data.lazy_imports.global_filter
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_global_filter_from_true(self):
+        """Filter returning True should allow lazy 'from' imports."""
+        import test.test_import.data.lazy_imports.global_filter_true
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class CompatibilityModeTests(unittest.TestCase):
+    """Tests for __lazy_modules__ compatibility mode."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_compatibility_mode(self):
+        """__lazy_modules__ should enable lazy imports for listed modules."""
+        import test.test_import.data.lazy_imports.basic_compatibility_mode
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_compatibility_mode_used(self):
+        """Using a lazy import from __lazy_modules__ should load the module."""
+        import test.test_import.data.lazy_imports.basic_compatibility_mode_used
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_compatibility_mode_func(self):
+        """Imports inside functions should be eager even in compatibility mode."""
+        import test.test_import.data.lazy_imports.compatibility_mode_func
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_compatibility_mode_try_except(self):
+        """Imports in try/except should be eager even in compatibility mode."""
+        import test.test_import.data.lazy_imports.compatibility_mode_try_except
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_compatibility_mode_relative(self):
+        """__lazy_modules__ should work with relative imports."""
+        import test.test_import.data.lazy_imports.basic_compatibility_mode_relative
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class ModuleIntrospectionTests(unittest.TestCase):
+    """Tests for module dict and getattr behavior with lazy imports."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_modules_dict(self):
+        """Accessing module.__dict__ should not trigger reification."""
+        import test.test_import.data.lazy_imports.modules_dict
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_modules_getattr(self):
+        """Module __getattr__ for lazy import name should trigger reification."""
+        import test.test_import.data.lazy_imports.modules_getattr
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_modules_getattr_other(self):
+        """Module __getattr__ for other names should not trigger reification."""
+        import test.test_import.data.lazy_imports.modules_getattr_other
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class LazyImportTypeTests(unittest.TestCase):
+    """Tests for the LazyImportType and its resolve() method."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_lazy_value_resolve(self):
+        """resolve() method should force the lazy import to load."""
+        import test.test_import.data.lazy_imports.lazy_get_value
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_lazy_import_type_exposed(self):
+        """LazyImportType should be exposed in types module."""
+        self.assertHasAttr(types, 'LazyImportType')
+        self.assertEqual(types.LazyImportType.__name__, 'lazy_import')
+
+    def test_lazy_import_type_cant_construct(self):
+        """LazyImportType should not be directly constructible."""
+        self.assertRaises(TypeError, types.LazyImportType, {}, "module")
+
+
+class SyntaxRestrictionTests(unittest.TestCase):
+    """Tests for syntax restrictions on lazy imports."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_lazy_try_except(self):
+        """lazy import inside try/except should raise SyntaxError."""
+        with self.assertRaises(SyntaxError):
+            import test.test_import.data.lazy_imports.lazy_try_except
+
+    def test_lazy_try_except_from(self):
+        """lazy from import inside try/except should raise SyntaxError."""
+        with self.assertRaises(SyntaxError):
+            import test.test_import.data.lazy_imports.lazy_try_except_from
+
+    def test_lazy_try_except_from_star(self):
+        """lazy from import * should raise SyntaxError."""
+        with self.assertRaises(SyntaxError):
+            import test.test_import.data.lazy_imports.lazy_try_except_from_star
+
+    def test_lazy_future_import(self):
+        """lazy from __future__ import should raise SyntaxError."""
+        with self.assertRaises(SyntaxError) as cm:
+            import test.test_import.data.lazy_imports.lazy_future_import
+        # Check we highlight 'lazy' (column offset 0, end offset 4)
+        self.assertEqual(cm.exception.offset, 1)
+        self.assertEqual(cm.exception.end_offset, 5)
+
+    def test_lazy_import_func(self):
+        """lazy import inside function should raise SyntaxError."""
+        with self.assertRaises(SyntaxError):
+            import test.test_import.data.lazy_imports.lazy_import_func
+
+    def test_lazy_import_exec_in_function(self):
+        """lazy import via exec() inside a function should raise SyntaxError."""
+        # exec() inside a function creates a non-module-level context
+        # where lazy imports are not allowed
+        def f():
+            exec("lazy import json")
+
+        with self.assertRaises(SyntaxError) as cm:
+            f()
+        self.assertIn("only allowed at module level", str(cm.exception))
+
+    def test_lazy_import_exec_at_module_level(self):
+        """lazy import via exec() at module level should work."""
+        # exec() at module level (globals == locals) should allow lazy imports
+        code = textwrap.dedent("""
+            import sys
+            exec("lazy import json")
+            # Should be lazy - not loaded yet
+            assert 'json' not in sys.modules
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class EagerImportInLazyModeTests(unittest.TestCase):
+    """Tests for imports that should remain eager even in lazy mode."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_try_except_eager(self):
+        """Imports in try/except should be eager even with mode='all'."""
+        sys.set_lazy_imports("all")
+        import test.test_import.data.lazy_imports.try_except_eager
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_try_except_eager_from(self):
+        """From imports in try/except should be eager even with mode='all'."""
+        sys.set_lazy_imports("all")
+        import test.test_import.data.lazy_imports.try_except_eager_from
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_eager_import_func(self):
+        """Imports inside functions should return modules, not proxies."""
+        sys.set_lazy_imports("all")
+        import test.test_import.data.lazy_imports.eager_import_func
+
+        f = test.test_import.data.lazy_imports.eager_import_func.f
+        self.assertEqual(type(f()), type(sys))
+
+
+class WithStatementTests(unittest.TestCase):
+    """Tests for lazy imports in with statement context."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_lazy_with(self):
+        """lazy import with 'with' statement should work."""
+        import test.test_import.data.lazy_imports.lazy_with
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_lazy_with_from(self):
+        """lazy from import with 'with' statement should work."""
+        import test.test_import.data.lazy_imports.lazy_with_from
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class PackageTests(unittest.TestCase):
+    """Tests for lazy imports with packages."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_lazy_import_pkg(self):
+        """lazy import of package submodule should load the package."""
+        import test.test_import.data.lazy_imports.lazy_import_pkg
+
+        self.assertIn("test.test_import.data.lazy_imports.pkg", sys.modules)
+        self.assertIn("test.test_import.data.lazy_imports.pkg.bar", sys.modules)
+
+    def test_lazy_import_pkg_cross_import(self):
+        """Cross-imports within package should preserve lazy imports."""
+        import test.test_import.data.lazy_imports.pkg.c
+
+        self.assertIn("test.test_import.data.lazy_imports.pkg", sys.modules)
+        self.assertIn("test.test_import.data.lazy_imports.pkg.c", sys.modules)
+        self.assertNotIn("test.test_import.data.lazy_imports.pkg.b", sys.modules)
+
+        g = test.test_import.data.lazy_imports.pkg.c.get_globals()
+        self.assertEqual(type(g["x"]), int)
+        self.assertEqual(type(g["b"]), types.LazyImportType)
+
+
+class DunderLazyImportTests(unittest.TestCase):
+    """Tests for __lazy_import__ builtin function."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_dunder_lazy_import(self):
+        """__lazy_import__ should create lazy import proxy."""
+        import test.test_import.data.lazy_imports.dunder_lazy_import
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_dunder_lazy_import_used(self):
+        """Using __lazy_import__ result should trigger module load."""
+        import test.test_import.data.lazy_imports.dunder_lazy_import_used
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_dunder_lazy_import_builtins(self):
+        """__lazy_import__ should use module's __builtins__ for __import__."""
+        from test.test_import.data.lazy_imports import dunder_lazy_import_builtins
+
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+        self.assertEqual(dunder_lazy_import_builtins.basic.basic2, 42)
+
+
+class SysLazyImportsAPITests(unittest.TestCase):
+    """Tests for sys lazy imports API functions."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_set_lazy_imports_requires_string(self):
+        """set_lazy_imports should reject non-string arguments."""
+        with self.assertRaises(TypeError):
+            sys.set_lazy_imports(True)
+        with self.assertRaises(TypeError):
+            sys.set_lazy_imports(None)
+        with self.assertRaises(TypeError):
+            sys.set_lazy_imports(1)
+
+    def test_set_lazy_imports_rejects_invalid_mode(self):
+        """set_lazy_imports should reject invalid mode strings."""
+        with self.assertRaises(ValueError):
+            sys.set_lazy_imports("invalid")
+        with self.assertRaises(ValueError):
+            sys.set_lazy_imports("on")
+        with self.assertRaises(ValueError):
+            sys.set_lazy_imports("off")
+
+    def test_get_lazy_imports_returns_string(self):
+        """get_lazy_imports should return string modes."""
+        sys.set_lazy_imports("normal")
+        self.assertEqual(sys.get_lazy_imports(), "normal")
+
+        sys.set_lazy_imports("all")
+        self.assertEqual(sys.get_lazy_imports(), "all")
+
+        sys.set_lazy_imports("none")
+        self.assertEqual(sys.get_lazy_imports(), "none")
+
+    def test_get_lazy_imports_filter_default(self):
+        """get_lazy_imports_filter should return None by default."""
+        sys.set_lazy_imports_filter(None)
+        self.assertIsNone(sys.get_lazy_imports_filter())
+
+    def test_set_and_get_lazy_imports_filter(self):
+        """set/get_lazy_imports_filter should round-trip filter function."""
+        def my_filter(name):
+            return name.startswith("test.")
+
+        sys.set_lazy_imports_filter(my_filter)
+        self.assertIs(sys.get_lazy_imports_filter(), my_filter)
+
+    def test_lazy_modules_attribute_is_set(self):
+        """sys.lazy_modules should be a set per PEP 810."""
+        self.assertIsInstance(sys.lazy_modules, dict)
+
+    def test_lazy_modules_tracks_lazy_imports(self):
+        """sys.lazy_modules should track lazily imported module names."""
+        code = textwrap.dedent("""
+            import sys
+            initial_count = len(sys.lazy_modules)
+            import test.test_import.data.lazy_imports.basic_unused
+            assert "test.test_import.data.lazy_imports" in sys.lazy_modules
+            assert sys.lazy_modules["test.test_import.data.lazy_imports"] == {"basic2"}
+            assert len(sys.lazy_modules) > initial_count
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class ErrorHandlingTests(unittest.TestCase):
+    """Tests for error handling during lazy import reification.
+
+    PEP 810: Errors during reification should show exception chaining with
+    both the lazy import definition location and the access location.
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_import_error_shows_chained_traceback(self):
+        """ImportError during reification should chain to show both definition and access."""
+        # Errors at reification must show where the lazy import was defined
+        # AND where the access happened, per PEP 810 "Reification" section
+        code = textwrap.dedent("""
+            import sys
+            lazy import test.test_import.data.lazy_imports.nonexistent_module
+
+            try:
+                x = test.test_import.data.lazy_imports.nonexistent_module
+            except ImportError as e:
+                # Should have __cause__ showing the original error
+                # The exception chain shows both where import was defined and where access happened
+                assert e.__cause__ is not None, "Expected chained exception"
+                print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_attribute_error_on_from_import_shows_chained_traceback(self):
+        """Accessing missing attribute from lazy from-import should chain errors."""
+        # Tests 'lazy from module import nonexistent' behavior
+        code = textwrap.dedent("""
+            import sys
+            lazy from test.test_import.data.lazy_imports.basic2 import nonexistent_name
+
+            try:
+                x = nonexistent_name
+            except ImportError as e:
+                # PEP 810: Enhanced error reporting through exception chaining
+                assert e.__cause__ is not None, "Expected chained exception"
+                print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_reification_retries_on_failure(self):
+        """Failed reification should allow retry on subsequent access.
+
+        PEP 810: "If reification fails, the lazy object is not reified or replaced.
+        Subsequent uses of the lazy object will re-try the reification."
+        """
+        code = textwrap.dedent("""
+            import sys
+            import types
+
+            lazy import test.test_import.data.lazy_imports.broken_module
+
+            # First access - should fail
+            try:
+                x = test.test_import.data.lazy_imports.broken_module
+            except ValueError:
+                pass
+
+            # The lazy object should still be a lazy proxy (not reified)
+            g = globals()
+            lazy_obj = g['test']
+            # The root 'test' binding should still allow retry
+            # Second access - should also fail (retry the import)
+            try:
+                x = test.test_import.data.lazy_imports.broken_module
+            except ValueError:
+                print("OK - retry worked")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_error_during_module_execution_propagates(self):
+        """Errors in module code during reification should propagate correctly."""
+        # Module that raises during import should propagate with chaining
+        code = textwrap.dedent("""
+            import sys
+            lazy import test.test_import.data.lazy_imports.broken_module
+
+            try:
+                _ = test.test_import.data.lazy_imports.broken_module
+                print("FAIL - should have raised")
+            except ValueError as e:
+                # The ValueError from the module should be the cause
+                if "always fails" in str(e) or (e.__cause__ and "always fails" in str(e.__cause__)):
+                    print("OK")
+                else:
+                    print(f"FAIL - wrong error: {e}")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class GlobalsAndDictTests(unittest.TestCase):
+    """Tests for globals() and __dict__ behavior with lazy imports.
+
+    PEP 810: "Calling globals() or accessing a module's __dict__ does not trigger
+    reification â€“ they return the module's dictionary, and accessing lazy objects
+    through that dictionary still returns lazy proxy objects."
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_globals_returns_lazy_proxy_when_accessed_from_function(self):
+        """globals() accessed from a function should return lazy proxy without reification.
+
+        Note: At module level, accessing globals()['name'] triggers LOAD_NAME which
+        automatically resolves lazy imports. Inside a function, accessing globals()['name']
+        uses BINARY_SUBSCR which returns the lazy proxy without resolution.
+        """
+        code = textwrap.dedent("""
+            import sys
+            import types
+
+            lazy from test.test_import.data.lazy_imports.basic2 import x
+
+            # Check that module is not yet loaded
+            assert 'test.test_import.data.lazy_imports.basic2' not in sys.modules
+
+            def check_lazy():
+                # Access through globals() from inside a function
+                g = globals()
+                lazy_obj = g['x']
+                return type(lazy_obj) is types.LazyImportType
+
+            # Inside function, should get lazy proxy
+            is_lazy = check_lazy()
+            assert is_lazy, "Expected LazyImportType from function scope"
+
+            # Module should STILL not be loaded
+            assert 'test.test_import.data.lazy_imports.basic2' not in sys.modules
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_globals_dict_access_returns_lazy_proxy_inline(self):
+        """Accessing globals()['name'] inline should return lazy proxy.
+
+        Note: Assigning g['name'] to a local variable at module level triggers
+        reification due to STORE_NAME bytecode. Inline access preserves laziness.
+        """
+        code = textwrap.dedent("""
+            import sys
+            import types
+            lazy import json
+            # Inline access without assignment to local variable preserves lazy proxy
+            assert type(globals()['json']) is types.LazyImportType
+            assert 'json' not in sys.modules
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_module_dict_returns_lazy_proxy_without_reifying(self):
+        """module.__dict__ access should not trigger reification."""
+        import test.test_import.data.lazy_imports.globals_access
+
+        # Module not loaded yet via direct dict access
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+        # Access via get_from_globals should return lazy proxy
+        lazy_obj = test.test_import.data.lazy_imports.globals_access.get_from_globals()
+        self.assertEqual(type(lazy_obj), types.LazyImportType)
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_direct_access_triggers_reification(self):
+        """Direct name access (not through globals()) should trigger reification."""
+        import test.test_import.data.lazy_imports.globals_access
+
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+        # Direct access should reify
+        result = test.test_import.data.lazy_imports.globals_access.get_direct()
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_resolve_method_forces_reification(self):
+        """Calling resolve() on lazy proxy should force reification.
+
+        Note: Must access lazy proxy from within a function to avoid automatic
+        reification by LOAD_NAME at module level.
+        """
+        code = textwrap.dedent("""
+            import sys
+            import types
+
+            lazy from test.test_import.data.lazy_imports.basic2 import x
+
+            assert 'test.test_import.data.lazy_imports.basic2' not in sys.modules
+
+            def test_resolve():
+                g = globals()
+                lazy_obj = g['x']
+                assert type(lazy_obj) is types.LazyImportType, f"Expected lazy proxy, got {type(lazy_obj)}"
+
+                resolved = lazy_obj.resolve()
+
+                # Now module should be loaded
+                assert 'test.test_import.data.lazy_imports.basic2' in sys.modules
+                assert resolved == 42  # x is 42 in basic2.py
+                return True
+
+            assert test_resolve()
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_add_lazy_to_globals(self):
+        code = textwrap.dedent("""
+            import sys
+            import types
+
+            lazy from test.test_import.data.lazy_imports import basic2
+
+            assert 'test.test_import.data.lazy_imports.basic2' not in sys.modules
+
+            class C: pass
+            sneaky = C()
+            sneaky.x = 1
+
+            def f():
+                t = 0
+                for _ in range(5):
+                    t += sneaky.x
+                return t
+
+            f()
+            globals()["sneaky"] = globals()["basic2"]
+            assert f() == 210
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class MultipleNameFromImportTests(unittest.TestCase):
+    """Tests for lazy from ... import with multiple names.
+
+    PEP 810: "When using lazy from ... import, each imported name is bound to a
+    lazy proxy object. The first access to any of these names triggers loading
+    of the entire module and reifies only that specific name to its actual value.
+    Other names remain as lazy proxies until they are accessed."
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_accessing_one_name_leaves_others_as_proxies(self):
+        """Accessing one name from multi-name import should leave others lazy."""
+        code = textwrap.dedent("""
+            import sys
+            import types
+
+            lazy from test.test_import.data.lazy_imports.basic2 import f, x
+
+            # Neither should be loaded yet
+            assert 'test.test_import.data.lazy_imports.basic2' not in sys.modules
+
+            g = globals()
+            assert type(g['f']) is types.LazyImportType
+            assert type(g['x']) is types.LazyImportType
+
+            # Access 'x' - this loads the module and reifies only 'x'
+            value = x
+            assert value == 42
+
+            # Module is now loaded
+            assert 'test.test_import.data.lazy_imports.basic2' in sys.modules
+
+            # 'x' should be reified (int), 'f' should still be lazy proxy
+            assert type(g['x']) is int, f"Expected int, got {type(g['x'])}"
+            assert type(g['f']) is types.LazyImportType, f"Expected LazyImportType, got {type(g['f'])}"
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_all_names_reified_after_all_accessed(self):
+        """All names should be reified after each is accessed."""
+        code = textwrap.dedent("""
+            import sys
+            import types
+
+            lazy from test.test_import.data.lazy_imports.basic2 import f, x
+
+            g = globals()
+
+            # Access both
+            _ = x
+            _ = f
+
+            # Both should be reified now
+            assert type(g['x']) is int
+            assert callable(g['f'])
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class SysLazyModulesTrackingTests(unittest.TestCase):
+    """Tests for sys.lazy_modules tracking behavior.
+
+    PEP 810: "When the module is reified, it's removed from sys.lazy_modules"
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_module_added_to_lazy_modules_on_lazy_import(self):
+        """Module should be added to sys.lazy_modules when lazily imported."""
+        # PEP 810 states lazy_modules tracks modules that have been lazily imported
+        # Note: The current implementation keeps modules in lazy_modules even after
+        # reification (primarily for diagnostics and introspection)
+        code = textwrap.dedent("""
+            import sys
+
+            initial_count = len(sys.lazy_modules)
+
+            lazy import test.test_import.data.lazy_imports.basic2
+
+            # Should be in lazy_modules after lazy import
+            assert "test.test_import.data.lazy_imports" in sys.lazy_modules
+            assert sys.lazy_modules["test.test_import.data.lazy_imports"] == {"basic2"}
+            assert len(sys.lazy_modules) > initial_count
+
+            # Trigger reification
+            _ = test.test_import.data.lazy_imports.basic2.x
+
+            # Module should still be tracked (for diagnostics per PEP 810)
+            assert "test.test_import.data.lazy_imports" not in sys.lazy_modules
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_lazy_modules_is_per_interpreter(self):
+        """Each interpreter should have independent sys.lazy_modules."""
+        # Basic test that sys.lazy_modules exists and is a set
+        self.assertIsInstance(sys.lazy_modules, dict)
+
+
+class CommandLineAndEnvVarTests(unittest.TestCase):
+    """Tests for command-line and environment variable control.
+
+    PEP 810: The global lazy imports flag can be controlled through:
+    - The -X lazy_imports=<mode> command-line option
+    - The PYTHON_LAZY_IMPORTS=<mode> environment variable
+    """
+
+    def test_cli_lazy_imports_all_makes_regular_imports_lazy(self):
+        """-X lazy_imports=all should make all imports potentially lazy."""
+        code = textwrap.dedent("""
+            import sys
+            # In 'all' mode, regular imports become lazy
+            import json
+            # json should not be in sys.modules yet (lazy)
+            # Actually accessing it triggers reification
+            if 'json' not in sys.modules:
+                print("LAZY")
+            else:
+                print("EAGER")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-X", "lazy_imports=all", "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("LAZY", result.stdout)
+
+    def test_cli_lazy_imports_none_forces_all_imports_eager(self):
+        """-X lazy_imports=none should force all imports to be eager."""
+        code = textwrap.dedent("""
+            import sys
+            # Even explicit lazy imports should be eager in 'none' mode
+            lazy import json
+            if 'json' in sys.modules:
+                print("EAGER")
+            else:
+                print("LAZY")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-X", "lazy_imports=none", "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("EAGER", result.stdout)
+
+    def test_cli_lazy_imports_normal_respects_lazy_keyword_only(self):
+        """-X lazy_imports=normal should respect lazy keyword only."""
+        # Note: Use test modules instead of stdlib modules to avoid
+        # modules already loaded by the interpreter startup
+        code = textwrap.dedent("""
+            import sys
+            import test.test_import.data.lazy_imports.basic2  # Should be eager
+            lazy import test.test_import.data.lazy_imports.pkg.b  # Should be lazy
+
+            eager_loaded = 'test.test_import.data.lazy_imports.basic2' in sys.modules
+            lazy_loaded = 'test.test_import.data.lazy_imports.pkg.b' in sys.modules
+
+            if eager_loaded and not lazy_loaded:
+                print("OK")
+            else:
+                print(f"FAIL: eager={eager_loaded}, lazy={lazy_loaded}")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-X", "lazy_imports=normal", "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_env_var_lazy_imports_all_enables_global_lazy(self):
+        """PYTHON_LAZY_IMPORTS=all should enable global lazy imports."""
+        code = textwrap.dedent("""
+            import sys
+            import json
+            if 'json' not in sys.modules:
+                print("LAZY")
+            else:
+                print("EAGER")
+        """)
+        import os
+        env = os.environ.copy()
+        env["PYTHON_LAZY_IMPORTS"] = "all"
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True,
+            env=env
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("LAZY", result.stdout)
+
+    def test_env_var_lazy_imports_none_disables_all_lazy(self):
+        """PYTHON_LAZY_IMPORTS=none should disable all lazy imports."""
+        code = textwrap.dedent("""
+            import sys
+            lazy import json
+            if 'json' in sys.modules:
+                print("EAGER")
+            else:
+                print("LAZY")
+        """)
+        import os
+        env = os.environ.copy()
+        env["PYTHON_LAZY_IMPORTS"] = "none"
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True,
+            env=env
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("EAGER", result.stdout)
+
+    def test_cli_overrides_env_var(self):
+        """Command-line option should take precedence over environment variable."""
+        # PEP 810: -X lazy_imports takes precedence over PYTHON_LAZY_IMPORTS
+        code = textwrap.dedent("""
+            import sys
+            lazy import json
+            if 'json' in sys.modules:
+                print("EAGER")
+            else:
+                print("LAZY")
+        """)
+        import os
+        env = os.environ.copy()
+        env["PYTHON_LAZY_IMPORTS"] = "all"  # env says all
+        result = subprocess.run(
+            [sys.executable, "-X", "lazy_imports=none", "-c", code],  # CLI says none
+            capture_output=True,
+            text=True,
+            env=env
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        # CLI should win - imports should be eager
+        self.assertIn("EAGER", result.stdout)
+
+    def test_sys_set_lazy_imports_overrides_cli(self):
+        """sys.set_lazy_imports() should take precedence over CLI option."""
+        code = textwrap.dedent("""
+            import sys
+            sys.set_lazy_imports("none")  # Override CLI
+            lazy import json
+            if 'json' in sys.modules:
+                print("EAGER")
+            else:
+                print("LAZY")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-X", "lazy_imports=all", "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("EAGER", result.stdout)
+
+
+class FilterFunctionSignatureTests(unittest.TestCase):
+    """Tests for the filter function signature per PEP 810.
+
+    PEP 810: func(importer: str, name: str, fromlist: tuple[str, ...] | None) -> bool
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_filter_receives_correct_arguments_for_import(self):
+        """Filter should receive (importer, name, fromlist=None) for 'import x'."""
+        code = textwrap.dedent("""
+            import sys
+
+            received_args = []
+
+            def my_filter(importer, name, fromlist):
+                received_args.append((importer, name, fromlist))
+                return True
+
+            sys.set_lazy_imports_filter(my_filter)
+
+            lazy import json
+
+            assert len(received_args) == 1, f"Expected 1 call, got {len(received_args)}"
+            importer, name, fromlist = received_args[0]
+            assert name == "json", f"Expected name='json', got {name!r}"
+            assert fromlist is None, f"Expected fromlist=None, got {fromlist!r}"
+            assert isinstance(importer, str), f"Expected str importer, got {type(importer)}"
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_filter_receives_fromlist_for_from_import(self):
+        """Filter should receive fromlist tuple for 'from x import y, z'."""
+        code = textwrap.dedent("""
+            import sys
+
+            received_args = []
+
+            def my_filter(importer, name, fromlist):
+                received_args.append((importer, name, fromlist))
+                return True
+
+            sys.set_lazy_imports_filter(my_filter)
+
+            lazy from json import dumps, loads
+
+            assert len(received_args) == 1, f"Expected 1 call, got {len(received_args)}"
+            importer, name, fromlist = received_args[0]
+            assert name == "json", f"Expected name='json', got {name!r}"
+            assert fromlist == ("dumps", "loads"), f"Expected ('dumps', 'loads'), got {fromlist!r}"
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_filter_returning_false_forces_eager_import(self):
+        """Filter returning False should make import eager."""
+        code = textwrap.dedent("""
+            import sys
+
+            def deny_filter(importer, name, fromlist):
+                return False
+
+            sys.set_lazy_imports_filter(deny_filter)
+
+            lazy import json
+
+            # Should be eager due to filter
+            if 'json' in sys.modules:
+                print("EAGER")
+            else:
+                print("LAZY")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}")
+        self.assertIn("EAGER", result.stdout)
+
+
+class AdditionalSyntaxRestrictionTests(unittest.TestCase):
+    """Additional syntax restriction tests per PEP 810."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_lazy_import_inside_class_raises_syntax_error(self):
+        """lazy import inside class body should raise SyntaxError."""
+        # PEP 810: "The soft keyword is only allowed at the global (module) level,
+        # not inside functions, class bodies, try blocks, or import *"
+        with self.assertRaises(SyntaxError):
+            import test.test_import.data.lazy_imports.lazy_class_body
+
+
+class MixedLazyEagerImportTests(unittest.TestCase):
+    """Tests for mixing lazy and eager imports of the same module.
+
+    PEP 810: "If module foo is imported both lazily and eagerly in the same
+    program, the eager import takes precedence and both bindings resolve to
+    the same module object."
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_eager_import_before_lazy_resolves_to_same_module(self):
+        """Eager import before lazy should make lazy resolve to same module."""
+        code = textwrap.dedent("""
+            import sys
+            import json  # Eager import first
+
+            lazy import json as lazy_json  # Lazy import same module
+
+            # lazy_json should resolve to the same object
+            assert json is lazy_json, "Lazy and eager imports should resolve to same module"
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_lazy_import_before_eager_resolves_to_same_module(self):
+        """Lazy import followed by eager should both point to same module."""
+        code = textwrap.dedent("""
+            import sys
+
+            lazy import json as lazy_json
+
+            # Lazy not loaded yet
+            assert 'json' not in sys.modules
+
+            import json  # Eager import triggers load
+
+            # Both should be the same object
+            assert json is lazy_json
+            print("OK")
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class RelativeImportTests(unittest.TestCase):
+    """Tests for relative imports with lazy keyword."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_relative_lazy_import(self):
+        """lazy from . import submodule should work."""
+        from test.test_import.data.lazy_imports import relative_lazy
+
+        # basic2 should not be loaded yet
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+        # Access triggers reification
+        result = relative_lazy.get_basic2()
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+    def test_relative_lazy_from_import(self):
+        """lazy from .module import name should work."""
+        from test.test_import.data.lazy_imports import relative_lazy_from
+
+        # basic2 should not be loaded yet
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+        # Access triggers reification
+        result = relative_lazy_from.get_x()
+        self.assertEqual(result, 42)
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class LazyModulesCompatibilityFromImportTests(unittest.TestCase):
+    """Tests for __lazy_modules__ with from imports.
+
+    PEP 810: "When a module is made lazy this way, from-imports using that
+    module are also lazy"
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_lazy_modules_makes_from_imports_lazy(self):
+        """__lazy_modules__ should make from imports of listed modules lazy."""
+        from test.test_import.data.lazy_imports import lazy_compat_from
+
+        # basic2 should not be loaded yet because it's in __lazy_modules__
+        self.assertNotIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+        # Access triggers reification
+        result = lazy_compat_from.get_x()
+        self.assertEqual(result, 42)
+        self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules)
+
+
+class ImportStateAtReificationTests(unittest.TestCase):
+    """Tests for import system state at reification time.
+
+    PEP 810: "Reification still calls __import__ to resolve the import, which uses
+    the state of the import system (e.g. sys.path, sys.meta_path, sys.path_hooks
+    and __import__) at reification time, not the state when the lazy import
+    statement was evaluated."
+    """
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_sys_path_at_reification_time_is_used(self):
+        """sys.path changes after lazy import should affect reification."""
+        code = textwrap.dedent("""
+            import sys
+            import tempfile
+            import os
+
+            # Create a temporary module
+            with tempfile.TemporaryDirectory() as tmpdir:
+                mod_path = os.path.join(tmpdir, "dynamic_test_module.py")
+                with open(mod_path, "w") as f:
+                    f.write("VALUE = 'from_temp_dir'\\n")
+
+                # Lazy import before adding to path
+                lazy import dynamic_test_module
+
+                # Module cannot be found yet
+                try:
+                    _ = dynamic_test_module
+                    print("FAIL - should not find module")
+                except ModuleNotFoundError:
+                    pass
+
+                # Now add temp dir to path
+                sys.path.insert(0, tmpdir)
+
+                # Now reification should succeed using current sys.path
+                assert dynamic_test_module.VALUE == 'from_temp_dir'
+                print("OK")
+
+                sys.path.remove(tmpdir)
+        """)
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class ThreadSafetyTests(unittest.TestCase):
+    """Tests for thread-safety of lazy imports."""
+
+    def tearDown(self):
+        for key in list(sys.modules.keys()):
+            if key.startswith('test.test_import.data.lazy_imports'):
+                del sys.modules[key]
+
+        sys.set_lazy_imports_filter(None)
+        sys.set_lazy_imports("normal")
+
+    def test_concurrent_lazy_import_reification(self):
+        """Multiple threads racing to reify the same lazy import should succeed."""
+        from test.test_import.data.lazy_imports import basic_unused
+
+        num_threads = 10
+        results = [None] * num_threads
+        errors = []
+        barrier = threading.Barrier(num_threads)
+
+        def access_lazy_import(idx):
+            try:
+                barrier.wait()
+                module = basic_unused.test.test_import.data.lazy_imports.basic2
+                results[idx] = module
+            except Exception as e:
+                errors.append((idx, e))
+
+        threads = [
+            threading.Thread(target=access_lazy_import, args=(i,))
+            for i in range(num_threads)
+        ]
+
+        for t in threads:
+            t.start()
+        for t in threads:
+            t.join()
+
+        self.assertEqual(errors, [], f"Errors occurred: {errors}")
+        self.assertTrue(all(r is not None for r in results))
+        first_module = results[0]
+        for r in results[1:]:
+            self.assertIs(r, first_module)
+
+    def test_concurrent_reification_multiple_modules(self):
+        """Multiple threads reifying different lazy imports concurrently."""
+        code = textwrap.dedent("""
+            import sys
+            import threading
+
+            sys.set_lazy_imports("all")
+
+            lazy import json
+            lazy import os
+            lazy import io
+            lazy import re
+
+            num_threads = 8
+            results = {}
+            errors = []
+            barrier = threading.Barrier(num_threads)
+
+            def access_modules(idx):
+                try:
+                    barrier.wait()
+                    mods = [json, os, io, re]
+                    results[idx] = [type(m).__name__ for m in mods]
+                except Exception as e:
+                    errors.append((idx, e))
+
+            threads = [
+                threading.Thread(target=access_modules, args=(i,))
+                for i in range(num_threads)
+            ]
+
+            for t in threads:
+                t.start()
+            for t in threads:
+                t.join()
+
+            assert not errors, f"Errors: {errors}"
+            for idx, mods in results.items():
+                assert all(m == 'module' for m in mods), f"Thread {idx} got: {mods}"
+
+            print("OK")
+        """)
+
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_concurrent_lazy_modules_set_updates(self):
+        """Multiple threads creating lazy imports should safely update sys.lazy_modules."""
+        code = textwrap.dedent("""
+            import sys
+            import threading
+
+            num_threads = 16
+            iterations = 50
+            errors = []
+            barrier = threading.Barrier(num_threads)
+
+            def create_lazy_imports(idx):
+                try:
+                    barrier.wait()
+                    for i in range(iterations):
+                        exec(f"lazy import json as json_{idx}_{i}", globals())
+                        exec(f"lazy import os as os_{idx}_{i}", globals())
+                except Exception as e:
+                    errors.append((idx, e))
+
+            threads = [
+                threading.Thread(target=create_lazy_imports, args=(i,))
+                for i in range(num_threads)
+            ]
+
+            for t in threads:
+                t.start()
+            for t in threads:
+                t.join()
+
+            assert not errors, f"Errors: {errors}"
+            assert isinstance(sys.lazy_modules, dict), "sys.lazy_modules is not a dict"
+            print("OK")
+        """)
+
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_concurrent_reification_same_module_high_contention(self):
+        """High contention: many threads reifying the exact same lazy import."""
+        code = textwrap.dedent("""
+            import sys
+            import threading
+            import types
+
+            sys.set_lazy_imports("all")
+
+            lazy import json
+
+            num_threads = 20
+            results = [None] * num_threads
+            errors = []
+            barrier = threading.Barrier(num_threads)
+
+            def access_json(idx):
+                try:
+                    barrier.wait()
+                    for _ in range(100):
+                        _ = json.dumps
+                        _ = json.loads
+                    results[idx] = json
+                except Exception as e:
+                    errors.append((idx, e))
+
+            threads = [
+                threading.Thread(target=access_json, args=(i,))
+                for i in range(num_threads)
+            ]
+
+            for t in threads:
+                t.start()
+            for t in threads:
+                t.join()
+
+            assert not errors, f"Errors: {errors}"
+            assert all(r is not None for r in results), "Some threads got None"
+            first = results[0]
+            assert all(r is first for r in results), "Inconsistent module objects"
+            assert not isinstance(first, types.LazyImportType), "Got lazy import instead of module"
+            print("OK")
+        """)
+
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+    def test_concurrent_reification_with_module_attribute_access(self):
+        """Threads racing to reify and immediately access module attributes."""
+        code = textwrap.dedent("""
+            import sys
+            import threading
+
+            sys.set_lazy_imports("all")
+
+            lazy import collections
+            lazy import functools
+            lazy import itertools
+
+            num_threads = 12
+            results = {}
+            errors = []
+            barrier = threading.Barrier(num_threads)
+
+            def stress_lazy_imports(idx):
+                try:
+                    barrier.wait()
+                    for _ in range(50):
+                        _ = collections.OrderedDict
+                        _ = functools.partial
+                        _ = itertools.chain
+                        _ = collections.defaultdict
+                        _ = functools.lru_cache
+                        _ = itertools.islice
+                    results[idx] = (
+                        type(collections).__name__,
+                        type(functools).__name__,
+                        type(itertools).__name__,
+                    )
+                except Exception as e:
+                    errors.append((idx, e))
+
+            threads = [
+                threading.Thread(target=stress_lazy_imports, args=(i,))
+                for i in range(num_threads)
+            ]
+
+            for t in threads:
+                t.start()
+            for t in threads:
+                t.join()
+
+            assert not errors, f"Errors: {errors}"
+            for idx, types_tuple in results.items():
+                assert all(t == 'module' for t in types_tuple), f"Thread {idx}: {types_tuple}"
+            print("OK")
+        """)
+
+        result = subprocess.run(
+            [sys.executable, "-c", code],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0, f"stdout: {result.stdout}, stderr: {result.stderr}")
+        self.assertIn("OK", result.stdout)
+
+
+class LazyImportDisTests(unittest.TestCase):
+    def test_lazy_import_dis(self):
+        """dis should properly show lazy import"""
+        code = compile("lazy import foo", "exec", "exec")
+        f = io.StringIO()
+        dis.dis(code, file=f)
+        self.assertIn("foo + lazy", f.getvalue())
+
+    def test_normal_import_dis(self):
+        """non lazy imports should just show the name"""
+        code = compile("import foo", "exec", "exec")
+        f = io.StringIO()
+        dis.dis(code, file=f)
+        for line in f.getvalue().split('\n'):
+            if "IMPORT_NAME" in line:
+                self.assertIn("(foo)", line)
+                break
+        else:
+            self.assertFail("IMPORT_NAME not found")
+
+
+@unittest.skipIf(_testcapi is None, 'need the _testcapi module')
+class LazyCApiTests(unittest.TestCase):
+    def tearDown(self):
+        sys.set_lazy_imports("normal")
+        sys.set_lazy_imports_filter(None)
+
+    def test_set_matches_sys(self):
+        self.assertEqual(_testcapi.PyImport_GetLazyImportsMode(), sys.get_lazy_imports())
+        for mode in ("normal", "all", "none"):
+            _testcapi.PyImport_SetLazyImportsMode(mode)
+            self.assertEqual(_testcapi.PyImport_GetLazyImportsMode(), sys.get_lazy_imports())
+
+    def test_filter_matches_sys(self):
+        self.assertEqual(_testcapi.PyImport_GetLazyImportsFilter(), sys.get_lazy_imports_filter())
+
+        def filter(*args):
+            pass
+
+        _testcapi.PyImport_SetLazyImportsFilter(filter)
+        self.assertEqual(_testcapi.PyImport_GetLazyImportsFilter(), sys.get_lazy_imports_filter())
+
+    def test_set_bad_filter(self):
+        self.assertRaises(ValueError, _testcapi.PyImport_SetLazyImportsFilter, 42)
+
+
+if __name__ == '__main__':
+    unittest.main()
index 656a1e441e0e473874cd9fd34eac4a4768b25324..3c55b6bdaeee9e85abcd4f204876bd5968880d05 100644 (file)
@@ -89,10 +89,42 @@ class TestUtils(TestCase):
             ("obj.list", [(".", "op")]),
             ("obj.match", [(".", "op")]),
             ("b. \\\n format", [(".", "op")]),
+            ("lazy", []),
+            ("lazy()", [('(', 'op'), (')', 'op')]),
             # highlights
             ("set", [("set", "builtin")]),
             ("list", [("list", "builtin")]),
             ("    \n dict", [("dict", "builtin")]),
+            (
+                "    lazy import",
+                [("lazy", "soft_keyword"), ("import", "keyword")],
+            ),
+            (
+                "lazy from cool_people import pablo",
+                [
+                    ("lazy", "soft_keyword"),
+                    ("from", "keyword"),
+                    ("import", "keyword"),
+                ],
+            ),
+            (
+                "if sad: lazy import happy",
+                [
+                    ("if", "keyword"),
+                    (":", "op"),
+                    ("lazy", "soft_keyword"),
+                    ("import", "keyword"),
+                ],
+            ),
+            (
+                "pass; lazy import z",
+                [
+                    ("pass", "keyword"),
+                    (";", "op"),
+                    ("lazy", "soft_keyword"),
+                    ("import", "keyword"),
+                ],
+            ),
         ]
         for code, expected_highlights in cases:
             with self.subTest(code=code):
index 93f0b98de71d8102f8011621485ebfa54ee82de9..19427f2469ec43cf783f4a1e5b9f387518ff8476 100644 (file)
@@ -3475,6 +3475,128 @@ while 1:
         ]:
             self._check_error(f"x = {lhs_stmt} if 1 else {rhs_stmt}", msg)
 
+
+class LazyImportRestrictionTestCase(SyntaxErrorTestCase):
+    """Test syntax restrictions for lazy imports."""
+
+    def test_lazy_import_in_try_block(self):
+        """Test that lazy imports are not allowed inside try blocks."""
+        self._check_error("""\
+try:
+    lazy import os
+except:
+    pass
+""", "lazy import not allowed inside try/except blocks")
+
+        self._check_error("""\
+try:
+    lazy from sys import path
+except ImportError:
+    pass
+""", "lazy from ... import not allowed inside try/except blocks")
+
+    def test_lazy_import_in_trystar_block(self):
+        """Test that lazy imports are not allowed inside try* blocks."""
+        self._check_error("""\
+try:
+    lazy import json
+except* Exception:
+    pass
+""", "lazy import not allowed inside try/except blocks")
+
+        self._check_error("""\
+try:
+    lazy from collections import defaultdict
+except* ImportError:
+    pass
+""", "lazy from ... import not allowed inside try/except blocks")
+
+    def test_lazy_import_in_except_block(self):
+        """Test that lazy imports are not allowed inside except blocks."""
+        self._check_error("""\
+try:
+    sys.modules # trigger the except block
+except* Exception:
+   lazy import sys
+""", "lazy import not allowed inside try/except blocks")
+
+    def test_lazy_import_in_function(self):
+        """Test that lazy imports are not allowed inside functions."""
+        self._check_error("""\
+def func():
+    lazy import math
+""", "lazy import not allowed inside functions")
+
+        self._check_error("""\
+def func():
+    lazy from datetime import datetime
+""", "lazy from ... import not allowed inside functions")
+
+    def test_lazy_import_in_async_function(self):
+        """Test that lazy imports are not allowed inside async functions."""
+        self._check_error("""\
+async def async_func():
+    lazy import asyncio
+""", "lazy import not allowed inside functions")
+
+        self._check_error("""\
+async def async_func():
+    lazy from json import loads
+""", "lazy from ... import not allowed inside functions")
+
+    def test_lazy_import_in_class(self):
+        """Test that lazy imports are not allowed inside classes."""
+        self._check_error("""\
+class MyClass:
+    lazy import typing
+""", "lazy import not allowed inside classes")
+
+        self._check_error("""\
+class MyClass:
+    lazy from abc import ABC
+""", "lazy from ... import not allowed inside classes")
+
+    def test_lazy_import_star_forbidden(self):
+        """Test that 'lazy from ... import *' is forbidden everywhere."""
+        # At module level should also be forbidden
+        self._check_error("lazy from os import *",
+                         "lazy from ... import \\* is not allowed")
+
+        # Inside function should give lazy function error first
+        self._check_error("""\
+def func():
+    lazy from sys import *
+""", "lazy from ... import not allowed inside functions")
+
+    def test_lazy_import_nested_scopes(self):
+        """Test lazy imports in nested scopes."""
+        self._check_error("""\
+class Outer:
+    def method(self):
+        lazy import sys
+""", "lazy import not allowed inside functions")
+
+        self._check_error("""\
+def outer():
+    class Inner:
+        lazy import json
+""", "lazy import not allowed inside classes")
+
+        self._check_error("""\
+def outer():
+    def inner():
+        lazy from collections import deque
+""", "lazy from ... import not allowed inside functions")
+
+    def test_lazy_import_valid_cases(self):
+        """Test that lazy imports work at module level."""
+        # These should compile without errors
+        compile("lazy import os", "<test>", "exec")
+        compile("lazy from sys import path", "<test>", "exec")
+        compile("lazy import json as j", "<test>", "exec")
+        compile("lazy from datetime import datetime as dt", "<test>", "exec")
+
+
 def load_tests(loader, tests, pattern):
     tests.addTest(doctest.DocTestSuite())
     return tests
index b44e0c9779aa599c875267bc380418bfdd974e69..8974361c2537d24cc46ba1bd3ff36e505d01d968 100644 (file)
@@ -865,7 +865,8 @@ class SysModuleTest(unittest.TestCase):
                  "dont_write_bytecode", "no_user_site", "no_site",
                  "ignore_environment", "verbose", "bytes_warning", "quiet",
                  "hash_randomization", "isolated", "dev_mode", "utf8_mode",
-                 "warn_default_encoding", "safe_path", "int_max_str_digits")
+                 "warn_default_encoding", "safe_path", "int_max_str_digits",
+                 "lazy_imports")
         for attr in attrs:
             self.assertHasAttr(sys.flags, attr)
             attr_type = bool if attr in ("dev_mode", "safe_path") else int
index a4a49fd44bb2e07b490a48b9d60f40bd92fa4353..eaca62b12d3eb1261dd089f8ea7729bbedddc38c 100644 (file)
@@ -5319,5 +5319,47 @@ class TestColorizedTraceback(unittest.TestCase):
         ]
         self.assertEqual(actual, expected(**colors))
 
+
+class TestLazyImportSuggestions(unittest.TestCase):
+    """Test that lazy imports are not reified when computing AttributeError suggestions."""
+
+    def test_attribute_error_does_not_reify_lazy_imports(self):
+        """Printing an AttributeError should not trigger lazy import reification."""
+        # pkg.bar prints "BAR_MODULE_LOADED" when imported.
+        # If lazy import is reified during suggestion computation, we'll see it.
+        code = textwrap.dedent("""
+            lazy import test.test_import.data.lazy_imports.pkg.bar
+            test.test_import.data.lazy_imports.pkg.nonexistent
+        """)
+        rc, stdout, stderr = assert_python_failure('-c', code)
+        self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
+
+    def test_traceback_formatting_does_not_reify_lazy_imports(self):
+        """Formatting a traceback should not trigger lazy import reification."""
+        code = textwrap.dedent("""
+            import traceback
+            lazy import test.test_import.data.lazy_imports.pkg.bar
+            try:
+                test.test_import.data.lazy_imports.pkg.nonexistent
+            except AttributeError:
+                traceback.format_exc()
+            print("OK")
+        """)
+        rc, stdout, stderr = assert_python_ok('-c', code)
+        self.assertIn(b"OK", stdout)
+        self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
+
+    def test_suggestion_still_works_for_non_lazy_attributes(self):
+        """Suggestions should still work for non-lazy module attributes."""
+        code = textwrap.dedent("""
+            lazy import test.test_import.data.lazy_imports.pkg.bar
+            # Typo for __name__
+            test.test_import.data.lazy_imports.pkg.__nme__
+        """)
+        rc, stdout, stderr = assert_python_failure('-c', code)
+        self.assertIn(b"__name__", stderr)
+        self.assertNotIn(b"BAR_MODULE_LOADED", stdout)
+
+
 if __name__ == "__main__":
     unittest.main()
index b0d2348c0e1cba9ebaa862be8a40a87319b32e17..39d57c5f5b61c9a8b2761b5f2ebc1e65cfef92b3 100644 (file)
@@ -41,7 +41,7 @@ def clear_typing_caches():
 class TypesTests(unittest.TestCase):
 
     def test_names(self):
-        c_only_names = {'CapsuleType'}
+        c_only_names = {'CapsuleType', 'LazyImportType'}
         ignored = {'new_class', 'resolve_bases', 'prepare_class',
                    'get_original_bases', 'DynamicClassAttribute', 'coroutine'}
 
@@ -55,10 +55,11 @@ class TypesTests(unittest.TestCase):
             'CoroutineType', 'EllipsisType', 'FrameType', 'FunctionType',
             'FrameLocalsProxyType',
             'GeneratorType', 'GenericAlias', 'GetSetDescriptorType',
-            'LambdaType', 'MappingProxyType', 'MemberDescriptorType',
-            'MethodDescriptorType', 'MethodType', 'MethodWrapperType',
-            'ModuleType', 'NoneType', 'NotImplementedType', 'SimpleNamespace',
-            'TracebackType', 'UnionType', 'WrapperDescriptorType',
+            'LambdaType', 'LazyImportType', 'MappingProxyType',
+            'MemberDescriptorType', 'MethodDescriptorType', 'MethodType',
+            'MethodWrapperType', 'ModuleType', 'NoneType',
+            'NotImplementedType', 'SimpleNamespace', 'TracebackType',
+            'UnionType', 'WrapperDescriptorType',
         }
         self.assertEqual(all_names, set(c_types.__all__))
         self.assertEqual(all_names - c_only_names, set(py_types.__all__))
index 97d83f3ddd3297aaac155147b35cbde3b4ee53b4..b121733c27fd8c1b233d35d7da5f7460fdd20811 100644 (file)
@@ -5,6 +5,7 @@ import itertools
 import linecache
 import sys
 import textwrap
+import types
 import warnings
 import codeop
 import keyword
@@ -1629,12 +1630,29 @@ def _substitution_cost(ch_a, ch_b):
     return _MOVE_COST
 
 
+def _is_lazy_import(obj, attr_name):
+    """Check if attr_name in obj's __dict__ is a lazy import.
+
+    Returns True if obj is a module and the attribute is a LazyImportType,
+    False otherwise. This avoids triggering module loading when computing
+    suggestions for AttributeError.
+    """
+    if not isinstance(obj, types.ModuleType):
+        return False
+    obj_dict = getattr(obj, '__dict__', None)
+    if obj_dict is None:
+        return False
+    attr_value = obj_dict.get(attr_name)
+    return isinstance(attr_value, types.LazyImportType)
+
+
 def _check_for_nested_attribute(obj, wrong_name, attrs):
     """Check if any attribute of obj has the wrong_name as a nested attribute.
 
     Returns the first nested attribute suggestion found, or None.
     Limited to checking 20 attributes.
     Only considers non-descriptor attributes to avoid executing arbitrary code.
+    Skips lazy imports to avoid triggering module loading.
     """
     # Check for nested attributes (only one level deep)
     attrs_to_check = [x for x in attrs if not x.startswith('_')][:20]  # Limit number of attributes to check
@@ -1645,6 +1663,10 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
             if attr_from_class is not None and hasattr(attr_from_class, '__get__'):
                 continue  # Skip descriptors to avoid executing arbitrary code
 
+            # Skip lazy imports to avoid triggering module loading
+            if _is_lazy_import(obj, attr_name):
+                continue
+
             # Safe to get the attribute since it's not a descriptor
             attr_obj = getattr(obj, attr_name)
 
@@ -1675,6 +1697,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
             except TypeError:  # Attributes are unsortable, e.g. int and str
                 d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
             d = sorted([x for x in d if isinstance(x, str)])
+            # Filter out lazy imports to avoid triggering module loading
+            d = [x for x in d if not _is_lazy_import(obj, x)]
             hide_underscored = (wrong_name[:1] != '_')
             if hide_underscored and tb is not None:
                 while tb.tb_next is not None:
@@ -1694,6 +1718,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
             except TypeError:  # Attributes are unsortable, e.g. int and str
                 d = list(mod.__dict__.keys())
             d = sorted([x for x in d if isinstance(x, str)])
+            # Filter out lazy imports to avoid triggering module loading
+            d = [x for x in d if not _is_lazy_import(mod, x)]
             if wrong_name[:1] != '_':
                 d = [x for x in d if x[:1] != '_']
         except Exception:
index 99f23c3f44270f5fada410f042320fa34ea271e3..b4f9a5c5140860bef8530b51634e71b179d84f82 100644 (file)
@@ -76,6 +76,9 @@ except ImportError:
     # CapsuleType cannot be accessed from pure Python,
     # so there is no fallback definition.
 
+    # LazyImportType in pure Python cannot be guaranteed
+    # without overriding the filter, so there is no fallback.
+
     del sys, _f, _g, _C, _c, _ag, _cell_factory  # Not for export
 
 
index d27e3301666868031ef1d1b62747de6f60de4c7b..f4119abf324fca4dcc5ed83ac42e2773caab79ef 100644 (file)
@@ -539,6 +539,7 @@ OBJECT_OBJS=        \
                Objects/funcobject.o \
                Objects/interpolationobject.o \
                Objects/iterobject.o \
+               Objects/lazyimportobject.o \
                Objects/listobject.o \
                Objects/longobject.o \
                Objects/dictobject.o \
@@ -1372,6 +1373,7 @@ PYTHON_HEADERS= \
                $(srcdir)/Include/internal/pycore_interpolation.h \
                $(srcdir)/Include/internal/pycore_intrinsics.h \
                $(srcdir)/Include/internal/pycore_jit.h \
+               $(srcdir)/Include/internal/pycore_lazyimportobject.h \
                $(srcdir)/Include/internal/pycore_list.h \
                $(srcdir)/Include/internal/pycore_llist.h \
                $(srcdir)/Include/internal/pycore_lock.h \
@@ -2676,6 +2678,8 @@ TESTSUBDIRS=      idlelib/idle_test \
                test/test_import/data/package3 \
                test/test_import/data/package4 \
                test/test_import/data/unwritable \
+               test/test_import/data/lazy_imports \
+               test/test_import/data/lazy_imports/pkg \
                test/test_importlib \
                test/test_importlib/builtin \
                test/test_importlib/extension \
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-06-15-46-32.gh-issue-142349.IdTuYL.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-06-15-46-32.gh-issue-142349.IdTuYL.rst
new file mode 100644 (file)
index 0000000..73cc167
--- /dev/null
@@ -0,0 +1 @@
+Implement :pep:`810`. Patch by Pablo Galindo and Dino Viehland.
index 27d37498f3cd836cac4aab76d3a1c195228444a6..ebb1032fdd1c3206054084053fc297aa30fd8dd1 100644 (file)
@@ -30,9 +30,75 @@ pyimport_importmoduleattrstring(PyObject *self, PyObject *args)
 }
 
 
+static PyObject *
+pyimport_setlazyimportsmode(PyObject *self, PyObject *args)
+{
+    PyObject *mode;
+    if (!PyArg_ParseTuple(args, "U", &mode)) {
+        return NULL;
+    }
+    if (strcmp(PyUnicode_AsUTF8(mode), "normal") == 0) {
+        PyImport_SetLazyImportsMode(PyImport_LAZY_NORMAL);
+    } else if (strcmp(PyUnicode_AsUTF8(mode), "all") == 0) {
+        PyImport_SetLazyImportsMode(PyImport_LAZY_ALL);
+    } else if (strcmp(PyUnicode_AsUTF8(mode), "none") == 0) {
+        PyImport_SetLazyImportsMode(PyImport_LAZY_NONE);
+    } else {
+        PyErr_SetString(PyExc_ValueError, "invalid mode");
+        return NULL;
+    }
+
+    Py_RETURN_NONE;
+}
+
+static PyObject *
+pyimport_getlazyimportsmode(PyObject *self, PyObject *args)
+{
+    switch (PyImport_GetLazyImportsMode()) {
+        case PyImport_LAZY_NORMAL:
+            return PyUnicode_FromString("normal");
+        case PyImport_LAZY_ALL:
+            return PyUnicode_FromString("all");
+        case PyImport_LAZY_NONE:
+            return PyUnicode_FromString("none");
+        default:
+            PyErr_SetString(PyExc_ValueError, "unknown mode");
+            return NULL;
+    }
+}
+
+static PyObject *
+pyimport_setlazyimportsfilter(PyObject *self, PyObject *args)
+{
+    PyObject *filter;
+    if (!PyArg_ParseTuple(args, "O", &filter)) {
+        return NULL;
+    }
+
+    if (PyImport_SetLazyImportsFilter(filter) < 0) {
+        return NULL;
+    }
+
+    Py_RETURN_NONE;
+}
+
+static PyObject *
+pyimport_getlazyimportsfilter(PyObject *self, PyObject *args)
+{
+    PyObject *res = PyImport_GetLazyImportsFilter();
+    if (res == NULL) {
+        Py_RETURN_NONE;
+    }
+    return res;
+}
+
 static PyMethodDef test_methods[] = {
     {"PyImport_ImportModuleAttr", pyimport_importmoduleattr, METH_VARARGS},
     {"PyImport_ImportModuleAttrString", pyimport_importmoduleattrstring, METH_VARARGS},
+    {"PyImport_SetLazyImportsMode", pyimport_setlazyimportsmode, METH_VARARGS},
+    {"PyImport_GetLazyImportsMode", pyimport_getlazyimportsmode, METH_NOARGS},
+    {"PyImport_SetLazyImportsFilter", pyimport_setlazyimportsfilter, METH_VARARGS},
+    {"PyImport_GetLazyImportsFilter", pyimport_getlazyimportsfilter, METH_NOARGS},
     {NULL},
 };
 
@@ -41,4 +107,3 @@ _PyTestCapi_Init_Import(PyObject *m)
 {
     return PyModule_AddFunctions(m, test_methods);
 }
-
index 35b1bc4e17c5d509566fb477125ed86b6fdf3af4..ddd8fcdc231bf12e78327fc529e2543b5fdf9e99 100644 (file)
             _PyStackRef res;
             from = stack_pointer[-1];
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
-            _PyFrame_SetStackPointer(frame, stack_pointer);
-            PyObject *res_o = _PyEval_ImportFrom(tstate, PyStackRef_AsPyObjectBorrow(from), name);
-            stack_pointer = _PyFrame_GetStackPointer(frame);
+            PyObject *res_o;
+            if (PyLazyImport_CheckExact(PyStackRef_AsPyObjectBorrow(from))) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_LazyImportFrom(
+                    tstate, frame, PyStackRef_AsPyObjectBorrow(from), name);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
+            else {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_ImportFrom(
+                    tstate, PyStackRef_AsPyObjectBorrow(from), name);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             if (res_o == NULL) {
                 JUMP_TO_LABEL(error);
             }
             _PyStackRef res;
             fromlist = stack_pointer[-1];
             level = stack_pointer[-2];
-            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
+            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2);
+            PyObject *res_o;
+            if (!(oparg & 0x02)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_LazyImportName(tstate, BUILTINS(), GLOBALS(),
+                    LOCALS(), name,
+                    PyStackRef_AsPyObjectBorrow(fromlist),
+                    PyStackRef_AsPyObjectBorrow(level),
+                    oparg & 0x01);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
+            else {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_ImportName(tstate, BUILTINS(), GLOBALS(),
+                    LOCALS(), name,
+                    PyStackRef_AsPyObjectBorrow(fromlist),
+                    PyStackRef_AsPyObjectBorrow(level));
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            PyObject *res_o = _PyEval_ImportName(tstate, frame, name,
-                PyStackRef_AsPyObjectBorrow(fromlist),
-                PyStackRef_AsPyObjectBorrow(level));
             _PyStackRef tmp = fromlist;
             fromlist = PyStackRef_NULL;
             stack_pointer[-1] = fromlist;
                         }
                         JUMP_TO_LABEL(error);
                     }
+                    if (PyLazyImport_CheckExact(v_o)) {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                        Py_SETREF(v_o, l_v);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                        if (v_o == NULL) {
+                            JUMP_TO_LABEL(error);
+                        }
+                    }
                 }
                 else {
                     _PyFrame_SetStackPointer(frame, stack_pointer);
                             JUMP_TO_LABEL(error);
                         }
                     }
+                    if (PyLazyImport_CheckExact(v_o)) {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                        Py_SETREF(v_o, l_v);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                        if (v_o == NULL) {
+                            JUMP_TO_LABEL(error);
+                        }
+                    }
                 }
             }
             v = PyStackRef_FromPyObjectSteal(v_o);
             if (v_o == NULL) {
                 JUMP_TO_LABEL(error);
             }
+            if (PyLazyImport_CheckExact(v_o)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                if (l_v == NULL) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    Py_DECREF(v_o);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    JUMP_TO_LABEL(error);
+                }
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                int err = PyDict_SetItem(GLOBALS(), name, l_v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                if (err < 0) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    Py_DECREF(v_o);
+                    Py_DECREF(l_v);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    JUMP_TO_LABEL(error);
+                }
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                Py_SETREF(v_o, l_v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             v = PyStackRef_FromPyObjectSteal(v_o);
             stack_pointer[0] = v;
             stack_pointer += 1;
index df6b4c93cb87a66d31a9b7227575ac1ab65f39e8..6c9e7a0a3ba053a008d8e62fc947e2f692b2ecd7 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "Python.h"
 #include "pycore_descrobject.h"   // _PyMethodWrapper_Type
+#include "pycore_lazyimportobject.h" // PyLazyImport_Type
 #include "pycore_namespace.h"     // _PyNamespace_Type
 #include "pycore_object.h"        // _PyNone_Type, _PyNotImplemented_Type
 #include "pycore_unionobject.h"   // _PyUnion_Type
@@ -35,6 +36,7 @@ _types_exec(PyObject *m)
     EXPORT_STATIC_TYPE("GetSetDescriptorType", PyGetSetDescr_Type);
     // LambdaType is the same as FunctionType
     EXPORT_STATIC_TYPE("LambdaType", PyFunction_Type);
+    EXPORT_STATIC_TYPE("LazyImportType", PyLazyImport_Type);
     EXPORT_STATIC_TYPE("MappingProxyType", PyDictProxy_Type);
     EXPORT_STATIC_TYPE("MemberDescriptorType", PyMemberDescr_Type);
     EXPORT_STATIC_TYPE("MethodDescriptorType", PyMethodDescr_Type);
index c1584be3f0ed4a38bac83d84d718a7941af7240b..ae7bf61767dc3bab6e1024dc24b55580c606fc34 100644 (file)
@@ -4693,6 +4693,14 @@ _PyDict_SizeOf_LockHeld(PyDictObject *mp)
     return (Py_ssize_t)res;
 }
 
+void
+_PyDict_ClearKeysVersionLockHeld(PyObject *mp)
+{
+    ASSERT_DICT_LOCKED(mp);
+
+    FT_ATOMIC_STORE_UINT32_RELAXED(((PyDictObject *)mp)->ma_keys->dk_version, 0);
+}
+
 Py_ssize_t
 _PyDict_SizeOf(PyDictObject *mp)
 {
@@ -7655,8 +7663,8 @@ PyDict_AddWatcher(PyDict_WatchCallback callback)
 {
     PyInterpreterState *interp = _PyInterpreterState_GET();
 
-    /* Start at 2, as 0 and 1 are reserved for CPython */
-    for (int i = 2; i < DICT_MAX_WATCHERS; i++) {
+    /* Some watchers are reserved for CPython, start at the first available one */
+    for (int i = FIRST_AVAILABLE_WATCHER; i < DICT_MAX_WATCHERS; i++) {
         if (!interp->dict_state.watchers[i]) {
             interp->dict_state.watchers[i] = callback;
             return i;
index ca6f323ac11fbcc24646e6cd370d1f6253980fdb..499fb2b34b34a894536ff72cdd284630c70a7b51 100644 (file)
@@ -2014,6 +2014,12 @@ static PyTypeObject _PyExc_ImportError = {
 };
 PyObject *PyExc_ImportError = (PyObject *)&_PyExc_ImportError;
 
+/*
+ *    ImportCycleError extends ImportError
+ */
+
+MiddlingExtendsException(PyExc_ImportError, ImportCycleError, ImportError,
+                         "Import produces a cycle.");
 /*
  *    ModuleNotFoundError extends ImportError
  */
@@ -4454,6 +4460,7 @@ static struct static_exception static_exceptions[] = {
     {&_PyExc_IncompleteInputError, "_IncompleteInputError"}, // base: SyntaxError(Exception)
     ITEM(IndexError),  // base: LookupError(Exception)
     ITEM(KeyError),  // base: LookupError(Exception)
+    ITEM(ImportCycleError), // base: ImportError(Exception)
     ITEM(ModuleNotFoundError), // base: ImportError(Exception)
     ITEM(NotImplementedError),  // base: RuntimeError(Exception)
     ITEM(PythonFinalizationError),  // base: RuntimeError(Exception)
@@ -4643,4 +4650,3 @@ _PyException_AddNote(PyObject *exc, PyObject *note)
     Py_XDECREF(r);
     return res;
 }
-
diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c
new file mode 100644 (file)
index 0000000..451f335
--- /dev/null
@@ -0,0 +1,157 @@
+// Lazy object implementation.
+
+#include "Python.h"
+#include "pycore_ceval.h"
+#include "pycore_frame.h"
+#include "pycore_import.h"
+#include "pycore_interpframe.h"
+#include "pycore_lazyimportobject.h"
+#include "pycore_modsupport.h"
+
+#define PyLazyImportObject_CAST(op) ((PyLazyImportObject *)(op))
+
+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");
+        return NULL;
+    }
+    if (fromlist == Py_None || fromlist == NULL) {
+        fromlist = NULL;
+    }
+    else if (!PyUnicode_Check(fromlist) && !PyTuple_Check(fromlist)) {
+        PyErr_SetString(PyExc_TypeError,
+            "lazy_import: fromlist must be None, a string, or a tuple");
+        return NULL;
+    }
+    m = PyObject_GC_New(PyLazyImportObject, &PyLazyImport_Type);
+    if (m == NULL) {
+        return NULL;
+    }
+    m->lz_builtins = Py_XNewRef(builtins);
+    m->lz_from = Py_NewRef(name);
+    m->lz_attr = Py_XNewRef(fromlist);
+
+    // Capture frame information for the original import location.
+    m->lz_code = NULL;
+    m->lz_instr_offset = -1;
+
+    if (frame != NULL) {
+        PyCodeObject *code = _PyFrame_GetCode(frame);
+        if (code != NULL) {
+            m->lz_code = (PyCodeObject *)Py_NewRef(code);
+            // Calculate the instruction offset from the current frame.
+            m->lz_instr_offset = _PyInterpreterFrame_LASTI(frame);
+        }
+    }
+
+    _PyObject_GC_TRACK(m);
+    return (PyObject *)m;
+}
+
+static int
+lazy_import_traverse(PyObject *op, visitproc visit, void *arg)
+{
+    PyLazyImportObject *m = PyLazyImportObject_CAST(op);
+    Py_VISIT(m->lz_builtins);
+    Py_VISIT(m->lz_from);
+    Py_VISIT(m->lz_attr);
+    Py_VISIT(m->lz_code);
+    return 0;
+}
+
+static int
+lazy_import_clear(PyObject *op)
+{
+    PyLazyImportObject *m = PyLazyImportObject_CAST(op);
+    Py_CLEAR(m->lz_builtins);
+    Py_CLEAR(m->lz_from);
+    Py_CLEAR(m->lz_attr);
+    Py_CLEAR(m->lz_code);
+    return 0;
+}
+
+static void
+lazy_import_dealloc(PyObject *op)
+{
+    _PyObject_GC_UNTRACK(op);
+    (void)lazy_import_clear(op);
+    Py_TYPE(op)->tp_free(op);
+}
+
+static PyObject *
+lazy_import_name(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);
+        }
+    }
+    return Py_NewRef(m->lz_from);
+}
+
+static PyObject *
+lazy_import_repr(PyObject *op)
+{
+    PyLazyImportObject *m = PyLazyImportObject_CAST(op);
+    PyObject *name = lazy_import_name(m);
+    if (name == NULL) {
+        return NULL;
+    }
+    PyObject *res = PyUnicode_FromFormat("<%T '%U'>", op, name);
+    Py_DECREF(name);
+    return res;
+}
+
+PyObject *
+_PyLazyImport_GetName(PyObject *op)
+{
+    PyLazyImportObject *lazy_import = PyLazyImportObject_CAST(op);
+    assert(PyLazyImport_CheckExact(lazy_import));
+    return lazy_import_name(lazy_import);
+}
+
+static PyObject *
+lazy_import_resolve(PyObject *self, PyObject *args)
+{
+    return _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self);
+}
+
+static PyMethodDef lazy_import_methods[] = {
+    {
+        "resolve", lazy_import_resolve, METH_NOARGS,
+        PyDoc_STR("resolves the lazy import and returns the actual object")
+    },
+    {NULL, NULL}
+};
+
+
+PyDoc_STRVAR(lazy_import_doc,
+"lazy_import(builtins, name, fromlist=None, /)\n"
+"--\n"
+"\n"
+"Represents a deferred import that will be resolved on first use.\n"
+"\n"
+"Instances of this object accessed from the global scope will be\n"
+"automatically imported based upon their name and then replaced with\n"
+"the imported value.");
+
+PyTypeObject PyLazyImport_Type = {
+    PyVarObject_HEAD_INIT(&PyType_Type, 0)
+    .tp_name = "lazy_import",
+    .tp_basicsize = sizeof(PyLazyImportObject),
+    .tp_dealloc = lazy_import_dealloc,
+    .tp_repr = lazy_import_repr,
+    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
+    .tp_doc = lazy_import_doc,
+    .tp_traverse = lazy_import_traverse,
+    .tp_clear = lazy_import_clear,
+    .tp_methods = lazy_import_methods,
+    .tp_alloc = PyType_GenericAlloc,
+    .tp_free = PyObject_GC_Del,
+};
index b72700770281fbb6f22c0790f8bc247995e0b171..e3868097c0ba9f9d9c2ee52b9fe69ba9d84e6131 100644 (file)
@@ -7,6 +7,7 @@
 #include "pycore_fileutils.h"     // _Py_wgetcwd
 #include "pycore_import.h"        // _PyImport_GetNextModuleIndex()
 #include "pycore_interp.h"        // PyInterpreterState.importlib
+#include "pycore_lazyimportobject.h" // _PyLazyImportObject_Check()
 #include "pycore_long.h"          // _PyLong_GetOne()
 #include "pycore_modsupport.h"    // _PyModule_CreateInitialized()
 #include "pycore_moduleobject.h"  // _PyModule_GetDefOrNull()
@@ -18,7 +19,6 @@
 
 #include "osdefs.h"               // MAXPATHLEN
 
-
 #define _PyModule_CAST(op) \
     (assert(PyModule_Check(op)), _Py_CAST(PyModuleObject*, (op)))
 
@@ -195,11 +195,40 @@ new_module_notrack(PyTypeObject *mt)
     return m;
 }
 
+/* Module dict watcher callback.
+ * When a module dictionary is modified, we need to clear the keys version
+ * to invalidate any cached lookups that depend on the dictionary structure.
+ */
+static int
+module_dict_watcher(PyDict_WatchEvent event, PyObject *dict,
+                    PyObject *key, PyObject *new_value)
+{
+    assert(PyDict_Check(dict));
+    // Only if a new lazy object shows up do we need to clear the dictionary. If
+    // this is adding a new key then the version will be reset anyway.
+    if (event == PyDict_EVENT_MODIFIED &&
+        new_value != NULL &&
+        PyLazyImport_CheckExact(new_value)) {
+        _PyDict_ClearKeysVersionLockHeld(dict);
+    }
+    return 0;
+}
+
+int
+_PyModule_InitModuleDictWatcher(PyInterpreterState *interp)
+{
+    // This is a reserved watcher for CPython so there's no need to check for non-NULL.
+    assert(interp->dict_state.watchers[MODULE_WATCHER_ID] == NULL);
+    interp->dict_state.watchers[MODULE_WATCHER_ID] = &module_dict_watcher;
+    return 0;
+}
+
 static void
 track_module(PyModuleObject *m)
 {
     _PyDict_EnablePerThreadRefcounting(m->md_dict);
     _PyObject_SetDeferredRefcount((PyObject *)m);
+    PyDict_Watch(MODULE_WATCHER_ID, m->md_dict);
     PyObject_GC_Track(m);
 }
 
@@ -1308,6 +1337,28 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
     PyObject *attr, *mod_name, *getattr;
     attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress);
     if (attr) {
+        if (PyLazyImport_CheckExact(attr)) {
+            PyObject *new_value = _PyImport_LoadLazyImportTstate(
+                PyThreadState_GET(), attr);
+            if (new_value == NULL) {
+                if (suppress &&
+                    PyErr_ExceptionMatches(PyExc_ImportCycleError)) {
+                    // ImportCycleError is raised when a lazy object tries
+                    // to import itself. In this case, the error should not
+                    // propagate to the caller and instead treated as if the
+                    // attribute doesn't exist.
+                    PyErr_Clear();
+                }
+                Py_DECREF(attr);
+                return NULL;
+            }
+
+            if (PyDict_SetItem(m->md_dict, name, new_value) < 0) {
+                Py_CLEAR(new_value);
+            }
+            Py_DECREF(attr);
+            return new_value;
+        }
         return attr;
     }
     if (suppress == 1) {
@@ -1506,7 +1557,12 @@ static PyObject *
 module_dir(PyObject *self, PyObject *args)
 {
     PyObject *result = NULL;
-    PyObject *dict = PyObject_GetAttr(self, &_Py_ID(__dict__));
+    PyObject *dict;
+    if (PyModule_CheckExact(self)) {
+        dict = Py_NewRef(((PyModuleObject *)self)->md_dict);
+    } else {
+        dict = PyObject_GetAttr(self, &_Py_ID(__dict__));
+    }
 
     if (dict != NULL) {
         if (PyDict_Check(dict)) {
@@ -1534,7 +1590,7 @@ static PyMethodDef module_methods[] = {
 };
 
 static PyObject *
-module_get_dict(PyModuleObject *m)
+module_load_dict(PyModuleObject *m)
 {
     PyObject *dict = PyObject_GetAttr((PyObject *)m, &_Py_ID(__dict__));
     if (dict == NULL) {
@@ -1553,7 +1609,7 @@ module_get_annotate(PyObject *self, void *Py_UNUSED(ignored))
 {
     PyModuleObject *m = _PyModule_CAST(self);
 
-    PyObject *dict = module_get_dict(m);
+    PyObject *dict = module_load_dict(m);
     if (dict == NULL) {
         return NULL;
     }
@@ -1578,7 +1634,7 @@ module_set_annotate(PyObject *self, PyObject *value, void *Py_UNUSED(ignored))
         return -1;
     }
 
-    PyObject *dict = module_get_dict(m);
+    PyObject *dict = module_load_dict(m);
     if (dict == NULL) {
         return -1;
     }
@@ -1608,7 +1664,7 @@ module_get_annotations(PyObject *self, void *Py_UNUSED(ignored))
 {
     PyModuleObject *m = _PyModule_CAST(self);
 
-    PyObject *dict = module_get_dict(m);
+    PyObject *dict = module_load_dict(m);
     if (dict == NULL) {
         return NULL;
     }
@@ -1680,7 +1736,7 @@ module_set_annotations(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)
 {
     PyModuleObject *m = _PyModule_CAST(self);
 
-    PyObject *dict = module_get_dict(m);
+    PyObject *dict = module_load_dict(m);
     if (dict == NULL) {
         return -1;
     }
@@ -1709,7 +1765,6 @@ module_set_annotations(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)
     return ret;
 }
 
-
 static PyGetSetDef module_getsets[] = {
     {"__annotations__", module_get_annotations, module_set_annotations},
     {"__annotate__", module_get_annotate, module_set_annotate},
index 605861ad3fd06c10b791653d3273c5a4005c23a6..cb806459596084b089782c2faf0725f1991c4a9f 100644 (file)
     <ClCompile Include="..\Objects\genobject.c" />
     <ClCompile Include="..\Objects\interpolationobject.c" />
     <ClCompile Include="..\Objects\iterobject.c" />
+    <ClCompile Include="..\Objects\lazyimportobject.c" />
     <ClCompile Include="..\Objects\listobject.c" />
     <ClCompile Include="..\Objects\longobject.c" />
     <ClCompile Include="..\Objects\memoryobject.c" />
index c67fe53363ee847daf686cd36167714e864fbced..6dcf0e8712903a83195d76eaa3a5a01d7cc564df 100644 (file)
     <ClCompile Include="..\Python\jit.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Objects\lazyimportobject.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\listobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
index 5e7e739dc5f787128e18ae2b4490b653c79a5bb3..5e37dd33c849f2c77daadc1dfd67e0a7f444c8bf 100644 (file)
     <ClInclude Include="..\Include\internal\pycore_interpolation.h" />
     <ClInclude Include="..\Include\internal\pycore_intrinsics.h" />
     <ClInclude Include="..\Include\internal\pycore_jit.h" />
+    <ClInclude Include="..\Include\internal\pycore_lazyimportobject.h" />
     <ClInclude Include="..\Include\internal\pycore_list.h" />
     <ClInclude Include="..\Include\internal\pycore_llist.h" />
     <ClInclude Include="..\Include\internal\pycore_lock.h" />
     <ClCompile Include="..\Objects\longobject.c" />
     <ClCompile Include="..\Objects\memoryobject.c" />
     <ClCompile Include="..\Objects\methodobject.c" />
+    <ClCompile Include="..\Objects\lazyimportobject.c" />
     <ClCompile Include="..\Objects\moduleobject.c" />
     <ClCompile Include="..\Objects\namespaceobject.c" />
     <ClCompile Include="..\Objects\object.c" />
index 247f4b5a784f9c630c467e55a8ae316b2a579979..664788e69af19a362f466d3400f25e10a091f3e8 100644 (file)
     <ClInclude Include="..\Include\internal\pycore_long.h">
       <Filter>Include\internal</Filter>
     </ClInclude>
+    <ClInclude Include="..\Include\internal\pycore_lazyimportobject.h">
+      <Filter>Include\internal</Filter>
+    </ClInclude>
     <ClInclude Include="..\Include\internal\pycore_mmap.h">
       <Filter>Include\internal</Filter>
     </ClInclude>
     <ClCompile Include="..\Objects\methodobject.c">
       <Filter>Objects</Filter>
     </ClCompile>
+    <ClCompile Include="..\Objects\lazyimportobject.c">
+      <Filter>Objects</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\moduleobject.c">
       <Filter>Objects</Filter>
     </ClCompile>
index dbe226f837243ca36c73c7ef3a77db0d2b8fc190..2f0b123858f8d18928322f4e62b9eebffc662940 100644 (file)
@@ -45,8 +45,8 @@ module Python
           | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody)
           | Assert(expr test, expr? msg)
 
-          | Import(alias* names)
-          | ImportFrom(identifier? module, alias* names, int? level)
+          | Import(alias* names, int? is_lazy)
+          | ImportFrom(identifier? module, alias* names, int? level, int? is_lazy)
 
           | Global(identifier* names)
           | Nonlocal(identifier* names)
index 50856686335a147b072b376253db9e6749d4ea15..1f5b6220ba1baa5999a361d90e99ae944a69088b 100644 (file)
@@ -1971,10 +1971,16 @@ _PyPegen_concatenate_strings(Parser *p, asdl_expr_seq *strings,
 }
 
 stmt_ty
-_PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * names, int level,
-                                          int lineno, int col_offset, int end_lineno, int end_col_offset,
-                                  PyArena *arena) {
+_PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * names,
+                               int level, expr_ty lazy_token, int lineno,
+                               int col_offset, int end_lineno, int end_col_offset,
+                               PyArena *arena) {
     if (level == 0 && PyUnicode_CompareWithASCIIString(module, "__future__") == 0) {
+        if (lazy_token) {
+            RAISE_SYNTAX_ERROR_KNOWN_LOCATION(lazy_token,
+                "lazy from __future__ import is not allowed");
+            return NULL;
+        }
         for (Py_ssize_t i = 0; i < asdl_seq_LEN(names); i++) {
             alias_ty alias = asdl_seq_GET(names, i);
             if (PyUnicode_CompareWithASCIIString(alias->name, "barry_as_FLUFL") == 0) {
@@ -1982,7 +1988,8 @@ _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * na
             }
         }
     }
-    return _PyAST_ImportFrom(module, names, level, lineno, col_offset, end_lineno, end_col_offset, arena);
+    return _PyAST_ImportFrom(module, names, level, lazy_token ? 1 : 0, lineno,
+                             col_offset, end_lineno, end_col_offset, arena);
 }
 
 asdl_stmt_seq*
index b9848a865b687555f8a5da1f0a8f20ec2ac2911d..37c19c4c9020c8bd061b025487a43d5059ae699d 100644 (file)
@@ -60,8 +60,8 @@ static KeywordToken *reserved_keywords[] = {
         {NULL, -1},
     },
     (KeywordToken[]) {
-        {"return", 522},
         {"import", 647},
+        {"return", 522},
         {"assert", 638},
         {"global", 530},
         {"except", 690},
@@ -81,6 +81,7 @@ static KeywordToken *reserved_keywords[] = {
 static char *soft_keywords[] = {
     "_",
     "case",
+    "lazy",
     "match",
     "type",
     NULL,
@@ -1565,9 +1566,9 @@ simple_stmts_rule(Parser *p)
 // simple_stmt:
 //     | assignment
 //     | &"type" type_alias
+//     | &('import' | 'from' | "lazy") import_stmt
 //     | star_expressions
 //     | &'return' return_stmt
-//     | &('import' | 'from') import_stmt
 //     | &'raise' raise_stmt
 //     | &'pass' pass_stmt
 //     | &'del' del_stmt
@@ -1642,6 +1643,27 @@ simple_stmt_rule(Parser *p)
         D(fprintf(stderr, "%*c%s simple_stmt[%d-%d]: %s failed!\n", p->level, ' ',
                   p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "&\"type\" type_alias"));
     }
+    { // &('import' | 'from' | "lazy") import_stmt
+        if (p->error_indicator) {
+            p->level--;
+            return NULL;
+        }
+        D(fprintf(stderr, "%*c> simple_stmt[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "&('import' | 'from' | \"lazy\") import_stmt"));
+        stmt_ty import_stmt_var;
+        if (
+            _PyPegen_lookahead(1, _tmp_5_rule, p)
+            &&
+            (import_stmt_var = import_stmt_rule(p))  // import_stmt
+        )
+        {
+            D(fprintf(stderr, "%*c+ simple_stmt[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "&('import' | 'from' | \"lazy\") import_stmt"));
+            _res = import_stmt_var;
+            goto done;
+        }
+        p->mark = _mark;
+        D(fprintf(stderr, "%*c%s simple_stmt[%d-%d]: %s failed!\n", p->level, ' ',
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "&('import' | 'from' | \"lazy\") import_stmt"));
+    }
     { // star_expressions
         if (p->error_indicator) {
             p->level--;
@@ -1696,27 +1718,6 @@ simple_stmt_rule(Parser *p)
         D(fprintf(stderr, "%*c%s simple_stmt[%d-%d]: %s failed!\n", p->level, ' ',
                   p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "&'return' return_stmt"));
     }
-    { // &('import' | 'from') import_stmt
-        if (p->error_indicator) {
-            p->level--;
-            return NULL;
-        }
-        D(fprintf(stderr, "%*c> simple_stmt[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "&('import' | 'from') import_stmt"));
-        stmt_ty import_stmt_var;
-        if (
-            _PyPegen_lookahead(1, _tmp_5_rule, p)
-            &&
-            (import_stmt_var = import_stmt_rule(p))  // import_stmt
-        )
-        {
-            D(fprintf(stderr, "%*c+ simple_stmt[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "&('import' | 'from') import_stmt"));
-            _res = import_stmt_var;
-            goto done;
-        }
-        p->mark = _mark;
-        D(fprintf(stderr, "%*c%s simple_stmt[%d-%d]: %s failed!\n", p->level, ' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "&('import' | 'from') import_stmt"));
-    }
     { // &'raise' raise_stmt
         if (p->error_indicator) {
             p->level--;
@@ -3511,6 +3512,10 @@ import_stmt_rule(Parser *p)
         return NULL;
     }
     stmt_ty _res = NULL;
+    if (_PyPegen_is_memoized(p, import_stmt_type, &_res)) {
+        p->level--;
+        return _res;
+    }
     int _mark = p->mark;
     if (p->call_invalid_rules) { // invalid_import
         if (p->error_indicator) {
@@ -3571,11 +3576,12 @@ import_stmt_rule(Parser *p)
     }
     _res = NULL;
   done:
+    _PyPegen_insert_memo(p, _mark, import_stmt_type, _res);
     p->level--;
     return _res;
 }
 
-// import_name: 'import' dotted_as_names
+// import_name: "lazy"? 'import' dotted_as_names
 static stmt_ty
 import_name_rule(Parser *p)
 {
@@ -3597,21 +3603,24 @@ import_name_rule(Parser *p)
     UNUSED(_start_lineno); // Only used by EXTRA macro
     int _start_col_offset = p->tokens[_mark]->col_offset;
     UNUSED(_start_col_offset); // Only used by EXTRA macro
-    { // 'import' dotted_as_names
+    { // "lazy"? 'import' dotted_as_names
         if (p->error_indicator) {
             p->level--;
             return NULL;
         }
-        D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "'import' dotted_as_names"));
+        D(fprintf(stderr, "%*c> import_name[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
         Token * _keyword;
         asdl_alias_seq* a;
+        void *lazy;
         if (
+            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"), !p->error_indicator)  // "lazy"?
+            &&
             (_keyword = _PyPegen_expect_token(p, 647))  // token='import'
             &&
             (a = dotted_as_names_rule(p))  // dotted_as_names
         )
         {
-            D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'import' dotted_as_names"));
+            D(fprintf(stderr, "%*c+ import_name[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
             Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
             if (_token == NULL) {
                 p->level--;
@@ -3621,7 +3630,7 @@ import_name_rule(Parser *p)
             UNUSED(_end_lineno); // Only used by EXTRA macro
             int _end_col_offset = _token->end_col_offset;
             UNUSED(_end_col_offset); // Only used by EXTRA macro
-            _res = _PyAST_Import ( a , EXTRA );
+            _res = _PyAST_Import ( a , lazy ? 1 : 0 , EXTRA );
             if (_res == NULL && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -3631,7 +3640,7 @@ import_name_rule(Parser *p)
         }
         p->mark = _mark;
         D(fprintf(stderr, "%*c%s import_name[%d-%d]: %s failed!\n", p->level, ' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'import' dotted_as_names"));
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "\"lazy\"? 'import' dotted_as_names"));
     }
     _res = NULL;
   done:
@@ -3640,8 +3649,8 @@ import_name_rule(Parser *p)
 }
 
 // import_from:
-//     | 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
-//     | 'from' (('.' | '...'))+ 'import' import_from_targets
+//     | "lazy"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
+//     | "lazy"? 'from' (('.' | '...'))+ 'import' import_from_targets
 static stmt_ty
 import_from_rule(Parser *p)
 {
@@ -3663,18 +3672,21 @@ import_from_rule(Parser *p)
     UNUSED(_start_lineno); // Only used by EXTRA macro
     int _start_col_offset = p->tokens[_mark]->col_offset;
     UNUSED(_start_col_offset); // Only used by EXTRA macro
-    { // 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
+    { // "lazy"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets
         if (p->error_indicator) {
             p->level--;
             return NULL;
         }
-        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
+        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
         Token * _keyword;
         Token * _keyword_1;
         asdl_seq * a;
         expr_ty b;
         asdl_alias_seq* c;
+        void *lazy;
         if (
+            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"), !p->error_indicator)  // "lazy"?
+            &&
             (_keyword = _PyPegen_expect_token(p, 646))  // token='from'
             &&
             (a = _loop0_17_rule(p))  // (('.' | '...'))*
@@ -3686,7 +3698,7 @@ import_from_rule(Parser *p)
             (c = import_from_targets_rule(p))  // import_from_targets
         )
         {
-            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
+            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
             Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
             if (_token == NULL) {
                 p->level--;
@@ -3696,7 +3708,7 @@ import_from_rule(Parser *p)
             UNUSED(_end_lineno); // Only used by EXTRA macro
             int _end_col_offset = _token->end_col_offset;
             UNUSED(_end_col_offset); // Only used by EXTRA macro
-            _res = _PyPegen_checked_future_import ( p , b -> v . Name . id , c , _PyPegen_seq_count_dots ( a ) , EXTRA );
+            _res = _PyPegen_checked_future_import ( p , b -> v . Name . id , c , _PyPegen_seq_count_dots ( a ) , lazy , EXTRA );
             if (_res == NULL && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -3706,19 +3718,22 @@ import_from_rule(Parser *p)
         }
         p->mark = _mark;
         D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, ' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))* dotted_name 'import' import_from_targets"));
     }
-    { // 'from' (('.' | '...'))+ 'import' import_from_targets
+    { // "lazy"? 'from' (('.' | '...'))+ 'import' import_from_targets
         if (p->error_indicator) {
             p->level--;
             return NULL;
         }
-        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))+ 'import' import_from_targets"));
+        D(fprintf(stderr, "%*c> import_from[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import' import_from_targets"));
         Token * _keyword;
         Token * _keyword_1;
         asdl_seq * a;
         asdl_alias_seq* b;
+        void *lazy;
         if (
+            (lazy = _PyPegen_expect_soft_keyword(p, "lazy"), !p->error_indicator)  // "lazy"?
+            &&
             (_keyword = _PyPegen_expect_token(p, 646))  // token='from'
             &&
             (a = _loop1_18_rule(p))  // (('.' | '...'))+
@@ -3728,7 +3743,7 @@ import_from_rule(Parser *p)
             (b = import_from_targets_rule(p))  // import_from_targets
         )
         {
-            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'from' (('.' | '...'))+ 'import' import_from_targets"));
+            D(fprintf(stderr, "%*c+ import_from[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import' import_from_targets"));
             Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
             if (_token == NULL) {
                 p->level--;
@@ -3738,7 +3753,7 @@ import_from_rule(Parser *p)
             UNUSED(_end_lineno); // Only used by EXTRA macro
             int _end_col_offset = _token->end_col_offset;
             UNUSED(_end_col_offset); // Only used by EXTRA macro
-            _res = _PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a ) , EXTRA );
+            _res = _PyAST_ImportFrom ( NULL , b , _PyPegen_seq_count_dots ( a ) , lazy ? 1 : 0 , EXTRA );
             if (_res == NULL && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -3748,7 +3763,7 @@ import_from_rule(Parser *p)
         }
         p->mark = _mark;
         D(fprintf(stderr, "%*c%s import_from[%d-%d]: %s failed!\n", p->level, ' ',
-                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from' (('.' | '...'))+ 'import' import_from_targets"));
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "\"lazy\"? 'from' (('.' | '...'))+ 'import' import_from_targets"));
     }
     _res = NULL;
   done:
@@ -28556,7 +28571,7 @@ _gather_4_rule(Parser *p)
     return _res;
 }
 
-// _tmp_5: 'import' | 'from'
+// _tmp_5: 'import' | 'from' | "lazy"
 static void *
 _tmp_5_rule(Parser *p)
 {
@@ -28607,6 +28622,25 @@ _tmp_5_rule(Parser *p)
         D(fprintf(stderr, "%*c%s _tmp_5[%d-%d]: %s failed!\n", p->level, ' ',
                   p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'from'"));
     }
+    { // "lazy"
+        if (p->error_indicator) {
+            p->level--;
+            return NULL;
+        }
+        D(fprintf(stderr, "%*c> _tmp_5[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "\"lazy\""));
+        expr_ty _keyword;
+        if (
+            (_keyword = _PyPegen_expect_soft_keyword(p, "lazy"))  // soft_keyword='"lazy"'
+        )
+        {
+            D(fprintf(stderr, "%*c+ _tmp_5[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "\"lazy\""));
+            _res = _keyword;
+            goto done;
+        }
+        p->mark = _mark;
+        D(fprintf(stderr, "%*c%s _tmp_5[%d-%d]: %s failed!\n", p->level, ' ',
+                  p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "\"lazy\""));
+    }
     _res = NULL;
   done:
     p->level--;
index be5333eb2684ae2d3aec630ca175da34204ac0a1..85c9ada765d9bd4e3b4fbea0aa547a5a83f355a4 100644 (file)
@@ -367,7 +367,7 @@ void *_PyPegen_arguments_parsing_error(Parser *, expr_ty);
 expr_ty _PyPegen_get_last_comprehension_item(comprehension_ty comprehension);
 void *_PyPegen_nonparen_genexp_in_call(Parser *p, expr_ty args, asdl_comprehension_seq *comprehensions);
 stmt_ty _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq *,
-                                       int , int, int , int , int , PyArena *);
+                                       int, expr_ty, int, int, int, int, PyArena *);
 asdl_stmt_seq* _PyPegen_register_stmts(Parser *p, asdl_stmt_seq* stmts);
 stmt_ty _PyPegen_register_stmt(Parser *p, stmt_ty s);
 
index dbeedb7ffe0ce6249116383947cba6c1bad79bc0..f808544045e1539d9f060c547487f5857d9b9755 100644 (file)
@@ -2,7 +2,7 @@
 unsigned char M_test_frozenmain[] = {
     227,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,
     0,0,0,0,0,243,184,0,0,0,128,0,94,0,82,1,
-    73,0,116,0,94,0,82,1,73,1,116,1,93,2,33,0,
+    73,0,116,0,94,0,82,1,73,4,116,1,93,2,33,0,
     82,2,52,1,0,0,0,0,0,0,31,0,93,2,33,0,
     82,3,93,0,80,6,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,52,2,0,0,0,0,0,0,
index 577adb825b070d71776da25784617388fe56f7ec..5d319992dcda1e9fca5ac6b9da421c16fcc47d5f 100644 (file)
@@ -222,6 +222,7 @@ void _PyAST_Fini(PyInterpreterState *interp)
     Py_CLEAR(state->id);
     Py_CLEAR(state->ifs);
     Py_CLEAR(state->is_async);
+    Py_CLEAR(state->is_lazy);
     Py_CLEAR(state->items);
     Py_CLEAR(state->iter);
     Py_CLEAR(state->key);
@@ -327,6 +328,7 @@ static int init_identifiers(struct ast_state *state)
     if ((state->id = PyUnicode_InternFromString("id")) == NULL) return -1;
     if ((state->ifs = PyUnicode_InternFromString("ifs")) == NULL) return -1;
     if ((state->is_async = PyUnicode_InternFromString("is_async")) == NULL) return -1;
+    if ((state->is_lazy = PyUnicode_InternFromString("is_lazy")) == NULL) return -1;
     if ((state->items = PyUnicode_InternFromString("items")) == NULL) return -1;
     if ((state->iter = PyUnicode_InternFromString("iter")) == NULL) return -1;
     if ((state->key = PyUnicode_InternFromString("key")) == NULL) return -1;
@@ -527,11 +529,13 @@ static const char * const Assert_fields[]={
 };
 static const char * const Import_fields[]={
     "names",
+    "is_lazy",
 };
 static const char * const ImportFrom_fields[]={
     "module",
     "names",
     "level",
+    "is_lazy",
 };
 static const char * const Global_fields[]={
     "names",
@@ -2254,6 +2258,21 @@ add_ast_annotations(struct ast_state *state)
             return 0;
         }
     }
+    {
+        PyObject *type = (PyObject *)&PyLong_Type;
+        type = _Py_union_type_or(type, Py_None);
+        cond = type != NULL;
+        if (!cond) {
+            Py_DECREF(Import_annotations);
+            return 0;
+        }
+        cond = PyDict_SetItemString(Import_annotations, "is_lazy", type) == 0;
+        Py_DECREF(type);
+        if (!cond) {
+            Py_DECREF(Import_annotations);
+            return 0;
+        }
+    }
     cond = PyObject_SetAttrString(state->Import_type, "_field_types",
                                   Import_annotations) == 0;
     if (!cond) {
@@ -2315,6 +2334,22 @@ add_ast_annotations(struct ast_state *state)
             return 0;
         }
     }
+    {
+        PyObject *type = (PyObject *)&PyLong_Type;
+        type = _Py_union_type_or(type, Py_None);
+        cond = type != NULL;
+        if (!cond) {
+            Py_DECREF(ImportFrom_annotations);
+            return 0;
+        }
+        cond = PyDict_SetItemString(ImportFrom_annotations, "is_lazy", type) ==
+                                    0;
+        Py_DECREF(type);
+        if (!cond) {
+            Py_DECREF(ImportFrom_annotations);
+            return 0;
+        }
+    }
     cond = PyObject_SetAttrString(state->ImportFrom_type, "_field_types",
                                   ImportFrom_annotations) == 0;
     if (!cond) {
@@ -6223,8 +6258,8 @@ init_types(void *arg)
         "     | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody)\n"
         "     | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody)\n"
         "     | Assert(expr test, expr? msg)\n"
-        "     | Import(alias* names)\n"
-        "     | ImportFrom(identifier? module, alias* names, int? level)\n"
+        "     | Import(alias* names, int? is_lazy)\n"
+        "     | ImportFrom(identifier? module, alias* names, int? level, int? is_lazy)\n"
         "     | Global(identifier* names)\n"
         "     | Nonlocal(identifier* names)\n"
         "     | Expr(expr value)\n"
@@ -6353,17 +6388,21 @@ init_types(void *arg)
     if (PyObject_SetAttr(state->Assert_type, state->msg, Py_None) == -1)
         return -1;
     state->Import_type = make_type(state, "Import", state->stmt_type,
-                                   Import_fields, 1,
-        "Import(alias* names)");
+                                   Import_fields, 2,
+        "Import(alias* names, int? is_lazy)");
     if (!state->Import_type) return -1;
+    if (PyObject_SetAttr(state->Import_type, state->is_lazy, Py_None) == -1)
+        return -1;
     state->ImportFrom_type = make_type(state, "ImportFrom", state->stmt_type,
-                                       ImportFrom_fields, 3,
-        "ImportFrom(identifier? module, alias* names, int? level)");
+                                       ImportFrom_fields, 4,
+        "ImportFrom(identifier? module, alias* names, int? level, int? is_lazy)");
     if (!state->ImportFrom_type) return -1;
     if (PyObject_SetAttr(state->ImportFrom_type, state->module, Py_None) == -1)
         return -1;
     if (PyObject_SetAttr(state->ImportFrom_type, state->level, Py_None) == -1)
         return -1;
+    if (PyObject_SetAttr(state->ImportFrom_type, state->is_lazy, Py_None) == -1)
+        return -1;
     state->Global_type = make_type(state, "Global", state->stmt_type,
                                    Global_fields, 1,
         "Global(identifier* names)");
@@ -7605,8 +7644,8 @@ _PyAST_Assert(expr_ty test, expr_ty msg, int lineno, int col_offset, int
 }
 
 stmt_ty
-_PyAST_Import(asdl_alias_seq * names, int lineno, int col_offset, int
-              end_lineno, int end_col_offset, PyArena *arena)
+_PyAST_Import(asdl_alias_seq * names, int is_lazy, int lineno, int col_offset,
+              int end_lineno, int end_col_offset, PyArena *arena)
 {
     stmt_ty p;
     p = (stmt_ty)_PyArena_Malloc(arena, sizeof(*p));
@@ -7614,6 +7653,7 @@ _PyAST_Import(asdl_alias_seq * names, int lineno, int col_offset, int
         return NULL;
     p->kind = Import_kind;
     p->v.Import.names = names;
+    p->v.Import.is_lazy = is_lazy;
     p->lineno = lineno;
     p->col_offset = col_offset;
     p->end_lineno = end_lineno;
@@ -7623,8 +7663,8 @@ _PyAST_Import(asdl_alias_seq * names, int lineno, int col_offset, int
 
 stmt_ty
 _PyAST_ImportFrom(identifier module, asdl_alias_seq * names, int level, int
-                  lineno, int col_offset, int end_lineno, int end_col_offset,
-                  PyArena *arena)
+                  is_lazy, int lineno, int col_offset, int end_lineno, int
+                  end_col_offset, PyArena *arena)
 {
     stmt_ty p;
     p = (stmt_ty)_PyArena_Malloc(arena, sizeof(*p));
@@ -7634,6 +7674,7 @@ _PyAST_ImportFrom(identifier module, asdl_alias_seq * names, int level, int
     p->v.ImportFrom.module = module;
     p->v.ImportFrom.names = names;
     p->v.ImportFrom.level = level;
+    p->v.ImportFrom.is_lazy = is_lazy;
     p->lineno = lineno;
     p->col_offset = col_offset;
     p->end_lineno = end_lineno;
@@ -9467,6 +9508,11 @@ ast2obj_stmt(struct ast_state *state, void* _o)
         if (PyObject_SetAttr(result, state->names, value) == -1)
             goto failed;
         Py_DECREF(value);
+        value = ast2obj_int(state, o->v.Import.is_lazy);
+        if (!value) goto failed;
+        if (PyObject_SetAttr(result, state->is_lazy, value) == -1)
+            goto failed;
+        Py_DECREF(value);
         break;
     case ImportFrom_kind:
         tp = (PyTypeObject *)state->ImportFrom_type;
@@ -9488,6 +9534,11 @@ ast2obj_stmt(struct ast_state *state, void* _o)
         if (PyObject_SetAttr(result, state->level, value) == -1)
             goto failed;
         Py_DECREF(value);
+        value = ast2obj_int(state, o->v.ImportFrom.is_lazy);
+        if (!value) goto failed;
+        if (PyObject_SetAttr(result, state->is_lazy, value) == -1)
+            goto failed;
+        Py_DECREF(value);
         break;
     case Global_kind:
         tp = (PyTypeObject *)state->Global_type;
@@ -13483,6 +13534,7 @@ obj2ast_stmt(struct ast_state *state, PyObject* obj, stmt_ty* out, PyArena*
     }
     if (isinstance) {
         asdl_alias_seq* names;
+        int is_lazy;
 
         if (PyObject_GetOptionalAttr(obj, state->names, &tmp) < 0) {
             return -1;
@@ -13522,7 +13574,24 @@ obj2ast_stmt(struct ast_state *state, PyObject* obj, stmt_ty* out, PyArena*
             }
             Py_CLEAR(tmp);
         }
-        *out = _PyAST_Import(names, lineno, col_offset, end_lineno,
+        if (PyObject_GetOptionalAttr(obj, state->is_lazy, &tmp) < 0) {
+            return -1;
+        }
+        if (tmp == NULL || tmp == Py_None) {
+            Py_CLEAR(tmp);
+            is_lazy = 0;
+        }
+        else {
+            int res;
+            if (_Py_EnterRecursiveCall(" while traversing 'Import' node")) {
+                goto failed;
+            }
+            res = obj2ast_int(state, tmp, &is_lazy, arena);
+            _Py_LeaveRecursiveCall();
+            if (res != 0) goto failed;
+            Py_CLEAR(tmp);
+        }
+        *out = _PyAST_Import(names, is_lazy, lineno, col_offset, end_lineno,
                              end_col_offset, arena);
         if (*out == NULL) goto failed;
         return 0;
@@ -13536,6 +13605,7 @@ obj2ast_stmt(struct ast_state *state, PyObject* obj, stmt_ty* out, PyArena*
         identifier module;
         asdl_alias_seq* names;
         int level;
+        int is_lazy;
 
         if (PyObject_GetOptionalAttr(obj, state->module, &tmp) < 0) {
             return -1;
@@ -13609,8 +13679,25 @@ obj2ast_stmt(struct ast_state *state, PyObject* obj, stmt_ty* out, PyArena*
             if (res != 0) goto failed;
             Py_CLEAR(tmp);
         }
-        *out = _PyAST_ImportFrom(module, names, level, lineno, col_offset,
-                                 end_lineno, end_col_offset, arena);
+        if (PyObject_GetOptionalAttr(obj, state->is_lazy, &tmp) < 0) {
+            return -1;
+        }
+        if (tmp == NULL || tmp == Py_None) {
+            Py_CLEAR(tmp);
+            is_lazy = 0;
+        }
+        else {
+            int res;
+            if (_Py_EnterRecursiveCall(" while traversing 'ImportFrom' node")) {
+                goto failed;
+            }
+            res = obj2ast_int(state, tmp, &is_lazy, arena);
+            _Py_LeaveRecursiveCall();
+            if (res != 0) goto failed;
+            Py_CLEAR(tmp);
+        }
+        *out = _PyAST_ImportFrom(module, names, level, is_lazy, lineno,
+                                 col_offset, end_lineno, end_col_offset, arena);
         if (*out == NULL) goto failed;
         return 0;
     }
index c2d780ac9b9270cd74601d62831504ab979b639e..9144793ae73ce1b36d7d2443f080c34b3659b0f9 100644 (file)
@@ -9,6 +9,7 @@
 #include "pycore_fileutils.h"     // _PyFile_Flush
 #include "pycore_floatobject.h"   // _PyFloat_ExactDealloc()
 #include "pycore_interp.h"        // _PyInterpreterState_GetConfig()
+#include "pycore_import.h"        // _PyImport_LazyImportModuleLevelObject  ()
 #include "pycore_long.h"          // _PyLong_CompactValue
 #include "pycore_modsupport.h"    // _PyArg_NoKwnames()
 #include "pycore_object.h"        // _Py_AddToAllObjects()
@@ -287,6 +288,61 @@ builtin___import___impl(PyObject *module, PyObject *name, PyObject *globals,
 }
 
 
+/*[clinic input]
+__lazy_import__ as builtin___lazy_import__
+
+    name: object
+    globals: object(c_default="NULL") = None
+    locals: object(c_default="NULL") = None
+    fromlist: object(c_default="NULL") = ()
+    level: int = 0
+
+Lazily imports a module.
+
+Returns either the module to be imported or a imp.lazy_module object which
+indicates the module to be lazily imported.
+[clinic start generated code]*/
+
+static PyObject *
+builtin___lazy_import___impl(PyObject *module, PyObject *name,
+                             PyObject *globals, PyObject *locals,
+                             PyObject *fromlist, int level)
+/*[clinic end generated code: output=300f1771094b9e8c input=9394874f340b2948]*/
+{
+    PyObject *builtins;
+    PyThreadState *tstate = PyThreadState_GET();
+    if (globals == NULL) {
+        globals = PyEval_GetGlobals();
+    }
+    if (locals == NULL) {
+        locals = globals;
+    }
+
+    if (PyDict_GetItemRef(globals, &_Py_ID(__builtins__), &builtins) < 0) {
+        return NULL;
+    }
+    if (builtins == NULL) {
+        PyErr_SetString(PyExc_ValueError,
+                        "unable to get builtins for lazy import");
+        return NULL;
+    }
+    if (PyModule_Check(builtins)) {
+        PyObject *builtins_dict = Py_XNewRef(PyModule_GetDict(builtins));
+        if (builtins_dict == NULL) {
+            Py_DECREF(builtins);
+            PyErr_SetString(PyExc_AttributeError,
+                            "builtins module has no dict");
+            return NULL;
+        }
+        Py_SETREF(builtins, builtins_dict);
+    }
+
+    PyObject *res = _PyImport_LazyImportModuleLevelObject(
+        tstate, name, builtins, globals, locals, fromlist, level);
+    Py_DECREF(builtins);
+    return res;
+}
+
 /*[clinic input]
 abs as builtin_abs
 
@@ -3362,6 +3418,7 @@ static PyMethodDef builtin_methods[] = {
     {"__build_class__", _PyCFunction_CAST(builtin___build_class__),
      METH_FASTCALL | METH_KEYWORDS, build_class_doc},
     BUILTIN___IMPORT___METHODDEF
+    BUILTIN___LAZY_IMPORT___METHODDEF
     BUILTIN_ABS_METHODDEF
     BUILTIN_ALL_METHODDEF
     BUILTIN_ANY_METHODDEF
index e1884ac38c016dbdd6d6523b18ea5b4a0d3a40dd..b461f9b5bea8a6f2f250503214e46f37d4247a5c 100644 (file)
 #include "pycore_audit.h"         // _PySys_Audit()
 #include "pycore_backoff.h"
 #include "pycore_cell.h"          // PyCell_GetRef()
+#include "pycore_ceval.h"         // _PyEval_LazyImportName(), _PyEval_LazyImportFrom()
 #include "pycore_code.h"
 #include "pycore_emscripten_signal.h"  // _Py_CHECK_EMSCRIPTEN_SIGNALS
 #include "pycore_function.h"
+#include "pycore_import.h"        // _PyImport_LoadLazyImportTstate()
 #include "pycore_instruments.h"
 #include "pycore_interpolation.h" // _PyInterpolation_Build()
 #include "pycore_intrinsics.h"
+#include "pycore_lazyimportobject.h"  // PyLazyImport_CheckExact()
 #include "pycore_long.h"          // _PyLong_ExactDealloc(), _PyLong_GetZero()
 #include "pycore_moduleobject.h"  // PyModuleObject
 #include "pycore_object.h"        // _PyObject_GC_TRACK()
@@ -1795,6 +1798,12 @@ dummy_func(
                         }
                         ERROR_NO_POP();
                     }
+
+                    if (PyLazyImport_CheckExact(v_o)) {
+                        PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                        Py_SETREF(v_o, l_v);
+                        ERROR_IF(v_o == NULL);
+                    }
                 }
                 else {
                     /* Slow-path if globals or builtins is not a dict */
@@ -1812,6 +1821,11 @@ dummy_func(
                             ERROR_IF(true);
                         }
                     }
+                    if (PyLazyImport_CheckExact(v_o)) {
+                        PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                        Py_SETREF(v_o, l_v);
+                        ERROR_IF(v_o == NULL);
+                    }
                 }
             }
             v = PyStackRef_FromPyObjectSteal(v_o);
@@ -1821,6 +1835,22 @@ dummy_func(
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
             PyObject *v_o = _PyEval_LoadName(tstate, frame, name);
             ERROR_IF(v_o == NULL);
+            if (PyLazyImport_CheckExact(v_o)) {
+                PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                // cannot early-decref v_o as it may cause a side-effect on l_v
+                if (l_v == NULL) {
+                    Py_DECREF(v_o);
+                    ERROR_IF(true);
+                }
+                int err = PyDict_SetItem(GLOBALS(), name, l_v);
+                if (err < 0) {
+                    Py_DECREF(v_o);
+                    Py_DECREF(l_v);
+                    ERROR_IF(true);
+                }
+                Py_SETREF(v_o, l_v);
+            }
+
             v = PyStackRef_FromPyObjectSteal(v_o);
         }
 
@@ -1846,6 +1876,7 @@ dummy_func(
         op(_LOAD_GLOBAL, ( -- res[1])) {
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg>>1);
             _PyEval_LoadGlobalStackRef(GLOBALS(), BUILTINS(), name, res);
+
             ERROR_IF(PyStackRef_IsNull(*res));
         }
 
@@ -2962,11 +2993,23 @@ dummy_func(
             b = res ? PyStackRef_True : PyStackRef_False;
         }
 
-         inst(IMPORT_NAME, (level, fromlist -- res)) {
-            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
-            PyObject *res_o = _PyEval_ImportName(tstate, frame, name,
-                              PyStackRef_AsPyObjectBorrow(fromlist),
-                              PyStackRef_AsPyObjectBorrow(level));
+        inst(IMPORT_NAME, (level, fromlist -- res)) {
+            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2);
+            PyObject *res_o;
+            if (!(oparg & 0x02)) {
+                res_o = _PyEval_LazyImportName(tstate, BUILTINS(), GLOBALS(),
+                                LOCALS(), name,
+                                PyStackRef_AsPyObjectBorrow(fromlist),
+                                PyStackRef_AsPyObjectBorrow(level),
+                                oparg & 0x01);
+
+            }
+            else {
+                res_o = _PyEval_ImportName(tstate, BUILTINS(), GLOBALS(),
+                                LOCALS(), name,
+                                PyStackRef_AsPyObjectBorrow(fromlist),
+                                PyStackRef_AsPyObjectBorrow(level));
+            }
             DECREF_INPUTS();
             ERROR_IF(res_o == NULL);
             res = PyStackRef_FromPyObjectSteal(res_o);
@@ -2974,7 +3017,16 @@ dummy_func(
 
         inst(IMPORT_FROM, (from -- from, res)) {
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
-            PyObject *res_o = _PyEval_ImportFrom(tstate, PyStackRef_AsPyObjectBorrow(from), name);
+            PyObject *res_o;
+            if (PyLazyImport_CheckExact(PyStackRef_AsPyObjectBorrow(from))) {
+                res_o = _PyEval_LazyImportFrom(
+                    tstate, frame, PyStackRef_AsPyObjectBorrow(from), name);
+            }
+            else {
+                res_o = _PyEval_ImportFrom(
+                    tstate, PyStackRef_AsPyObjectBorrow(from), name);
+            }
+
             ERROR_IF(res_o == NULL);
             res = PyStackRef_FromPyObjectSteal(res_o);
         }
index 61644d35b5e473a7aef477247605f69efa51a618..758b142d7a720e60bdc37631a9e3d817af362213 100644 (file)
@@ -2916,11 +2916,13 @@ _PyEval_SliceIndexNotNone(PyObject *v, Py_ssize_t *pi)
 }
 
 PyObject *
-_PyEval_ImportName(PyThreadState *tstate, _PyInterpreterFrame *frame,
-            PyObject *name, PyObject *fromlist, PyObject *level)
+_PyEval_ImportName(PyThreadState *tstate, PyObject *builtins,
+            PyObject *globals, PyObject *locals, PyObject *name,
+            PyObject *fromlist, PyObject *level)
 {
     PyObject *import_func;
-    if (PyMapping_GetOptionalItem(frame->f_builtins, &_Py_ID(__import__), &import_func) < 0) {
+    if (PyMapping_GetOptionalItem(builtins, &_Py_ID(__import__),
+                                  &import_func) < 0) {
         return NULL;
     }
     if (import_func == NULL) {
@@ -2928,29 +2930,143 @@ _PyEval_ImportName(PyThreadState *tstate, _PyInterpreterFrame *frame,
         return NULL;
     }
 
-    PyObject *locals = frame->f_locals;
+    PyObject *res = _PyEval_ImportNameWithImport(
+        tstate, import_func, globals, locals, name, fromlist, level);
+    Py_DECREF(import_func);
+    return res;
+}
+
+PyObject *
+_PyEval_ImportNameWithImport(PyThreadState *tstate, PyObject *import_func,
+                             PyObject *globals, PyObject *locals,
+                             PyObject *name, PyObject *fromlist, PyObject *level)
+{
     if (locals == NULL) {
         locals = Py_None;
     }
 
     /* Fast path for not overloaded __import__. */
     if (_PyImport_IsDefaultImportFunc(tstate->interp, import_func)) {
-        Py_DECREF(import_func);
         int ilevel = PyLong_AsInt(level);
         if (ilevel == -1 && _PyErr_Occurred(tstate)) {
             return NULL;
         }
         return PyImport_ImportModuleLevelObject(
                         name,
-                        frame->f_globals,
+                        globals,
                         locals,
                         fromlist,
                         ilevel);
     }
 
-    PyObject* args[5] = {name, frame->f_globals, locals, fromlist, level};
+    PyObject *args[5] = {name, globals, locals, fromlist, level};
     PyObject *res = PyObject_Vectorcall(import_func, args, 5, NULL);
-    Py_DECREF(import_func);
+    return res;
+}
+
+static int
+check_lazy_import_compatibility(PyThreadState *tstate, PyObject *globals,
+                               PyObject *name, PyObject *level)
+{
+     // Check if this module should be imported lazily due to
+     // the compatibility mode support via __lazy_modules__.
+    PyObject *lazy_modules = NULL;
+    PyObject *abs_name = NULL;
+    int res = -1;
+
+    if (globals != NULL &&
+        PyMapping_GetOptionalItem(globals, &_Py_ID(__lazy_modules__),
+                                  &lazy_modules) < 0)
+    {
+        return -1;
+    }
+    if (lazy_modules == NULL) {
+        assert(!PyErr_Occurred());
+        return 0;
+    }
+
+    int ilevel = PyLong_AsInt(level);
+    if (ilevel == -1 && _PyErr_Occurred(tstate)) {
+        goto error;
+    }
+
+    abs_name = _PyImport_GetAbsName(tstate, name, globals, ilevel);
+    if (abs_name == NULL) {
+        goto error;
+    }
+
+    res = PySequence_Contains(lazy_modules, abs_name);
+error:
+    Py_XDECREF(abs_name);
+    Py_XDECREF(lazy_modules);
+    return res;
+}
+
+PyObject *
+_PyEval_LazyImportName(PyThreadState *tstate, PyObject *builtins,
+                       PyObject *globals, PyObject *locals, PyObject *name,
+                       PyObject *fromlist, PyObject *level, int lazy)
+{
+    PyObject *res = NULL;
+    // Check if global policy overrides the local syntax
+    switch (PyImport_GetLazyImportsMode()) {
+        case PyImport_LAZY_NONE:
+            lazy = 0;
+            break;
+        case PyImport_LAZY_ALL:
+            lazy = 1;
+            break;
+        case PyImport_LAZY_NORMAL:
+            break;
+    }
+
+    if (!lazy) {
+        // See if __lazy_modules__ forces this to be lazy.
+        lazy = check_lazy_import_compatibility(tstate, globals, name, level);
+        if (lazy < 0) {
+            return NULL;
+        }
+    }
+
+    if (!lazy) {
+        // Not a lazy import or lazy imports are disabled, fallback to the
+        // regular import.
+        return _PyEval_ImportName(tstate, builtins, globals, locals,
+                                  name, fromlist, level);
+    }
+
+    PyObject *lazy_import_func;
+    if (PyMapping_GetOptionalItem(builtins, &_Py_ID(__lazy_import__),
+                                  &lazy_import_func) < 0) {
+        goto error;
+    }
+    if (lazy_import_func == NULL) {
+        assert(!PyErr_Occurred());
+        _PyErr_SetString(tstate, PyExc_ImportError,
+                         "__lazy_import__ not found");
+        goto error;
+    }
+
+    if (locals == NULL) {
+        locals = Py_None;
+    }
+
+    if (_PyImport_IsDefaultLazyImportFunc(tstate->interp, lazy_import_func)) {
+        int ilevel = PyLong_AsInt(level);
+        if (ilevel == -1 && PyErr_Occurred()) {
+            goto error;
+        }
+
+        res = _PyImport_LazyImportModuleLevelObject(
+            tstate, name, builtins, globals, locals, fromlist, ilevel
+        );
+        goto error;
+    }
+
+    PyObject *args[6] = {name, globals, locals, fromlist, level, builtins};
+    res = PyObject_Vectorcall(lazy_import_func, args, 6, NULL);
+error:
+    Py_XDECREF(lazy_import_func);
     return res;
 }
 
@@ -3122,6 +3238,64 @@ done:
     return NULL;
 }
 
+PyObject *
+_PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *v, PyObject *name)
+{
+    assert(PyLazyImport_CheckExact(v));
+    assert(name);
+    assert(PyUnicode_Check(name));
+    PyObject *ret;
+    PyLazyImportObject *d = (PyLazyImportObject *)v;
+    PyObject *mod = PyImport_GetModule(d->lz_from);
+    if (mod != NULL) {
+        // Check if the module already has the attribute, if so, resolve it
+        // eagerly.
+        if (PyModule_Check(mod)) {
+            PyObject *mod_dict = PyModule_GetDict(mod);
+            if (mod_dict != NULL) {
+                if (PyDict_GetItemRef(mod_dict, name, &ret) < 0) {
+                    Py_DECREF(mod);
+                    return NULL;
+                }
+                if (ret != NULL) {
+                    Py_DECREF(mod);
+                    return ret;
+                }
+            }
+        }
+        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;
+}
+
 #define CANNOT_CATCH_MSG "catching classes that do not inherit from "\
                          "BaseException is not allowed"
 
@@ -3410,6 +3584,24 @@ _PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name
         }
         *writeto = PyStackRef_FromPyObjectSteal(res);
     }
+
+    PyObject *res_o = PyStackRef_AsPyObjectBorrow(*writeto);
+    if (res_o != NULL && PyLazyImport_CheckExact(res_o)) {
+        PyObject *l_v = _PyImport_LoadLazyImportTstate(PyThreadState_GET(), res_o);
+        PyStackRef_CLOSE(writeto[0]);
+        if (l_v == NULL) {
+            assert(PyErr_Occurred());
+            *writeto = PyStackRef_NULL;
+            return;
+        }
+        int err = PyDict_SetItem(globals, name, l_v);
+        if (err < 0) {
+            Py_DECREF(l_v);
+            *writeto = PyStackRef_NULL;
+            return;
+        }
+        *writeto = PyStackRef_FromPyObjectSteal(l_v);
+    }
 }
 
 PyObject *
index b170643e236733c6d2539f5fbd0806af4fb584bb..bb5f7ddb85724674a8772b260ae09450fcbce571 100644 (file)
@@ -20,6 +20,7 @@
 #include "pycore_interpolation.h" // _PyInterpolation_Build()
 #include "pycore_intrinsics.h"
 #include "pycore_jit.h"
+#include "pycore_lazyimportobject.h"
 #include "pycore_list.h"          // _PyList_GetItemRef()
 #include "pycore_long.h"          // _PyLong_GetZero()
 #include "pycore_moduleobject.h"  // PyModuleObject
index f08e5847abe32a41e250639175dc21524c1faecc..c8c141f863d26aa0745e38b055db6f52f1061c0a 100644 (file)
@@ -113,6 +113,101 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(builtin___lazy_import____doc__,
+"__lazy_import__($module, /, name, globals=None, locals=None,\n"
+"                fromlist=(), level=0)\n"
+"--\n"
+"\n"
+"Lazily imports a module.\n"
+"\n"
+"Returns either the module to be imported or a imp.lazy_module object which\n"
+"indicates the module to be lazily imported.");
+
+#define BUILTIN___LAZY_IMPORT___METHODDEF    \
+    {"__lazy_import__", _PyCFunction_CAST(builtin___lazy_import__), METH_FASTCALL|METH_KEYWORDS, builtin___lazy_import____doc__},
+
+static PyObject *
+builtin___lazy_import___impl(PyObject *module, PyObject *name,
+                             PyObject *globals, PyObject *locals,
+                             PyObject *fromlist, int level);
+
+static PyObject *
+builtin___lazy_import__(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 5
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(name), &_Py_ID(globals), &_Py_ID(locals), &_Py_ID(fromlist), &_Py_ID(level), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"name", "globals", "locals", "fromlist", "level", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "__lazy_import__",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[5];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
+    PyObject *name;
+    PyObject *globals = NULL;
+    PyObject *locals = NULL;
+    PyObject *fromlist = NULL;
+    int level = 0;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 5, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    name = args[0];
+    if (!noptargs) {
+        goto skip_optional_pos;
+    }
+    if (args[1]) {
+        globals = args[1];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+    if (args[2]) {
+        locals = args[2];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+    if (args[3]) {
+        fromlist = args[3];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+    level = PyLong_AsInt(args[4]);
+    if (level == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+skip_optional_pos:
+    return_value = builtin___lazy_import___impl(module, name, globals, locals, fromlist, level);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(builtin_abs__doc__,
 "abs($module, number, /)\n"
 "--\n"
@@ -1285,4 +1380,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=06500bcc9a341e68 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=1c3327da8885bb8e input=a9049054013a1b77]*/
index 9bbb13f7566517428d5ace91e10ab5f7f88bd3e9..de62714ebddafa6e90e7b54f731936ea1b58f83d 100644 (file)
@@ -622,6 +622,41 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(_imp__set_lazy_attributes__doc__,
+"_set_lazy_attributes($module, modobj, name, /)\n"
+"--\n"
+"\n"
+"Sets attributes to lazy submodules on the module, as side effects.");
+
+#define _IMP__SET_LAZY_ATTRIBUTES_METHODDEF    \
+    {"_set_lazy_attributes", _PyCFunction_CAST(_imp__set_lazy_attributes), METH_FASTCALL, _imp__set_lazy_attributes__doc__},
+
+static PyObject *
+_imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
+                               PyObject *name);
+
+static PyObject *
+_imp__set_lazy_attributes(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    PyObject *modobj;
+    PyObject *name;
+
+    if (!_PyArg_CheckPositional("_set_lazy_attributes", nargs, 2, 2)) {
+        goto exit;
+    }
+    modobj = args[0];
+    if (!PyUnicode_Check(args[1])) {
+        _PyArg_BadArgument("_set_lazy_attributes", "argument 2", "str", args[1]);
+        goto exit;
+    }
+    name = args[1];
+    return_value = _imp__set_lazy_attributes_impl(module, modobj, name);
+
+exit:
+    return return_value;
+}
+
 #ifndef _IMP_CREATE_DYNAMIC_METHODDEF
     #define _IMP_CREATE_DYNAMIC_METHODDEF
 #endif /* !defined(_IMP_CREATE_DYNAMIC_METHODDEF) */
@@ -629,4 +664,4 @@ exit:
 #ifndef _IMP_EXEC_DYNAMIC_METHODDEF
     #define _IMP_EXEC_DYNAMIC_METHODDEF
 #endif /* !defined(_IMP_EXEC_DYNAMIC_METHODDEF) */
-/*[clinic end generated code: output=24f597d6b0f3feed input=a9049054013a1b77]*/
+/*[clinic end generated code: output=5fa42f580441b3fa input=a9049054013a1b77]*/
index 4c4a86de2f99bde7f38019081685bbe27607ab57..f8ae7f18acc80903e0530c9124520e6fb48f3a35 100644 (file)
@@ -1820,6 +1820,180 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(sys_set_lazy_imports_filter__doc__,
+"set_lazy_imports_filter($module, /, filter)\n"
+"--\n"
+"\n"
+"Set the lazy imports filter callback.\n"
+"\n"
+"The filter is a callable which disables lazy imports when they\n"
+"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"
+"\n"
+"Pass None to clear the filter.");
+
+#define SYS_SET_LAZY_IMPORTS_FILTER_METHODDEF    \
+    {"set_lazy_imports_filter", _PyCFunction_CAST(sys_set_lazy_imports_filter), METH_FASTCALL|METH_KEYWORDS, sys_set_lazy_imports_filter__doc__},
+
+static PyObject *
+sys_set_lazy_imports_filter_impl(PyObject *module, PyObject *filter);
+
+static PyObject *
+sys_set_lazy_imports_filter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(filter), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"filter", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "set_lazy_imports_filter",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    PyObject *filter;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    filter = args[0];
+    return_value = sys_set_lazy_imports_filter_impl(module, filter);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(sys_get_lazy_imports_filter__doc__,
+"get_lazy_imports_filter($module, /)\n"
+"--\n"
+"\n"
+"Get the current lazy imports filter callback.\n"
+"\n"
+"Returns the filter callable or None if no filter is set.");
+
+#define SYS_GET_LAZY_IMPORTS_FILTER_METHODDEF    \
+    {"get_lazy_imports_filter", (PyCFunction)sys_get_lazy_imports_filter, METH_NOARGS, sys_get_lazy_imports_filter__doc__},
+
+static PyObject *
+sys_get_lazy_imports_filter_impl(PyObject *module);
+
+static PyObject *
+sys_get_lazy_imports_filter(PyObject *module, PyObject *Py_UNUSED(ignored))
+{
+    return sys_get_lazy_imports_filter_impl(module);
+}
+
+PyDoc_STRVAR(sys_set_lazy_imports__doc__,
+"set_lazy_imports($module, /, mode)\n"
+"--\n"
+"\n"
+"Sets the global lazy imports mode.\n"
+"\n"
+"The mode parameter must be one of the following strings:\n"
+"- \"all\": All top-level imports become potentially lazy\n"
+"- \"none\": All lazy imports are suppressed (even explicitly marked ones)\n"
+"- \"normal\": Only explicitly marked imports (with \'lazy\' keyword) are lazy\n"
+"\n"
+"In addition to the mode, lazy imports can be controlled via the filter\n"
+"provided to sys.set_lazy_imports_filter");
+
+#define SYS_SET_LAZY_IMPORTS_METHODDEF    \
+    {"set_lazy_imports", _PyCFunction_CAST(sys_set_lazy_imports), METH_FASTCALL|METH_KEYWORDS, sys_set_lazy_imports__doc__},
+
+static PyObject *
+sys_set_lazy_imports_impl(PyObject *module, PyObject *mode);
+
+static PyObject *
+sys_set_lazy_imports(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(mode), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"mode", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "set_lazy_imports",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    PyObject *mode;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    mode = args[0];
+    return_value = sys_set_lazy_imports_impl(module, mode);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(sys_get_lazy_imports__doc__,
+"get_lazy_imports($module, /)\n"
+"--\n"
+"\n"
+"Gets the global lazy imports mode.\n"
+"\n"
+"Returns \"all\" if all top level imports are potentially lazy.\n"
+"Returns \"none\" if all explicitly marked lazy imports are suppressed.\n"
+"Returns \"normal\" if only explicitly marked imports are lazy.");
+
+#define SYS_GET_LAZY_IMPORTS_METHODDEF    \
+    {"get_lazy_imports", (PyCFunction)sys_get_lazy_imports, METH_NOARGS, sys_get_lazy_imports__doc__},
+
+static PyObject *
+sys_get_lazy_imports_impl(PyObject *module);
+
+static PyObject *
+sys_get_lazy_imports(PyObject *module, PyObject *Py_UNUSED(ignored))
+{
+    return sys_get_lazy_imports_impl(module);
+}
+
 PyDoc_STRVAR(_jit_is_available__doc__,
 "is_available($module, /)\n"
 "--\n"
@@ -1947,4 +2121,4 @@ exit:
 #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
     #define SYS_GETANDROIDAPILEVEL_METHODDEF
 #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
-/*[clinic end generated code: output=5f7d84c5bf00d557 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=adbadb629b98eabf input=a9049054013a1b77]*/
index 5227312b4f75f8906342bf528d1efd94d51b3f6c..32a03e7212eeab4ad3f1c73d17bbfd8eb240b458 100644 (file)
@@ -357,8 +357,8 @@ codegen_addop_o(compiler *c, location loc,
 #define LOAD_ZERO_SUPER_METHOD -4
 
 static int
-codegen_addop_name(compiler *c, location loc,
-                   int opcode, PyObject *dict, PyObject *o)
+codegen_addop_name_custom(compiler *c, location loc, int opcode,
+                          PyObject *dict, PyObject *o, int shift, int low)
 {
     PyObject *mangled = _PyCompile_MaybeMangle(c, o);
     if (!mangled) {
@@ -369,40 +369,51 @@ codegen_addop_name(compiler *c, location loc,
     if (arg < 0) {
         return ERROR;
     }
+    ADDOP_I(c, loc, opcode, (arg << shift) | low);
+    return SUCCESS;
+}
+
+static int
+codegen_addop_name(compiler *c, location loc,
+                   int opcode, PyObject *dict, PyObject *o)
+{
+    int shift = 0, low = 0;
     if (opcode == LOAD_ATTR) {
-        arg <<= 1;
+        shift = 1;
     }
     if (opcode == LOAD_METHOD) {
         opcode = LOAD_ATTR;
-        arg <<= 1;
-        arg |= 1;
+        shift = 1;
+        low = 1;
     }
     if (opcode == LOAD_SUPER_ATTR) {
-        arg <<= 2;
-        arg |= 2;
+        shift = 2;
+        low = 2;
     }
     if (opcode == LOAD_SUPER_METHOD) {
         opcode = LOAD_SUPER_ATTR;
-        arg <<= 2;
-        arg |= 3;
+        shift = 2;
+        low = 3;
     }
     if (opcode == LOAD_ZERO_SUPER_ATTR) {
         opcode = LOAD_SUPER_ATTR;
-        arg <<= 2;
+        shift = 2;
     }
     if (opcode == LOAD_ZERO_SUPER_METHOD) {
         opcode = LOAD_SUPER_ATTR;
-        arg <<= 2;
-        arg |= 1;
+        shift = 2;
+        low = 1;
     }
-    ADDOP_I(c, loc, opcode, arg);
-    return SUCCESS;
+    return codegen_addop_name_custom(c, loc, opcode, dict, o, shift, low);
 }
 
 #define ADDOP_NAME(C, LOC, OP, O, TYPE) \
     RETURN_IF_ERROR(codegen_addop_name((C), (LOC), (OP), METADATA(C)->u_ ## TYPE, (O)))
 
-static int
+#define ADDOP_NAME_CUSTOM(C, LOC, OP, O, TYPE, SHIFT, LOW) \
+    RETURN_IF_ERROR(codegen_addop_name_custom((C), (LOC), (OP), METADATA(C)->u_ ## TYPE, (O), SHIFT, LOW))
+
+    static int
 codegen_addop_j(instr_sequence *seq, location loc,
                 int opcode, jump_target_label target)
 {
@@ -2864,6 +2875,17 @@ codegen_import_as(compiler *c, location loc,
     return codegen_nameop(c, loc, asname, Store);
 }
 
+static int
+codegen_validate_lazy_import(compiler *c, location loc)
+{
+    if (_PyCompile_ScopeType(c) != COMPILE_SCOPE_MODULE) {
+        return _PyCompile_Error(
+            c, loc, "lazy imports only allowed in module scope");
+    }
+
+    return SUCCESS;
+}
+
 static int
 codegen_import(compiler *c, stmt_ty s)
 {
@@ -2884,7 +2906,18 @@ codegen_import(compiler *c, stmt_ty s)
 
         ADDOP_LOAD_CONST(c, loc, zero);
         ADDOP_LOAD_CONST(c, loc, Py_None);
-        ADDOP_NAME(c, loc, IMPORT_NAME, alias->name, names);
+        if (s->v.Import.is_lazy) {
+            RETURN_IF_ERROR(codegen_validate_lazy_import(c, loc));
+            ADDOP_NAME_CUSTOM(c, loc, IMPORT_NAME, alias->name, names, 2, 1);
+        } else {
+            if (_PyCompile_InExceptionHandler(c) ||
+                _PyCompile_ScopeType(c) != COMPILE_SCOPE_MODULE) {
+                // force eager import in try/except block
+                ADDOP_NAME_CUSTOM(c, loc, IMPORT_NAME, alias->name, names, 2, 2);
+            } else {
+                ADDOP_NAME_CUSTOM(c, loc, IMPORT_NAME, alias->name, names, 2, 0);
+            }
+        }
 
         if (alias->asname) {
             r = codegen_import_as(c, loc, alias->name, alias->asname);
@@ -2930,13 +2963,29 @@ codegen_from_import(compiler *c, stmt_ty s)
 
     ADDOP_LOAD_CONST_NEW(c, LOC(s), names);
 
+    identifier from = &_Py_STR(empty);
     if (s->v.ImportFrom.module) {
-        ADDOP_NAME(c, LOC(s), IMPORT_NAME, s->v.ImportFrom.module, names);
+        from = s->v.ImportFrom.module;
     }
-    else {
-        _Py_DECLARE_STR(empty, "");
-        ADDOP_NAME(c, LOC(s), IMPORT_NAME, &_Py_STR(empty), names);
+    if (s->v.ImportFrom.is_lazy) {
+        alias_ty alias = (alias_ty)asdl_seq_GET(s->v.ImportFrom.names, 0);
+        if (PyUnicode_READ_CHAR(alias->name, 0) == '*') {
+            return _PyCompile_Error(c, LOC(s), "cannot lazy import *");
+        }
+        RETURN_IF_ERROR(codegen_validate_lazy_import(c, LOC(s)));
+        ADDOP_NAME_CUSTOM(c, LOC(s), IMPORT_NAME, from, names, 2, 1);
+    } else {
+        alias_ty alias = (alias_ty)asdl_seq_GET(s->v.ImportFrom.names, 0);
+        if (_PyCompile_InExceptionHandler(c) ||
+            _PyCompile_ScopeType(c) != COMPILE_SCOPE_MODULE ||
+            PyUnicode_READ_CHAR(alias->name, 0) == '*') {
+            // forced non-lazy import due to try/except or import *
+            ADDOP_NAME_CUSTOM(c, LOC(s), IMPORT_NAME, from, names, 2, 2);
+        } else {
+            ADDOP_NAME_CUSTOM(c, LOC(s), IMPORT_NAME, from, names, 2, 0);
+        }
     }
+
     for (Py_ssize_t i = 0; i < n; i++) {
         alias_ty alias = (alias_ty)asdl_seq_GET(s->v.ImportFrom.names, i);
         identifier store_name;
index 1f154004969f31ce062a8c8f7a63f2a6c3408b97..96779a0a219a55aba912c1db18039b8f1d408cfc 100644 (file)
@@ -800,6 +800,26 @@ _PyCompile_TopFBlock(compiler *c)
     return &c->u->u_fblock[c->u->u_nfblocks - 1];
 }
 
+bool
+_PyCompile_InExceptionHandler(compiler *c)
+{
+    for (Py_ssize_t i = 0; i < c->u->u_nfblocks; i++) {
+        fblockinfo *block = &c->u->u_fblock[i];
+        switch (block->fb_type) {
+            case COMPILE_FBLOCK_TRY_EXCEPT:
+            case COMPILE_FBLOCK_FINALLY_TRY:
+            case COMPILE_FBLOCK_FINALLY_END:
+            case COMPILE_FBLOCK_EXCEPTION_HANDLER:
+            case COMPILE_FBLOCK_EXCEPTION_GROUP_HANDLER:
+            case COMPILE_FBLOCK_HANDLER_CLEANUP:
+                return true;
+            default:
+                break;
+        }
+    }
+    return false;
+}
+
 void
 _PyCompile_DeferredAnnotations(compiler *c,
                                PyObject **deferred_annotations,
index f976e150451d4996b8eea02161f954d276425981..9dead4eecc7826b66b9d848fc2c5eb9b6b6f487c 100644 (file)
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
             }
+            if (PyLazyImport_CheckExact(v_o)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                if (l_v == NULL) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    Py_DECREF(v_o);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    SET_CURRENT_CACHED_VALUES(0);
+                    JUMP_TO_ERROR();
+                }
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                int err = PyDict_SetItem(GLOBALS(), name, l_v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                if (err < 0) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    Py_DECREF(v_o);
+                    Py_DECREF(l_v);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    SET_CURRENT_CACHED_VALUES(0);
+                    JUMP_TO_ERROR();
+                }
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                Py_SETREF(v_o, l_v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             v = PyStackRef_FromPyObjectSteal(v_o);
             _tos_cache0 = v;
             _tos_cache1 = PyStackRef_ZERO_BITS;
             oparg = CURRENT_OPARG();
             fromlist = _stack_item_1;
             level = _stack_item_0;
-            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
-            stack_pointer[0] = level;
-            stack_pointer[1] = fromlist;
-            stack_pointer += 2;
-            ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2);
+            PyObject *res_o;
+            if (!(oparg & 0x02)) {
+                stack_pointer[0] = level;
+                stack_pointer[1] = fromlist;
+                stack_pointer += 2;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_LazyImportName(tstate, BUILTINS(), GLOBALS(),
+                    LOCALS(), name,
+                    PyStackRef_AsPyObjectBorrow(fromlist),
+                    PyStackRef_AsPyObjectBorrow(level),
+                    oparg & 0x01);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
+            else {
+                stack_pointer[0] = level;
+                stack_pointer[1] = fromlist;
+                stack_pointer += 2;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_ImportName(tstate, BUILTINS(), GLOBALS(),
+                    LOCALS(), name,
+                    PyStackRef_AsPyObjectBorrow(fromlist),
+                    PyStackRef_AsPyObjectBorrow(level));
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            PyObject *res_o = _PyEval_ImportName(tstate, frame, name,
-                PyStackRef_AsPyObjectBorrow(fromlist),
-                PyStackRef_AsPyObjectBorrow(level));
             _PyStackRef tmp = fromlist;
             fromlist = PyStackRef_NULL;
             stack_pointer[-1] = fromlist;
             oparg = CURRENT_OPARG();
             from = _stack_item_0;
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
-            stack_pointer[0] = from;
-            stack_pointer += 1;
-            ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
-            _PyFrame_SetStackPointer(frame, stack_pointer);
-            PyObject *res_o = _PyEval_ImportFrom(tstate, PyStackRef_AsPyObjectBorrow(from), name);
-            stack_pointer = _PyFrame_GetStackPointer(frame);
+            PyObject *res_o;
+            if (PyLazyImport_CheckExact(PyStackRef_AsPyObjectBorrow(from))) {
+                stack_pointer[0] = from;
+                stack_pointer += 1;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_LazyImportFrom(
+                    tstate, frame, PyStackRef_AsPyObjectBorrow(from), name);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
+            else {
+                stack_pointer[0] = from;
+                stack_pointer += 1;
+                ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_ImportFrom(
+                    tstate, PyStackRef_AsPyObjectBorrow(from), name);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             if (res_o == NULL) {
+                stack_pointer[-1] = from;
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
             }
index 202bf5edcf09df5ae0e8c9aae2baf6eba063b11d..37fa6d679190dd18f4261b6722ca3c238166705a 100644 (file)
             _PyStackRef res;
             from = stack_pointer[-1];
             PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
-            _PyFrame_SetStackPointer(frame, stack_pointer);
-            PyObject *res_o = _PyEval_ImportFrom(tstate, PyStackRef_AsPyObjectBorrow(from), name);
-            stack_pointer = _PyFrame_GetStackPointer(frame);
+            PyObject *res_o;
+            if (PyLazyImport_CheckExact(PyStackRef_AsPyObjectBorrow(from))) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_LazyImportFrom(
+                    tstate, frame, PyStackRef_AsPyObjectBorrow(from), name);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
+            else {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_ImportFrom(
+                    tstate, PyStackRef_AsPyObjectBorrow(from), name);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             if (res_o == NULL) {
                 JUMP_TO_LABEL(error);
             }
             _PyStackRef res;
             fromlist = stack_pointer[-1];
             level = stack_pointer[-2];
-            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
+            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2);
+            PyObject *res_o;
+            if (!(oparg & 0x02)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_LazyImportName(tstate, BUILTINS(), GLOBALS(),
+                    LOCALS(), name,
+                    PyStackRef_AsPyObjectBorrow(fromlist),
+                    PyStackRef_AsPyObjectBorrow(level),
+                    oparg & 0x01);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
+            else {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                res_o = _PyEval_ImportName(tstate, BUILTINS(), GLOBALS(),
+                    LOCALS(), name,
+                    PyStackRef_AsPyObjectBorrow(fromlist),
+                    PyStackRef_AsPyObjectBorrow(level));
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             _PyFrame_SetStackPointer(frame, stack_pointer);
-            PyObject *res_o = _PyEval_ImportName(tstate, frame, name,
-                PyStackRef_AsPyObjectBorrow(fromlist),
-                PyStackRef_AsPyObjectBorrow(level));
             _PyStackRef tmp = fromlist;
             fromlist = PyStackRef_NULL;
             stack_pointer[-1] = fromlist;
                         }
                         JUMP_TO_LABEL(error);
                     }
+                    if (PyLazyImport_CheckExact(v_o)) {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                        Py_SETREF(v_o, l_v);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                        if (v_o == NULL) {
+                            JUMP_TO_LABEL(error);
+                        }
+                    }
                 }
                 else {
                     _PyFrame_SetStackPointer(frame, stack_pointer);
                             JUMP_TO_LABEL(error);
                         }
                     }
+                    if (PyLazyImport_CheckExact(v_o)) {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                        Py_SETREF(v_o, l_v);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                        if (v_o == NULL) {
+                            JUMP_TO_LABEL(error);
+                        }
+                    }
                 }
             }
             v = PyStackRef_FromPyObjectSteal(v_o);
             if (v_o == NULL) {
                 JUMP_TO_LABEL(error);
             }
+            if (PyLazyImport_CheckExact(v_o)) {
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                PyObject *l_v = _PyImport_LoadLazyImportTstate(tstate, v_o);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                if (l_v == NULL) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    Py_DECREF(v_o);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    JUMP_TO_LABEL(error);
+                }
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                int err = PyDict_SetItem(GLOBALS(), name, l_v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+                if (err < 0) {
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    Py_DECREF(v_o);
+                    Py_DECREF(l_v);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    JUMP_TO_LABEL(error);
+                }
+                _PyFrame_SetStackPointer(frame, stack_pointer);
+                Py_SETREF(v_o, l_v);
+                stack_pointer = _PyFrame_GetStackPointer(frame);
+            }
             v = PyStackRef_FromPyObjectSteal(v_o);
             stack_pointer[0] = v;
             stack_pointer += 1;
index c9e892c96b03394e56a3c66821066386c1b6f461..c20c55727d2f94c41bac47cad1a6015bb7897022 100644 (file)
@@ -4,21 +4,28 @@
 #include "pycore_audit.h"         // _PySys_Audit()
 #include "pycore_ceval.h"
 #include "pycore_critical_section.h"  // Py_BEGIN_CRITICAL_SECTION()
+#include "pycore_dict.h"          // _PyDict_Contains_KnownHash()
 #include "pycore_hashtable.h"     // _Py_hashtable_new_full()
 #include "pycore_import.h"        // _PyImport_BootstrapImp()
 #include "pycore_initconfig.h"    // _PyStatus_OK()
 #include "pycore_interp.h"        // struct _import_runtime_state
+#include "pycore_interpframe.h"
+#include "pycore_lazyimportobject.h"
+#include "pycore_long.h"          // _PyLong_GetZero
 #include "pycore_magic_number.h"  // PYC_MAGIC_NUMBER_TOKEN
 #include "pycore_moduleobject.h"  // _PyModule_GetDef()
 #include "pycore_namespace.h"     // _PyNamespace_Type
 #include "pycore_object.h"        // _Py_SetImmortal()
+#include "pycore_pyatomic_ft_wrappers.h"
 #include "pycore_pyerrors.h"      // _PyErr_SetString()
 #include "pycore_pyhash.h"        // _Py_KeyedHash()
 #include "pycore_pylifecycle.h"
 #include "pycore_pymem.h"         // _PyMem_DefaultRawFree()
 #include "pycore_pystate.h"       // _PyInterpreterState_GET()
+#include "pycore_setobject.h"     // _PySet_NextEntry()
 #include "pycore_sysmodule.h"     // _PySys_ClearAttrString()
 #include "pycore_time.h"          // _PyTime_AsMicroseconds()
+#include "pycore_traceback.h"
 #include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL()
 #include "pycore_weakref.h"       // _PyWeakref_GET_REF()
 
@@ -85,6 +92,8 @@ static struct _inittab *inittab_copy = NULL;
     (interp)->imports.modules
 #define MODULES_BY_INDEX(interp) \
     (interp)->imports.modules_by_index
+#define LAZY_MODULES(interp) \
+    (interp)->imports.lazy_modules
 #define IMPORTLIB(interp) \
     (interp)->imports.importlib
 #define OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) \
@@ -98,12 +107,30 @@ static struct _inittab *inittab_copy = NULL;
 #define IMPORT_FUNC(interp) \
     (interp)->imports.import_func
 
+#define LAZY_IMPORT_FUNC(interp) \
+    (interp)->imports.lazy_import_func
+
 #define IMPORT_LOCK(interp) \
     (interp)->imports.lock
 
 #define FIND_AND_LOAD(interp) \
     (interp)->imports.find_and_load
 
+#define LAZY_IMPORTS_MODE(interp) \
+    (interp)->imports.lazy_imports_mode
+
+#define LAZY_IMPORTS_FILTER(interp) \
+    (interp)->imports.lazy_imports_filter
+
+#ifdef Py_GIL_DISABLED
+#define LAZY_IMPORTS_LOCK(interp) PyMutex_Lock(&(interp)->imports.lazy_mutex)
+#define LAZY_IMPORTS_UNLOCK(interp) PyMutex_Unlock(&(interp)->imports.lazy_mutex)
+#else
+#define LAZY_IMPORTS_LOCK(interp)
+#define LAZY_IMPORTS_UNLOCK(interp)
+#endif
+
+
 #define _IMPORT_TIME_HEADER(interp)                                           \
     do {                                                                      \
         if (FIND_AND_LOAD((interp)).header) {                                 \
@@ -241,6 +268,20 @@ import_get_module(PyThreadState *tstate, PyObject *name)
     return m;
 }
 
+PyObject *
+_PyImport_InitLazyModules(PyInterpreterState *interp)
+{
+    assert(LAZY_MODULES(interp) == NULL);
+    LAZY_MODULES(interp) = PyDict_New();
+    return LAZY_MODULES(interp);
+}
+
+void
+_PyImport_ClearLazyModules(PyInterpreterState *interp)
+{
+    Py_CLEAR(LAZY_MODULES(interp));
+}
+
 static int
 import_ensure_initialized(PyInterpreterState *interp, PyObject *mod, PyObject *name)
 {
@@ -2147,7 +2188,8 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,
                     if (filename == NULL) {
                         return NULL;
                     }
-                } else {
+                }
+                else {
                     filename = Py_NewRef(info->filename);
                 }
                 // XXX There's a refleak somewhere with the filename.
@@ -3539,6 +3581,13 @@ _PyImport_InitDefaultImportFunc(PyInterpreterState *interp)
         return -1;
     }
     IMPORT_FUNC(interp) = import_func;
+
+    // Get the __lazy_import__ function
+    if (PyDict_GetItemStringRef(interp->builtins, "__lazy_import__",
+                                &import_func) <= 0) {
+        return -1;
+    }
+    LAZY_IMPORT_FUNC(interp) = import_func;
     return 0;
 }
 
@@ -3548,6 +3597,11 @@ _PyImport_IsDefaultImportFunc(PyInterpreterState *interp, PyObject *func)
     return func == IMPORT_FUNC(interp);
 }
 
+int
+_PyImport_IsDefaultLazyImportFunc(PyInterpreterState *interp, PyObject *func)
+{
+    return func == LAZY_IMPORT_FUNC(interp);
+}
 
 /* Import a module, either built-in, frozen, or external, and return
    its module object WITH INCREMENTED REFERENCE COUNT */
@@ -3811,6 +3865,234 @@ resolve_name(PyThreadState *tstate, PyObject *name, PyObject *globals, int level
     return NULL;
 }
 
+PyObject *
+_PyImport_ResolveName(PyThreadState *tstate, PyObject *name,
+                      PyObject *globals, int level)
+{
+  return resolve_name(tstate, name, globals, level);
+}
+
+PyObject *
+_PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
+{
+    PyObject *obj = NULL;
+    PyObject *fromlist = Py_None;
+    PyObject *import_func = NULL;
+    assert(lazy_import != NULL);
+    assert(PyLazyImport_CheckExact(lazy_import));
+
+    PyLazyImportObject *lz = (PyLazyImportObject *)lazy_import;
+    PyInterpreterState *interp = tstate->interp;
+
+    // Acquire the global import lock to serialize reification
+    _PyImport_AcquireLock(interp);
+
+    // Check if we are already importing this module, if so, then we want to
+    // return an error that indicates we've hit a cycle which will indicate
+    // the value isn't yet available.
+    PyObject *importing = interp->imports.lazy_importing_modules;
+    if (importing == NULL) {
+        importing = interp->imports.lazy_importing_modules = PySet_New(NULL);
+        if (importing == NULL) {
+            _PyImport_ReleaseLock(interp);
+            return NULL;
+        }
+    }
+
+    assert(PyAnySet_CheckExact(importing));
+    int is_loading = _PySet_Contains((PySetObject *)importing, lazy_import);
+    if (is_loading < 0) {
+        _PyImport_ReleaseLock(interp);
+        return NULL;
+    }
+    else if (is_loading == 1) {
+        PyObject *name = _PyLazyImport_GetName(lazy_import);
+        if (name == NULL) {
+            _PyImport_ReleaseLock(interp);
+            return NULL;
+        }
+        PyObject *errmsg = PyUnicode_FromFormat(
+            "cannot import name %R (most likely due to a circular import)",
+            name);
+        if (errmsg == NULL) {
+            Py_DECREF(name);
+            _PyImport_ReleaseLock(interp);
+            return NULL;
+        }
+        PyErr_SetImportErrorSubclass(PyExc_ImportCycleError, errmsg,
+                                     lz->lz_from, NULL);
+        Py_DECREF(errmsg);
+        Py_DECREF(name);
+        _PyImport_ReleaseLock(interp);
+        return NULL;
+    }
+    else if (PySet_Add(importing, lazy_import) < 0) {
+        _PyImport_ReleaseLock(interp);
+        goto error;
+    }
+
+    Py_ssize_t dot = -1;
+    int full = 0;
+    if (lz->lz_attr != NULL) {
+        full = 1;
+    }
+    if (!full) {
+        dot = PyUnicode_FindChar(lz->lz_from, '.', 0,
+                                 PyUnicode_GET_LENGTH(lz->lz_from), 1);
+    }
+    if (dot < 0) {
+        full = 1;
+    }
+
+    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;
+        }
+    }
+
+    PyObject *globals = PyEval_GetGlobals();
+
+    if (PyMapping_GetOptionalItem(lz->lz_builtins, &_Py_ID(__import__),
+                                  &import_func) < 0) {
+        goto error;
+    }
+    if (import_func == NULL) {
+        PyErr_SetString(PyExc_ImportError, "__import__ not found");
+        goto error;
+    }
+    if (full) {
+        obj = _PyEval_ImportNameWithImport(
+            tstate, import_func, globals, globals,
+            lz->lz_from, fromlist, _PyLong_GetZero()
+        );
+    }
+    else {
+        PyObject *name = PyUnicode_Substring(lz->lz_from, 0, dot);
+        if (name == NULL) {
+            goto error;
+        }
+        obj = _PyEval_ImportNameWithImport(
+            tstate, import_func, globals, globals,
+            name, fromlist, _PyLong_GetZero()
+        );
+        Py_DECREF(name);
+    }
+    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;
+        }
+    }
+
+    assert(!PyLazyImport_CheckExact(obj));
+
+    goto ok;
+
+error:
+    Py_CLEAR(obj);
+
+    // If an error occurred and we have frame information, add it to the
+    // exception.
+    if (PyErr_Occurred() && lz->lz_code != NULL && lz->lz_instr_offset >= 0) {
+        // Get the current exception - this already has the full traceback
+        // from the access point.
+        PyObject *exc = _PyErr_GetRaisedException(tstate);
+
+        // Get import name - this can fail and set an exception.
+        PyObject *import_name = _PyLazyImport_GetName(lazy_import);
+        if (!import_name) {
+            // Failed to get import name, just restore original exception.
+            _PyErr_SetRaisedException(tstate, exc);
+            goto ok;
+        }
+
+        // Resolve line number from instruction offset on demand.
+        int lineno = PyCode_Addr2Line((PyCodeObject *)lz->lz_code,
+                                      lz->lz_instr_offset*2);
+
+        // Get strings - these can return NULL on encoding errors.
+        const char *filename_str = PyUnicode_AsUTF8(lz->lz_code->co_filename);
+        if (!filename_str) {
+            // Unicode conversion failed - clear error and restore original
+            // exception.
+            PyErr_Clear();
+            Py_DECREF(import_name);
+            _PyErr_SetRaisedException(tstate, exc);
+            goto ok;
+        }
+
+        const char *funcname_str = PyUnicode_AsUTF8(lz->lz_code->co_name);
+        if (!funcname_str) {
+            // Unicode conversion failed - clear error and restore original
+            // exception.
+            PyErr_Clear();
+            Py_DECREF(import_name);
+            _PyErr_SetRaisedException(tstate, exc);
+            goto ok;
+        }
+
+        // Create a cause exception showing where the lazy import was declared.
+        PyObject *msg = PyUnicode_FromFormat(
+            "deferred import of '%U' raised an exception during resolution",
+            import_name
+        );
+        Py_DECREF(import_name); // Done with import_name.
+
+        if (!msg) {
+            // Failed to create message - restore original exception.
+            _PyErr_SetRaisedException(tstate, exc);
+            goto ok;
+        }
+
+        PyObject *cause_exc = PyObject_CallOneArg(PyExc_ImportError, msg);
+        Py_DECREF(msg);  // Done with msg.
+
+        if (!cause_exc) {
+            // Failed to create exception - restore original.
+            _PyErr_SetRaisedException(tstate, exc);
+            goto ok;
+        }
+
+        // Add traceback entry for the lazy import declaration.
+        _PyErr_SetRaisedException(tstate, cause_exc);
+        _PyTraceback_Add(funcname_str, filename_str, lineno);
+        PyObject *cause_with_tb = _PyErr_GetRaisedException(tstate);
+
+        // Set the cause on the original exception.
+        PyException_SetCause(exc, cause_with_tb);  // Steals ref to cause_with_tb.
+
+        // Restore the original exception with its full traceback.
+        _PyErr_SetRaisedException(tstate, exc);
+    }
+
+ok:
+    if (PySet_Discard(importing, lazy_import) < 0) {
+        Py_CLEAR(obj);
+    }
+
+    // Release the global import lock.
+    _PyImport_ReleaseLock(interp);
+
+    Py_XDECREF(fromlist);
+    Py_XDECREF(import_func);
+    return obj;
+}
+
 static PyObject *
 import_find_and_load(PyThreadState *tstate, PyObject *abs_name)
 {
@@ -3865,6 +4147,28 @@ import_find_and_load(PyThreadState *tstate, PyObject *abs_name)
 #undef accumulated
 }
 
+static PyObject *
+get_abs_name(PyThreadState *tstate, PyObject *name, PyObject *globals,
+             int level)
+{
+    if (level > 0) {
+        return resolve_name(tstate, name, globals, level);
+    }
+    if (PyUnicode_GET_LENGTH(name) == 0) {
+        _PyErr_SetString(tstate, PyExc_ValueError, "Empty module name");
+        return NULL;
+    }
+    return Py_NewRef(name);
+}
+
+PyObject *
+_PyImport_GetAbsName(PyThreadState *tstate, PyObject *name,
+                     PyObject *globals, int level)
+{
+    return get_abs_name(tstate, name, globals, level);
+}
+
+
 PyObject *
 PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
                                  PyObject *locals, PyObject *fromlist,
@@ -3895,17 +4199,9 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
         goto error;
     }
 
-    if (level > 0) {
-        abs_name = resolve_name(tstate, name, globals, level);
-        if (abs_name == NULL)
-            goto error;
-    }
-    else {  /* level == 0 */
-        if (PyUnicode_GET_LENGTH(name) == 0) {
-            _PyErr_SetString(tstate, PyExc_ValueError, "Empty module name");
-            goto error;
-        }
-        abs_name = Py_NewRef(name);
+    abs_name = get_abs_name(tstate, name, globals, level);
+    if (abs_name == NULL) {
+        goto error;
     }
 
     mod = import_get_module(tstate, abs_name);
@@ -4029,6 +4325,243 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
     return final_mod;
 }
 
+static PyObject *
+get_mod_dict(PyObject *module)
+{
+    if (PyModule_Check(module)) {
+        return Py_NewRef(_PyModule_GetDict(module));
+    }
+
+    return PyObject_GetAttr(module, &_Py_ID(__dict__));
+}
+
+// ensure we have the set for the parent module name in sys.lazy_modules.
+// Returns a new reference.
+static PyObject *
+ensure_lazy_submodules(PyDictObject *lazy_modules, PyObject *parent)
+{
+    PyObject *lazy_submodules;
+    Py_BEGIN_CRITICAL_SECTION(lazy_modules);
+    int err = _PyDict_GetItemRef_Unicode_LockHeld(lazy_modules, parent,
+                                                  &lazy_submodules);
+    if (err == 0) {
+        // value isn't present
+        lazy_submodules = PySet_New(NULL);
+        if (lazy_submodules != NULL &&
+            _PyDict_SetItem_LockHeld(lazy_modules, parent,
+                                     lazy_submodules) < 0) {
+            Py_CLEAR(lazy_submodules);
+        }
+    }
+    Py_END_CRITICAL_SECTION();
+    return lazy_submodules;
+}
+
+static int
+register_lazy_on_parent(PyThreadState *tstate, PyObject *name,
+                        PyObject *builtins)
+{
+    int ret = -1;
+    PyObject *parent = NULL;
+    PyObject *child = NULL;
+    PyObject *parent_module = NULL;
+    PyObject *parent_dict = NULL;
+
+    PyInterpreterState *interp = tstate->interp;
+    PyObject *lazy_modules = LAZY_MODULES(interp);
+    assert(lazy_modules != NULL);
+
+    Py_INCREF(name);
+    while (true) {
+        Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0,
+                                            PyUnicode_GET_LENGTH(name), -1);
+        if (dot < 0) {
+            ret = 0;
+            goto done;
+        }
+        parent = PyUnicode_Substring(name, 0, dot);
+        // If `parent` is NULL then this has hit the end of the import, no
+        // more "parent.child" in the import name. The entire import will be
+        // resolved lazily.
+        if (parent == NULL) {
+            goto done;
+        }
+        Py_XDECREF(child);
+        child = PyUnicode_Substring(name, dot + 1, PyUnicode_GET_LENGTH(name));
+        if (child == NULL) {
+            goto done;
+        }
+
+        // Record the child as being lazily imported from the parent.
+        PyObject *lazy_submodules = ensure_lazy_submodules(
+            (PyDictObject *)lazy_modules, parent);
+        if (lazy_submodules == NULL) {
+            goto done;
+        }
+
+        if (PySet_Add(lazy_submodules, child) < 0) {
+            Py_DECREF(lazy_submodules);
+            goto done;
+        }
+        Py_DECREF(lazy_submodules);
+
+        // Add the lazy import for the child to the parent.
+        Py_XSETREF(parent_module, PyImport_GetModule(parent));
+        if (parent_module != NULL) {
+            Py_XSETREF(parent_dict, get_mod_dict(parent_module));
+            if (parent_dict == NULL) {
+                goto done;
+            }
+            if (PyDict_CheckExact(parent_dict)) {
+                int contains = PyDict_Contains(parent_dict, child);
+                if (contains < 0) {
+                    goto done;
+                }
+                if (!contains) {
+                    PyObject *lazy_module_attr = _PyLazyImport_New(
+                        tstate->current_frame, builtins, parent, child
+                    );
+                    if (lazy_module_attr == NULL) {
+                        goto done;
+                    }
+                    if (PyDict_SetItem(parent_dict, child,
+                                       lazy_module_attr) < 0) {
+                        Py_DECREF(lazy_module_attr);
+                        goto done;
+                    }
+                    Py_DECREF(lazy_module_attr);
+                }
+            }
+            ret = 0;
+            goto done;
+        }
+
+        Py_SETREF(name, parent);
+        parent = NULL;
+    }
+
+done:
+    Py_XDECREF(parent_dict);
+    Py_XDECREF(parent_module);
+    Py_XDECREF(child);
+    Py_XDECREF(parent);
+    Py_XDECREF(name);
+    return ret;
+}
+
+static int
+register_from_lazy_on_parent(PyThreadState *tstate, PyObject *abs_name,
+                             PyObject *from, PyObject *builtins)
+{
+    PyObject *fromname = PyUnicode_FromFormat("%U.%U", abs_name, from);
+    if (fromname == NULL) {
+        return -1;
+    }
+    int res = register_lazy_on_parent(tstate, fromname, builtins);
+    Py_DECREF(fromname);
+    return res;
+}
+
+PyObject *
+_PyImport_LazyImportModuleLevelObject(PyThreadState *tstate,
+                                      PyObject *name, PyObject *builtins,
+                                      PyObject *globals, PyObject *locals,
+                                      PyObject *fromlist, int level)
+{
+    PyObject *abs_name = get_abs_name(tstate, name, globals, level);
+    if (abs_name == NULL) {
+        return NULL;
+    }
+
+    PyInterpreterState *interp = tstate->interp;
+    _PyInterpreterFrame *frame = _PyEval_GetFrame();
+    if (frame == NULL || frame->f_globals != frame->f_locals) {
+        Py_DECREF(abs_name);
+        PyErr_SetString(PyExc_SyntaxError,
+                        "'lazy import' is only allowed at module level");
+        return NULL;
+    }
+
+    // Check if the filter disables the lazy import.
+    // We must hold a reference to the filter while calling it to prevent
+    // use-after-free if another thread replaces it via
+    // PyImport_SetLazyImportsFilter.
+    LAZY_IMPORTS_LOCK(interp);
+    PyObject *filter = Py_XNewRef(LAZY_IMPORTS_FILTER(interp));
+    LAZY_IMPORTS_UNLOCK(interp);
+
+    if (filter != NULL) {
+        PyObject *modname;
+        if (PyDict_GetItemRef(globals, &_Py_ID(__name__), &modname) < 0) {
+            Py_DECREF(filter);
+            Py_DECREF(abs_name);
+            return NULL;
+        }
+        if (modname == NULL) {
+            assert(!PyErr_Occurred());
+            modname = Py_NewRef(Py_None);
+        }
+        PyObject *args[] = {modname, name, fromlist};
+        PyObject *res = PyObject_Vectorcall(filter, args, 3, NULL);
+
+        Py_DECREF(modname);
+        Py_DECREF(filter);
+
+        if (res == NULL) {
+            Py_DECREF(abs_name);
+            return NULL;
+        }
+
+        int is_true = PyObject_IsTrue(res);
+        Py_DECREF(res);
+
+        if (is_true < 0) {
+            Py_DECREF(abs_name);
+            return NULL;
+        }
+        if (!is_true) {
+            Py_DECREF(abs_name);
+            return PyImport_ImportModuleLevelObject(
+                name, globals, locals, fromlist, level
+            );
+        }
+    }
+
+    // here, 'filter' is either NULL or is equivalent to a borrowed reference
+    PyObject *res = _PyLazyImport_New(frame, builtins, abs_name, fromlist);
+    if (res == NULL) {
+        Py_DECREF(abs_name);
+        return NULL;
+    }
+    if (fromlist && PyUnicode_Check(fromlist)) {
+        if (register_from_lazy_on_parent(tstate, abs_name, fromlist,
+                                         builtins) < 0) {
+            goto error;
+        }
+    }
+    else if (fromlist && PyTuple_Check(fromlist) &&
+             PyTuple_GET_SIZE(fromlist)) {
+        for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(fromlist); i++) {
+            if (register_from_lazy_on_parent(tstate, abs_name,
+                                             PyTuple_GET_ITEM(fromlist, i),
+                                             builtins) < 0)
+            {
+                goto error;
+            }
+        }
+    }
+    else if (register_lazy_on_parent(tstate, abs_name, builtins) < 0) {
+        goto error;
+    }
+
+    Py_DECREF(abs_name);
+    return res;
+error:
+    Py_DECREF(abs_name);
+    Py_DECREF(res);
+    return NULL;
+}
+
 PyObject *
 PyImport_ImportModuleLevel(const char *name, PyObject *globals, PyObject *locals,
                            PyObject *fromlist, int level)
@@ -4235,6 +4768,10 @@ _PyImport_ClearCore(PyInterpreterState *interp)
     Py_CLEAR(MODULES_BY_INDEX(interp));
     Py_CLEAR(IMPORTLIB(interp));
     Py_CLEAR(IMPORT_FUNC(interp));
+    Py_CLEAR(LAZY_IMPORT_FUNC(interp));
+    Py_CLEAR(interp->imports.lazy_modules);
+    Py_CLEAR(interp->imports.lazy_importing_modules);
+    Py_CLEAR(interp->imports.lazy_imports_filter);
 }
 
 void
@@ -4370,6 +4907,58 @@ PyImport_ImportModuleAttrString(const char *modname, const char *attrname)
     return result;
 }
 
+int
+PyImport_SetLazyImportsFilter(PyObject *filter)
+{
+    if (filter == Py_None) {
+        filter = NULL;
+    }
+    if (filter != NULL && !PyCallable_Check(filter)) {
+        PyErr_SetString(PyExc_ValueError,
+                        "filter provided but is not callable");
+        return -1;
+    }
+
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    // Exchange the filter w/ the lock held. We can't use Py_XSETREF
+    // because we need to release the lock before the decref.
+    LAZY_IMPORTS_LOCK(interp);
+    PyObject *old = LAZY_IMPORTS_FILTER(interp);
+    LAZY_IMPORTS_FILTER(interp) = Py_XNewRef(filter);
+    LAZY_IMPORTS_UNLOCK(interp);
+    Py_XDECREF(old);
+    return 0;
+}
+
+/* Return a strong reference to the current lazy imports filter
+ * or NULL if none exists. This function always succeeds.
+ */
+PyObject *
+PyImport_GetLazyImportsFilter(void)
+{
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    LAZY_IMPORTS_LOCK(interp);
+    PyObject *res = Py_XNewRef(LAZY_IMPORTS_FILTER(interp));
+    LAZY_IMPORTS_UNLOCK(interp);
+    return res;
+}
+
+int
+PyImport_SetLazyImportsMode(PyImport_LazyImportsMode mode)
+{
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    FT_ATOMIC_STORE_INT_RELAXED(LAZY_IMPORTS_MODE(interp), mode);
+    return 0;
+}
+
+/* Checks if lazy imports is globally enabled or disabled. Return 1 when
+ * globally forced on, 0 when globally forced off, or -1 when not set.*/
+PyImport_LazyImportsMode
+PyImport_GetLazyImportsMode(void)
+{
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    return FT_ATOMIC_LOAD_INT_RELAXED(LAZY_IMPORTS_MODE(interp));
+}
 
 /**************/
 /* the module */
@@ -4969,6 +5558,96 @@ _imp_source_hash_impl(PyObject *module, long key, Py_buffer *source)
     return PyBytes_FromStringAndSize(hash.data, sizeof(hash.data));
 }
 
+static int
+publish_lazy_imports_on_module(PyThreadState *tstate,
+                               PyObject *lazy_submodules,
+                               PyObject *name,
+                               PyObject *module_dict)
+{
+    PyObject *builtins = _PyEval_GetBuiltins(tstate);
+    PyObject *attr_name;
+    Py_ssize_t pos = 0;
+    Py_hash_t hash;
+
+    // Enumerate the set of lazy submodules which have been imported from the
+    // parent module.
+    while (_PySet_NextEntryRef(lazy_submodules, &pos, &attr_name, &hash)) {
+        if (_PyDict_Contains_KnownHash(module_dict, attr_name, hash)) {
+            Py_DECREF(attr_name);
+            continue;
+        }
+        // Create a new lazy module attr for the subpackage which was
+        // previously lazily imported.
+        PyObject *lazy_module_attr = _PyLazyImport_New(tstate->current_frame, builtins,
+                                                       name, attr_name);
+        if (lazy_module_attr == NULL) {
+            Py_DECREF(attr_name);
+            return -1;
+        }
+
+        // Publish on the module that was just imported.
+        if (PyDict_SetItem(module_dict, attr_name,
+                           lazy_module_attr) < 0) {
+            Py_DECREF(lazy_module_attr);
+            Py_DECREF(attr_name);
+            return -1;
+        }
+        Py_DECREF(lazy_module_attr);
+        Py_DECREF(attr_name);
+    }
+    return 0;
+}
+
+/*[clinic input]
+_imp._set_lazy_attributes
+    modobj: object
+    name: unicode
+    /
+Sets attributes to lazy submodules on the module, as side effects.
+[clinic start generated code]*/
+
+static PyObject *
+_imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj,
+                               PyObject *name)
+/*[clinic end generated code: output=3369bb3242b1f043 input=38ea6f30956dd7d6]*/
+{
+    PyThreadState *tstate = _PyThreadState_GET();
+    PyObject *module_dict = NULL;
+    PyObject *ret = NULL;
+    PyObject *lazy_modules = LAZY_MODULES(tstate->interp);
+    assert(lazy_modules != NULL);
+
+    PyObject *lazy_submodules;
+    if (PyDict_GetItemRef(lazy_modules, name, &lazy_submodules) < 0) {
+        return NULL;
+    }
+    else if (lazy_submodules == NULL) {
+        Py_RETURN_NONE;
+    }
+
+    module_dict = get_mod_dict(modobj);
+    if (module_dict == NULL || !PyDict_CheckExact(module_dict)) {
+        goto done;
+    }
+
+    assert(PyAnySet_CheckExact(lazy_submodules));
+    Py_BEGIN_CRITICAL_SECTION(lazy_submodules);
+    publish_lazy_imports_on_module(tstate, lazy_submodules, name, module_dict);
+    Py_END_CRITICAL_SECTION();
+    Py_DECREF(lazy_submodules);
+
+    // once a module is imported it is removed from sys.lazy_modules
+    if (PyDict_DelItem(lazy_modules, name) < 0) {
+        goto error;
+    }
+
+done:
+    ret = Py_NewRef(Py_None);
+
+error:
+    Py_XDECREF(module_dict);
+    return ret;
+}
 
 PyDoc_STRVAR(doc_imp,
 "(Extremely) low-level import machinery bits as used by importlib.");
@@ -4993,6 +5672,7 @@ static PyMethodDef imp_methods[] = {
     _IMP_EXEC_BUILTIN_METHODDEF
     _IMP__FIX_CO_FILENAME_METHODDEF
     _IMP_SOURCE_HASH_METHODDEF
+    _IMP__SET_LAZY_ATTRIBUTES_METHODDEF
     {NULL, NULL}  /* sentinel */
 };
 
index 46fd8929041f4538972948880acb43a57c789913..5ffee9eaf9f550b42e7f12c052a40acef52c18e5 100644 (file)
@@ -111,6 +111,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
     SPEC(base_prefix, WSTR_OPT, PUBLIC, SYS_ATTR("base_prefix")),
     SPEC(bytes_warning, UINT, PUBLIC, SYS_FLAG(9)),
     SPEC(cpu_count, INT, PUBLIC, NO_SYS),
+    SPEC(lazy_imports, INT, PUBLIC, NO_SYS),
     SPEC(exec_prefix, WSTR_OPT, PUBLIC, SYS_ATTR("exec_prefix")),
     SPEC(executable, WSTR_OPT, PUBLIC, SYS_ATTR("executable")),
     SPEC(inspect, BOOL, PUBLIC, SYS_FLAG(1)),
@@ -318,6 +319,8 @@ The following implementation-specific options are available:\n\
 "\
 -X importtime[=2]: show how long each import takes; use -X importtime=2 to\n\
          log imports of already-loaded modules; also PYTHONPROFILEIMPORTTIME\n\
+-X lazy_imports=[all|none|normal]: control global lazy imports;\n\
+         default is normal; also PYTHON_LAZY_IMPORTS\n\
 -X int_max_str_digits=N: limit the size of int<->str conversions;\n\
          0 disables the limit; also PYTHONINTMAXSTRDIGITS\n\
 -X no_debug_ranges: don't include extra location information in code objects;\n\
@@ -432,6 +435,7 @@ static const char usage_envvars[] =
 "PYTHON_PRESITE: import this module before site (-X presite)\n"
 #endif
 "PYTHONPROFILEIMPORTTIME: show how long each import takes (-X importtime)\n"
+"PYTHON_LAZY_IMPORTS: control global lazy imports (-X lazy_imports)\n"
 "PYTHONPYCACHEPREFIX: root directory for bytecode cache (pyc) files\n"
 "                  (-X pycache_prefix)\n"
 "PYTHONSAFEPATH  : don't prepend a potentially unsafe path to sys.path.\n"
@@ -941,6 +945,8 @@ config_check_consistency(const PyConfig *config)
     assert(config->int_max_str_digits >= 0);
     // cpu_count can be -1 if the user doesn't override it.
     assert(config->cpu_count != 0);
+    // lazy_imports can be -1 (default), 0 (off), or 1 (on).
+    assert(config->lazy_imports >= -1 && config->lazy_imports <= 1);
     // config->use_frozen_modules is initialized later
     // by _PyConfig_InitImportConfig().
     assert(config->thread_inherit_context >= 0);
@@ -1052,6 +1058,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
     config->_is_python_build = 0;
     config->code_debug_ranges = 1;
     config->cpu_count = -1;
+    config->lazy_imports = -1;
 #ifdef Py_GIL_DISABLED
     config->thread_inherit_context = 1;
     config->context_aware_warnings = 1;
@@ -2300,6 +2307,49 @@ config_init_import_time(PyConfig *config)
     return _PyStatus_OK();
 }
 
+static PyStatus
+config_init_lazy_imports(PyConfig *config)
+{
+    int lazy_imports = -1;
+
+    const char *env = config_get_env(config, "PYTHON_LAZY_IMPORTS");
+    if (env) {
+        if (strcmp(env, "all") == 0) {
+            lazy_imports = 1;
+        }
+        else if (strcmp(env, "none") == 0) {
+            lazy_imports = 0;
+        }
+        else if (strcmp(env, "normal") == 0) {
+            lazy_imports = -1;
+        }
+        else {
+            return _PyStatus_ERR("PYTHON_LAZY_IMPORTS: invalid value; "
+                                 "expected 'all', 'none', or 'normal'");
+        }
+        config->lazy_imports = lazy_imports;
+    }
+
+    const wchar_t *x_value = config_get_xoption_value(config, L"lazy_imports");
+    if (x_value) {
+        if (wcscmp(x_value, L"all") == 0) {
+            lazy_imports = 1;
+        }
+        else if (wcscmp(x_value, L"none") == 0) {
+            lazy_imports = 0;
+        }
+        else if (wcscmp(x_value, L"normal") == 0) {
+            lazy_imports = -1;
+        }
+        else {
+            return _PyStatus_ERR("-X lazy_imports: invalid value; "
+                                 "expected 'all', 'none', or 'normal'");
+        }
+        config->lazy_imports = lazy_imports;
+    }
+    return _PyStatus_OK();
+}
+
 static PyStatus
 config_read_complex_options(PyConfig *config)
 {
@@ -2323,6 +2373,13 @@ config_read_complex_options(PyConfig *config)
         }
     }
 
+    if (config->lazy_imports < 0) {
+        status = config_init_lazy_imports(config);
+        if (_PyStatus_EXCEPTION(status)) {
+            return status;
+        }
+    }
+
     if (config->tracemalloc < 0) {
         status = config_init_tracemalloc(config);
         if (_PyStatus_EXCEPTION(status)) {
@@ -2712,6 +2769,9 @@ config_read(PyConfig *config, int compute_path_config)
     if (config->tracemalloc < 0) {
         config->tracemalloc = 0;
     }
+    if (config->lazy_imports < 0) {
+        config->lazy_imports = -1;  // Default is auto/unset
+    }
     if (config->perf_profiling < 0) {
         config->perf_profiling = 0;
     }
index 7e47b70f1f48f6356a0dd1f740174b74ebc6626a..0791d042710efb0122c33893d81bb0777fa5ca16 100644 (file)
 #include "pycore_floatobject.h"
 #include "pycore_frame.h"
 #include "pycore_function.h"
+#include "pycore_import.h"
 #include "pycore_interpframe.h"
 #include "pycore_interpolation.h"
 #include "pycore_intrinsics.h"
+#include "pycore_lazyimportobject.h"
 #include "pycore_list.h"
 #include "pycore_long.h"
 #include "pycore_mmap.h"
index bb51f8d191c1c3b27b02b8bbe3a3d50f76352161..7dfeb5b847b25438b63a73b9664d92e3b450fd7b 100644 (file)
@@ -15,6 +15,7 @@
 #include "pycore_initconfig.h"    // _PyStatus_OK()
 #include "pycore_interpolation.h" // _PyInterpolation_InitTypes()
 #include "pycore_long.h"          // _PyLong_InitTypes()
+#include "pycore_moduleobject.h"  // _PyModule_InitModuleDictWatcher()
 #include "pycore_object.h"        // _PyDebug_PrintTotalRefs()
 #include "pycore_obmalloc.h"      // _PyMem_init_obmalloc()
 #include "pycore_optimizer.h"     // _Py_Executors_InvalidateAll
@@ -632,6 +633,11 @@ pycore_create_interpreter(_PyRuntimeState *runtime,
     _PyInterpreterState_SetWhence(interp, _PyInterpreterState_WHENCE_RUNTIME);
     interp->_ready = 1;
 
+    /* Initialize the module dict watcher early, before any modules are created */
+    if (_PyModule_InitModuleDictWatcher(interp) != 0) {
+        return _PyStatus_ERR("failed to initialize module dict watcher");
+    }
+
     status = _PyConfig_Copy(&interp->config, src_config);
     if (_PyStatus_EXCEPTION(status)) {
         return status;
@@ -1335,6 +1341,22 @@ init_interp_main(PyThreadState *tstate)
         }
     }
 
+    // Initialize lazy imports based on configuration. Do this after site
+    // module is imported to avoid circular imports during startup.
+    if (config->lazy_imports != -1) {
+        PyImport_LazyImportsMode lazy_mode;
+        if (config->lazy_imports == 1) {
+            lazy_mode = PyImport_LAZY_ALL;
+        }
+        else {
+            lazy_mode = PyImport_LAZY_NONE;
+        }
+        if (PyImport_SetLazyImportsMode(lazy_mode) < 0) {
+            return _PyStatus_ERR("failed to set lazy imports mode");
+        }
+    }
+    // If config->lazy_imports == -1, use the default mode, no change needed.
+
     if (is_main_interp) {
 #ifndef MS_WINDOWS
         emit_stderr_warning_for_legacy_locale(interp->runtime);
@@ -1802,6 +1824,9 @@ finalize_modules(PyThreadState *tstate)
     // initialization API)
     _PyImport_ClearModulesByIndex(interp);
 
+    // Clear the dict of lazily loaded module nname to submodule names
+    _PyImport_ClearLazyModules(interp);
+
     // Clear and delete the modules directory.  Actual modules will
     // still be there only if imported during the execution of some
     // destructor.
@@ -2449,6 +2474,11 @@ new_interpreter(PyThreadState **tstate_p,
     _PyInterpreterState_SetWhence(interp, whence);
     interp->_ready = 1;
 
+    /* Initialize the module dict watcher early, before any modules are created */
+    if (_PyModule_InitModuleDictWatcher(interp) != 0) {
+        goto error;
+    }
+
     /* From this point until the init_interp_create_gil() call,
        we must not do anything that requires that the GIL be held
        (or otherwise exist).  That applies whether or not the new
index 21e0eb802dd099f38792ebfb01c8e9c2a400fdb6..7c02e929d47d9e19ebd1b0cf0cb1948c921cc7f7 100644 (file)
@@ -8,6 +8,7 @@
 #include "pycore_dict.h"          // DICT_KEYS_UNICODE
 #include "pycore_function.h"      // _PyFunction_GetVersionForCurrentState()
 #include "pycore_interpframe.h"   // FRAME_SPECIALS_SIZE
+#include "pycore_lazyimportobject.h" // PyLazyImport_CheckExact
 #include "pycore_list.h"          // _PyListIterObject
 #include "pycore_long.h"          // _PyLong_IsNonNegativeCompact()
 #include "pycore_moduleobject.h"
@@ -129,6 +130,7 @@ _PyCode_Quicken(_Py_CODEUNIT *instructions, Py_ssize_t size, int enable_counters
 #define SPEC_FAIL_ATTR_BUILTIN_CLASS_METHOD 22
 #define SPEC_FAIL_ATTR_CLASS_METHOD_OBJ 23
 #define SPEC_FAIL_ATTR_OBJECT_SLOT 24
+#define SPEC_FAIL_ATTR_MODULE_LAZY_VALUE 25
 
 #define SPEC_FAIL_ATTR_INSTANCE_ATTRIBUTE 26
 #define SPEC_FAIL_ATTR_METACLASS_ATTRIBUTE 27
@@ -383,6 +385,10 @@ specialize_module_load_attr_lock_held(PyDictObject *dict, _Py_CODEUNIT *instr, P
     }
     PyObject *value;
     Py_ssize_t index = _PyDict_LookupIndexAndValue(dict, name, &value);
+    if (value != NULL && PyLazyImport_CheckExact(value)) {
+        SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_MODULE_LAZY_VALUE);
+        return -1;
+    }
     assert(index != DKIX_ERROR);
     if (index != (uint16_t)index) {
         SPECIALIZATION_FAIL(LOAD_ATTR,
@@ -1307,16 +1313,17 @@ specialize_load_global_lock_held(
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_LOAD_GLOBAL_NON_STRING_OR_SPLIT);
         goto fail;
     }
-#ifdef Py_GIL_DISABLED
     PyObject *value;
     Py_ssize_t index = _PyDict_LookupIndexAndValue((PyDictObject *)globals, name, &value);
-#else
-    Py_ssize_t index = _PyDictKeys_StringLookup(globals_keys, name);
-#endif
     if (index == DKIX_ERROR) {
         SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_EXPECTED_ERROR);
         goto fail;
     }
+    if (value != NULL && PyLazyImport_CheckExact(value)) {
+        SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_ATTR_MODULE_LAZY_VALUE);
+        Py_DECREF(value);
+        goto fail;
+    }
     PyInterpreterState *interp = _PyInterpreterState_GET();
     if (index != DKIX_EMPTY) {
         if (index != (uint16_t)index) {
index 29ac8f6880c575460eb330d1401bf0ac43b6bb2d..beb6df88d097e393e4ad09f939cf4d1521727c5f 100644 (file)
@@ -141,6 +141,7 @@ ste_new(struct symtable *st, identifier name, _Py_block_ty block,
     ste->ste_needs_classdict = 0;
     ste->ste_has_conditional_annotations = 0;
     ste->ste_in_conditional_block = 0;
+    ste->ste_in_try_block = 0;
     ste->ste_in_unevaluated_annotation = 0;
     ste->ste_annotation_block = NULL;
 
@@ -1747,6 +1748,13 @@ symtable_enter_type_param_block(struct symtable *st, identifier name,
 #define LEAVE_CONDITIONAL_BLOCK(ST) \
     (ST)->st_cur->ste_in_conditional_block = in_conditional_block;
 
+#define ENTER_TRY_BLOCK(ST) \
+    int in_try_block = (ST)->st_cur->ste_in_try_block; \
+    (ST)->st_cur->ste_in_try_block = 1;
+
+#define LEAVE_TRY_BLOCK(ST) \
+    (ST)->st_cur->ste_in_try_block = in_try_block;
+
 #define ENTER_RECURSIVE() \
 if (Py_EnterRecursiveCall(" during compilation")) { \
     return 0; \
@@ -1808,6 +1816,38 @@ check_import_from(struct symtable *st, stmt_ty s)
     return 1;
 }
 
+static int
+check_lazy_import_context(struct symtable *st, stmt_ty s,
+                          const char* import_type)
+{
+    // Check if inside try/except block.
+    if (st->st_cur->ste_in_try_block) {
+        PyErr_Format(PyExc_SyntaxError,
+                     "lazy %s not allowed inside try/except blocks",
+                     import_type);
+        SET_ERROR_LOCATION(st->st_filename, LOCATION(s));
+        return 0;
+    }
+
+    // Check if inside function scope.
+    if (st->st_cur->ste_type == FunctionBlock) {
+        PyErr_Format(PyExc_SyntaxError,
+                     "lazy %s not allowed inside functions", import_type);
+        SET_ERROR_LOCATION(st->st_filename, LOCATION(s));
+        return 0;
+    }
+
+    // Check if inside class scope.
+    if (st->st_cur->ste_type == ClassBlock) {
+        PyErr_Format(PyExc_SyntaxError,
+                     "lazy %s not allowed inside classes", import_type);
+        SET_ERROR_LOCATION(st->st_filename, LOCATION(s));
+        return 0;
+    }
+
+    return 1;
+}
+
 static bool
 allows_top_level_await(struct symtable *st)
 {
@@ -2076,19 +2116,23 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s)
         break;
     case Try_kind: {
         ENTER_CONDITIONAL_BLOCK(st);
+        ENTER_TRY_BLOCK(st);
         VISIT_SEQ(st, stmt, s->v.Try.body);
         VISIT_SEQ(st, excepthandler, s->v.Try.handlers);
         VISIT_SEQ(st, stmt, s->v.Try.orelse);
         VISIT_SEQ(st, stmt, s->v.Try.finalbody);
+        LEAVE_TRY_BLOCK(st);
         LEAVE_CONDITIONAL_BLOCK(st);
         break;
     }
     case TryStar_kind: {
         ENTER_CONDITIONAL_BLOCK(st);
+        ENTER_TRY_BLOCK(st);
         VISIT_SEQ(st, stmt, s->v.TryStar.body);
         VISIT_SEQ(st, excepthandler, s->v.TryStar.handlers);
         VISIT_SEQ(st, stmt, s->v.TryStar.orelse);
         VISIT_SEQ(st, stmt, s->v.TryStar.finalbody);
+        LEAVE_TRY_BLOCK(st);
         LEAVE_CONDITIONAL_BLOCK(st);
         break;
     }
@@ -2098,9 +2142,33 @@ symtable_visit_stmt(struct symtable *st, stmt_ty s)
             VISIT(st, expr, s->v.Assert.msg);
         break;
     case Import_kind:
+        if (s->v.Import.is_lazy) {
+            if (!check_lazy_import_context(st, s, "import")) {
+                return 0;
+            }
+        }
         VISIT_SEQ(st, alias, s->v.Import.names);
         break;
     case ImportFrom_kind:
+        if (s->v.ImportFrom.is_lazy) {
+            if (!check_lazy_import_context(st, s, "from ... import")) {
+                return 0;
+            }
+
+            // Check for import *
+            for (Py_ssize_t i = 0; i < asdl_seq_LEN(s->v.ImportFrom.names);
+                 i++) {
+                alias_ty alias = (alias_ty)asdl_seq_GET(
+                    s->v.ImportFrom.names, i);
+                if (alias->name &&
+                        _PyUnicode_EqualToASCIIString(alias->name, "*")) {
+                    PyErr_SetString(PyExc_SyntaxError,
+                                    "lazy from ... import * is not allowed");
+                    SET_ERROR_LOCATION(st->st_filename, LOCATION(s));
+                    return 0;
+                }
+            }
+        }
         VISIT_SEQ(st, alias, s->v.ImportFrom.names);
         if (!check_import_from(st, s)) {
             return 0;
index 61dbe5edb87186eff82c340cce4c2358e120db05..28b2108940c85303c600b0b8f71d20740bb2c8e4 100644 (file)
@@ -2785,6 +2785,128 @@ PyAPI_FUNC(int) PyUnstable_CopyPerfMapFile(const char* parent_filename) {
     return 0;
 }
 
+/*[clinic input]
+sys.set_lazy_imports_filter
+
+    filter: object
+
+Set the lazy imports filter callback.
+
+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])
+
+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]*/
+{
+    if (PyImport_SetLazyImportsFilter(filter) < 0) {
+        return NULL;
+    }
+
+    Py_RETURN_NONE;
+}
+
+/*[clinic input]
+sys.get_lazy_imports_filter
+
+Get the current lazy imports filter callback.
+
+Returns the filter callable or None if no filter is set.
+[clinic start generated code]*/
+
+static PyObject *
+sys_get_lazy_imports_filter_impl(PyObject *module)
+/*[clinic end generated code: output=3bf73022892165af input=cf1e07cb8e203c94]*/
+{
+    PyObject *filter = PyImport_GetLazyImportsFilter();
+    if (filter == NULL) {
+        assert(!PyErr_Occurred());
+        Py_RETURN_NONE;
+    }
+    return filter;
+}
+
+/*[clinic input]
+sys.set_lazy_imports
+
+    mode: object
+
+Sets the global lazy imports mode.
+
+The mode parameter must be one of the following strings:
+- "all": All top-level imports become potentially lazy
+- "none": All lazy imports are suppressed (even explicitly marked ones)
+- "normal": Only explicitly marked imports (with 'lazy' keyword) are lazy
+
+In addition to the mode, lazy imports can be controlled via the filter
+provided to sys.set_lazy_imports_filter
+
+[clinic start generated code]*/
+
+static PyObject *
+sys_set_lazy_imports_impl(PyObject *module, PyObject *mode)
+/*[clinic end generated code: output=1ff34ba6c4feaf73 input=f04e70d8bf9fe4f6]*/
+{
+    PyImport_LazyImportsMode lazy_mode;
+    if (!PyUnicode_Check(mode)) {
+        PyErr_SetString(PyExc_TypeError,
+                        "mode must be a string: 'normal', 'all', or 'none'");
+        return NULL;
+    }
+    if (PyUnicode_CompareWithASCIIString(mode, "normal") == 0) {
+        lazy_mode = PyImport_LAZY_NORMAL;
+    }
+    else if (PyUnicode_CompareWithASCIIString(mode, "all") == 0) {
+        lazy_mode = PyImport_LAZY_ALL;
+    }
+    else if (PyUnicode_CompareWithASCIIString(mode, "none") == 0) {
+        lazy_mode = PyImport_LAZY_NONE;
+    }
+    else {
+        PyErr_SetString(PyExc_ValueError,
+                        "mode must be 'normal', 'all', or 'none'");
+        return NULL;
+    }
+
+    if (PyImport_SetLazyImportsMode(lazy_mode)) {
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+/*[clinic input]
+sys.get_lazy_imports
+
+Gets the global lazy imports mode.
+
+Returns "all" if all top level imports are potentially lazy.
+Returns "none" if all explicitly marked lazy imports are suppressed.
+Returns "normal" if only explicitly marked imports are lazy.
+
+[clinic start generated code]*/
+
+static PyObject *
+sys_get_lazy_imports_impl(PyObject *module)
+/*[clinic end generated code: output=4147dec48c51ae99 input=8cb574f1e4e3003c]*/
+{
+    switch (PyImport_GetLazyImportsMode()) {
+        case PyImport_LAZY_NORMAL:
+            return PyUnicode_FromString("normal");
+        case PyImport_LAZY_ALL:
+            return PyUnicode_FromString("all");
+        case PyImport_LAZY_NONE:
+            return PyUnicode_FromString("none");
+        default:
+            PyErr_SetString(PyExc_RuntimeError, "unknown lazy imports mode");
+            return NULL;
+    }
+}
 
 static PyMethodDef sys_methods[] = {
     /* Might as well keep this in alphabetic order */
@@ -2850,6 +2972,10 @@ static PyMethodDef sys_methods[] = {
     SYS_UNRAISABLEHOOK_METHODDEF
     SYS_GET_INT_MAX_STR_DIGITS_METHODDEF
     SYS_SET_INT_MAX_STR_DIGITS_METHODDEF
+    SYS_GET_LAZY_IMPORTS_METHODDEF
+    SYS_SET_LAZY_IMPORTS_METHODDEF
+    SYS_GET_LAZY_IMPORTS_FILTER_METHODDEF
+    SYS_SET_LAZY_IMPORTS_FILTER_METHODDEF
     SYS__BASEREPL_METHODDEF
 #ifdef Py_STATS
     SYS__STATS_ON_METHODDEF
@@ -3374,6 +3500,7 @@ static PyStructSequence_Field flags_fields[] = {
     {"gil",                     "-X gil"},
     {"thread_inherit_context",  "-X thread_inherit_context"},
     {"context_aware_warnings",    "-X context_aware_warnings"},
+    {"lazy_imports",            "-X lazy_imports"},
     {0}
 };
 
@@ -3383,7 +3510,7 @@ static PyStructSequence_Desc flags_desc = {
     "sys.flags",        /* name */
     flags__doc__,       /* doc */
     flags_fields,       /* fields */
-    18
+    19
 };
 
 static void
@@ -3476,6 +3603,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
 #endif
     SetFlag(config->thread_inherit_context);
     SetFlag(config->context_aware_warnings);
+    SetFlag(config->lazy_imports);
 #undef SetFlagObj
 #undef SetFlag
     return 0;
@@ -4203,6 +4331,15 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p)
         goto error;
     }
 
+    PyObject *lazy_modules = _PyImport_InitLazyModules(interp); // borrowed reference
+    if (lazy_modules == NULL) {
+        goto error;
+    }
+
+    if (PyDict_SetItemString(sysdict, "lazy_modules", lazy_modules) < 0) {
+        goto error;
+    }
+
     PyStatus status = _PySys_SetPreliminaryStderr(sysdict);
     if (_PyStatus_EXCEPTION(status)) {
         return status;
index 301784f773d31f3f89734ebc9d48c5627c04265c..d645d2b6150d3414b4d5ba6edd5fc557c3b1aca3 100644 (file)
@@ -59,6 +59,7 @@ Objects/interpolationobject.c -       _PyInterpolation_Type   -
 Objects/iterobject.c   -       PyCallIter_Type -
 Objects/iterobject.c   -       PySeqIter_Type  -
 Objects/iterobject.c   -       _PyAnextAwaitable_Type  -
+Objects/lazyimportobject.c     -       PyLazyImport_Type       -
 Objects/listobject.c   -       PyListIter_Type -
 Objects/listobject.c   -       PyListRevIter_Type      -
 Objects/listobject.c   -       PyList_Type     -
@@ -176,6 +177,7 @@ Objects/exceptions.c        -       _PyExc_StopIteration    -
 Objects/exceptions.c   -       _PyExc_GeneratorExit    -
 Objects/exceptions.c   -       _PyExc_SystemExit       -
 Objects/exceptions.c   -       _PyExc_KeyboardInterrupt        -
+Objects/exceptions.c   -       _PyExc_ImportCycleError -
 Objects/exceptions.c   -       _PyExc_ImportError      -
 Objects/exceptions.c   -       _PyExc_ModuleNotFoundError      -
 Objects/exceptions.c   -       _PyExc_OSError  -
@@ -242,6 +244,7 @@ Objects/exceptions.c        -       PyExc_StopIteration     -
 Objects/exceptions.c   -       PyExc_GeneratorExit     -
 Objects/exceptions.c   -       PyExc_SystemExit        -
 Objects/exceptions.c   -       PyExc_KeyboardInterrupt -
+Objects/exceptions.c   -       PyExc_ImportCycleError  -
 Objects/exceptions.c   -       PyExc_ImportError       -
 Objects/exceptions.c   -       PyExc_ModuleNotFoundError       -
 Objects/exceptions.c   -       PyExc_OSError   -
index 3537c74a8203650c5d8b5df962bdbcbf3c4d69e8..afdd9b77e7c7ffa30bf26a57d506eabe38c8ab55 100644 (file)
 #include "pycore_frame.h"
 #include "pycore_function.h"
 #include "pycore_genobject.h"
+#include "pycore_import.h"
 #include "pycore_interpframe.h"
 #include "pycore_interpolation.h"
 #include "pycore_intrinsics.h"
+#include "pycore_lazyimportobject.h"
 #include "pycore_jit.h"
 #include "pycore_list.h"
 #include "pycore_long.h"