diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 698a9d0689d..843b8129c73 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -911,6 +911,43 @@ 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:: 3.15 + + +.. 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:: 3.15 + + +.. function:: get_lazy_modules() + + Returns a set of fully-qualified module names that have been lazily imported. + This is primarily useful for diagnostics and introspection. + + Note that modules are removed from this set when they are reified (actually + loaded on first use). + + .. versionadded:: 3.15 + + .. function:: getrefcount(object) Return the reference count of the *object*. The count returned is generally one @@ -1715,6 +1752,57 @@ 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:: 3.15 + + +.. 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:: 3.15 + + .. function:: setprofile(profilefunc) .. index:: diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5379ac3abba..5a10c036c35 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -65,6 +65,8 @@ Summary -- Release highlights .. PEP-sized items next. +* :pep:`810`: :ref:`Explicit lazy imports for faster startup times + ` * :pep:`799`: :ref:`A dedicated profiling package for organizing Python profiling tools ` * :pep:`686`: :ref:`Python now uses UTF-8 as the default encoding @@ -77,6 +79,97 @@ 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 +``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 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 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. + +When a lazy import eventually 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 traceback includes both the location where the name was accessed and the +original import statement, making it straightforward to diagnose the problem. + +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 the fully-qualified module name and returns a boolean. This allows +patterns like making only your own application's modules lazy while keeping +third-party dependencies eager: + +.. code-block:: python + + import sys + + sys.set_lazy_imports_filter(lambda name: name.startswith("myapp.")) + sys.set_lazy_imports("all") + + import myapp.slow_module # lazy (matches filter) + import json # eager (does not match filter) + +For debugging and introspection, :func:`sys.get_lazy_modules` returns a set +containing the names of all modules that have been lazily imported but not yet +loaded. 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 ``lazy`` can appear. Lazy imports are only +permitted at module scope; using ``lazy`` inside a function, class body, or +``try``/``except``/``finally`` block raises a :exc:`SyntaxError`. Star imports +cannot be lazy (``lazy from module import *`` is a syntax error), and future +imports cannot be lazy either (``lazy from __future__ import ...`` raises +:exc:`SyntaxError`). + +.. seealso:: :pep:`810` for the full specification and rationale. + +(Contributed by Pablo Galindo Salgado and Dino Viehland.) + + .. _whatsnew315-sampling-profiler: :pep:`799`: High frequency statistical sampling profiler diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 1bf545ec849..a147b9e5d73 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -318,6 +318,7 @@ struct _import_state { PyObject *lazy_imports_filter; PyObject *lazy_importing_modules; PyObject *lazy_modules; + PyObject *lazy_modules_set; /* Set of fully-qualified module names lazily imported (PEP 810) */ /* The global import lock. */ _PyRecursiveMutex lock; /* diagnostic info in PyImport_ImportModuleLevelObject() */ diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 78856bf07aa..072021e5959 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -2571,302 +2571,6 @@ def test_disallowed_reimport(self): self.assertIsNot(excsnap, None) -class LazyImportTests(unittest.TestCase): - def tearDown(self): - """Make sure no modules pre-exist in sys.modules which are being used to - test.""" - 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(None) - - def test_basic_unused(self): - try: - import test.test_import.data.lazy_imports.basic_unused - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_basic_unused_use_externally(self): - try: - from test.test_import.data.lazy_imports import basic_unused - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - x = basic_unused.test.test_import.data.lazy_imports.basic2 - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_basic_from_unused_use_externally(self): - try: - from test.test_import.data.lazy_imports import basic_from_unused - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - x = basic_from_unused.basic2 - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_basic_unused_dir(self): - try: - import test.test_import.data.lazy_imports.basic_unused - except ImportError as e: - self.fail('lazy import failed') - - x = dir(test.test_import.data.lazy_imports.basic_unused) - self.assertIn("test", x) - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_basic_dir(self): - try: - from test.test_import.data.lazy_imports import basic_dir - except ImportError as e: - self.fail('lazy import failed') - - self.assertIn("test", basic_dir.x) - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_basic_used(self): - try: - import test.test_import.data.lazy_imports.basic_used - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_global_off(self): - try: - import test.test_import.data.lazy_imports.global_off - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_global_on(self): - try: - import test.test_import.data.lazy_imports.global_on - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_global_filter(self): - try: - import test.test_import.data.lazy_imports.global_filter - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_global_filter_true(self): - try: - import test.test_import.data.lazy_imports.global_filter_true - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_global_filter_from(self): - try: - import test.test_import.data.lazy_imports.global_filter - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_global_filter_from_true(self): - try: - import test.test_import.data.lazy_imports.global_filter_true - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_compatibility_mode(self): - try: - import test.test_import.data.lazy_imports.basic_compatibility_mode - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_compatibility_mode_used(self): - try: - import test.test_import.data.lazy_imports.basic_compatibility_mode_used - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_compatibility_mode_func(self): - try: - import test.test_import.data.lazy_imports.compatibility_mode_func - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_compatibility_mode_try_except(self): - try: - import test.test_import.data.lazy_imports.compatibility_mode_try_except - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_compatibility_mode_relative(self): - try: - import test.test_import.data.lazy_imports.basic_compatibility_mode_relative - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_modules_dict(self): - try: - import test.test_import.data.lazy_imports.modules_dict - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_modules_geatattr(self): - try: - import test.test_import.data.lazy_imports.modules_getattr - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_modules_geatattr_other(self): - try: - import test.test_import.data.lazy_imports.modules_getattr_other - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_lazy_value_get(self): - try: - import test.test_import.data.lazy_imports.lazy_get_value - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_lazy_try_except(self): - with self.assertRaises(SyntaxError): - import test.test_import.data.lazy_imports.lazy_try_except - - def test_lazy_try_except_from(self): - with self.assertRaises(SyntaxError): - import test.test_import.data.lazy_imports.lazy_try_except_from - - def test_lazy_try_except_from_star(self): - with self.assertRaises(SyntaxError): - import test.test_import.data.lazy_imports.lazy_try_except_from_star - - def test_try_except_eager(self): - sys.set_lazy_imports(True) - try: - import test.test_import.data.lazy_imports.try_except_eager - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_try_except_eager_from(self): - sys.set_lazy_imports(True) - try: - import test.test_import.data.lazy_imports.try_except_eager_from - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_lazy_with(self): - try: - import test.test_import.data.lazy_imports.lazy_with - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_lazy_with_from(self): - try: - import test.test_import.data.lazy_imports.lazy_with_from - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_lazy_import_func(self): - with self.assertRaises(SyntaxError): - import test.test_import.data.lazy_imports.lazy_import_func - - def test_eager_import_func(self): - sys.set_lazy_imports(True) - try: - import test.test_import.data.lazy_imports.eager_import_func - except ImportError as e: - self.fail('lazy import failed') - - f = test.test_import.data.lazy_imports.eager_import_func.f - self.assertEqual(type(f()), type(sys)) - - def test_lazy_import_pkg(self): - try: - import test.test_import.data.lazy_imports.lazy_import_pkg - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.pkg" in sys.modules) - self.assertTrue("test.test_import.data.lazy_imports.pkg.bar" in sys.modules) - - def test_lazy_import_pkg_cross_import(self): - try: - import test.test_import.data.lazy_imports.pkg.c - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.pkg" in sys.modules) - self.assertTrue("test.test_import.data.lazy_imports.pkg.c" in sys.modules) - self.assertFalse("test.test_import.data.lazy_imports.pkg.b" in 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) - - def test_dunder_lazy_import(self): - try: - import test.test_import.data.lazy_imports.dunder_lazy_import - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_dunder_lazy_import_used(self): - try: - import test.test_import.data.lazy_imports.dunder_lazy_import_used - except ImportError as e: - self.fail('lazy import failed') - - self.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) - - def test_dunder_lazy_import_builtins(self): - """__lazy_import__ uses modules __builtins__ to get __import__""" - try: - from test.test_import.data.lazy_imports import dunder_lazy_import_builtins - except ImportError as e: - self.fail('lazy import failed') - - self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) - self.assertEqual(dunder_lazy_import_builtins.basic, 42) - - class TestSinglePhaseSnapshot(ModuleSnapshot): """A representation of a single-phase init module for testing. 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 index aaba46fa9d7..4881b30fb02 100644 --- a/Lib/test/test_import/data/lazy_imports/global_filter_true.py +++ b/Lib/test/test_import/data/lazy_imports/global_filter_true.py @@ -5,7 +5,7 @@ def filter(module_name, imported_name, from_list): assert imported_name == "test.test_import.data.lazy_imports.basic2" return True -sys.set_lazy_imports(None) +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 index 6ca986e9420..4f202744a9e 100644 --- a/Lib/test/test_import/data/lazy_imports/global_off.py +++ b/Lib/test/test_import/data/lazy_imports/global_off.py @@ -1,5 +1,5 @@ import sys -sys.set_lazy_imports(False) +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 index f6df882ceca..3f8e1d2aa01 100644 --- a/Lib/test/test_import/data/lazy_imports/global_on.py +++ b/Lib/test/test_import/data/lazy_imports/global_on.py @@ -1,5 +1,5 @@ import sys -sys.set_lazy_imports(True) +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/lazy_future_import.py b/Lib/test/test_import/data/lazy_imports/lazy_future_import.py new file mode 100644 index 00000000000..8bd258b76b4 --- /dev/null +++ b/Lib/test/test_import/data/lazy_imports/lazy_future_import.py @@ -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 index 60e8ab79259..0ff572fa1e3 100644 --- a/Lib/test/test_import/data/lazy_imports/lazy_get_value.py +++ b/Lib/test/test_import/data/lazy_imports/lazy_get_value.py @@ -2,6 +2,6 @@ def f(): x = globals() - return x['basic2'].get() + return x['basic2'].resolve() f() diff --git a/Lib/test/test_import/test_lazy_imports.py b/Lib/test/test_import/test_lazy_imports.py new file mode 100644 index 00000000000..98f22e8bb8a --- /dev/null +++ b/Lib/test/test_import/test_lazy_imports.py @@ -0,0 +1,524 @@ +"""Tests for PEP 810 lazy imports.""" + +import sys +import types +import unittest +import threading +import textwrap +import subprocess +from test.support import import_helper + + +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") + + 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.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.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.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) + x = basic_unused.test.test_import.data.lazy_imports.basic2 + self.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) + x = basic_from_unused.basic2 + self.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) + + def test_global_on(self): + """Mode 'all' should make regular imports lazy.""" + import test.test_import.data.lazy_imports.global_on + self.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) + + def test_global_filter(self): + """Filter returning False should prevent lazy loading.""" + import test.test_import.data.lazy_imports.global_filter + self.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) + + def test_global_filter_from(self): + """Filter should work with 'from' imports.""" + import test.test_import.data.lazy_imports.global_filter + self.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in sys.modules) + + def test_lazy_import_type_exposed(self): + """LazyImportType should be exposed in types module.""" + self.assertTrue(hasattr(types, 'LazyImportType')) + self.assertEqual(types.LazyImportType.__name__, 'lazy_import') + + +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): + import test.test_import.data.lazy_imports.lazy_future_import + + 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 + + +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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.pkg" in sys.modules) + self.assertTrue("test.test_import.data.lazy_imports.pkg.bar" in 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.assertTrue("test.test_import.data.lazy_imports.pkg" in sys.modules) + self.assertTrue("test.test_import.data.lazy_imports.pkg.c" in sys.modules) + self.assertFalse("test.test_import.data.lazy_imports.pkg.b" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in 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.assertTrue("test.test_import.data.lazy_imports.basic2" in 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.assertFalse("test.test_import.data.lazy_imports.basic2" in sys.modules) + self.assertEqual(dunder_lazy_import_builtins.basic, 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_get_lazy_modules_returns_set(self): + """get_lazy_modules should return a set per PEP 810.""" + result = sys.get_lazy_modules() + self.assertIsInstance(result, set) + + def test_lazy_modules_attribute_is_set(self): + """sys.lazy_modules should be a set per PEP 810.""" + self.assertIsInstance(sys.lazy_modules, set) + self.assertIs(sys.lazy_modules, sys.get_lazy_modules()) + + 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.basic2" in sys.lazy_modules + 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 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) + + +if __name__ == '__main__': + unittest.main() diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 20e4d833a9d..0e7cc8098bc 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -131,20 +131,20 @@ _PyLazyImport_GetName(PyObject *lazy_import) } static PyObject * -lazy_get(PyObject *self, PyObject *args) +lazy_resolve(PyObject *self, PyObject *args) { return _PyImport_LoadLazyImportTstate(PyThreadState_GET(), self); } static PyMethodDef lazy_methods[] = { - {"get", lazy_get, METH_NOARGS, PyDoc_STR("gets the value that the lazy function references")}, + {"resolve", lazy_resolve, METH_NOARGS, PyDoc_STR("resolves the lazy import and returns the actual object")}, {0} }; PyTypeObject PyLazyImport_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) - "LazyImport", /* tp_name */ + "lazy_import", /* tp_name */ sizeof(PyLazyImportObject), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)lazy_import_dealloc, /* tp_dealloc */ diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index 81dc03a267f..e773066fcbf 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1946,6 +1946,10 @@ _PyPegen_checked_future_import(Parser *p, identifier module, asdl_alias_seq * na int is_lazy, int lineno, int col_offset, int end_lineno, int end_col_offset, PyArena *arena) { if (level == 0 && PyUnicode_CompareWithASCIIString(module, "__future__") == 0) { + if (is_lazy) { + RAISE_SYNTAX_ERROR("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) { diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index e5ab7237075..49ec484355e 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -1907,23 +1907,24 @@ sys_get_lazy_imports_filter(PyObject *module, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(sys_set_lazy_imports__doc__, -"set_lazy_imports($module, /, enabled)\n" +"set_lazy_imports($module, /, mode)\n" "--\n" "\n" -"Sets the global lazy imports flag.\n" +"Sets the global lazy imports mode.\n" "\n" -"True sets all imports at the top level as potentially lazy.\n" -"False disables lazy imports for any explicitly marked imports.\n" -"None causes only explicitly marked imports as lazy.\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" +"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 *enabled); +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) @@ -1940,7 +1941,7 @@ sys_set_lazy_imports(PyObject *module, PyObject *const *args, Py_ssize_t nargs, } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(enabled), }, + .ob_item = { &_Py_ID(mode), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1949,7 +1950,7 @@ sys_set_lazy_imports(PyObject *module, PyObject *const *args, Py_ssize_t nargs, # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"enabled", NULL}; + static const char * const _keywords[] = {"mode", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "set_lazy_imports", @@ -1957,15 +1958,15 @@ sys_set_lazy_imports(PyObject *module, PyObject *const *args, Py_ssize_t nargs, }; #undef KWTUPLE PyObject *argsbuf[1]; - PyObject *enabled; + PyObject *mode; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!args) { goto exit; } - enabled = args[0]; - return_value = sys_set_lazy_imports_impl(module, enabled); + mode = args[0]; + return_value = sys_set_lazy_imports_impl(module, mode); exit: return return_value; @@ -1975,11 +1976,11 @@ PyDoc_STRVAR(sys_get_lazy_imports__doc__, "get_lazy_imports($module, /)\n" "--\n" "\n" -"Gets the global lazy imports flag.\n" +"Gets the global lazy imports mode.\n" "\n" -"Returns True if all top level imports are potentially lazy.\n" -"Returns False if all explicilty marked lazy imports are suppressed.\n" -"Returns None if only explicitly marked imports are lazy."); +"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__}, @@ -1993,6 +1994,28 @@ sys_get_lazy_imports(PyObject *module, PyObject *Py_UNUSED(ignored)) return sys_get_lazy_imports_impl(module); } +PyDoc_STRVAR(sys_get_lazy_modules__doc__, +"get_lazy_modules($module, /)\n" +"--\n" +"\n" +"Gets the set of module names that have been lazily imported.\n" +"\n" +"Returns a set of fully-qualified module names that have been lazily imported\n" +"at some point (primarily for diagnostics and introspection). Note that modules\n" +"are removed from this set when they are reified (actually loaded)."); + +#define SYS_GET_LAZY_MODULES_METHODDEF \ + {"get_lazy_modules", (PyCFunction)sys_get_lazy_modules, METH_NOARGS, sys_get_lazy_modules__doc__}, + +static PyObject * +sys_get_lazy_modules_impl(PyObject *module); + +static PyObject * +sys_get_lazy_modules(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return sys_get_lazy_modules_impl(module); +} + PyDoc_STRVAR(_jit_is_available__doc__, "is_available($module, /)\n" "--\n" @@ -2120,4 +2143,4 @@ exit: #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=dd304e713c0d089f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=cec3ca2ba0ad32cc input=a9049054013a1b77]*/ diff --git a/Python/import.c b/Python/import.c index 2712428b733..9e815d13256 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4267,10 +4267,26 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate, } PyObject *res = _PyLazyImport_New(builtins, abs_name, fromlist); + if (res == NULL) { + Py_DECREF(abs_name); + return NULL; + } if (register_lazy_on_parent(tstate, abs_name, builtins) < 0) { Py_DECREF(res); - res = NULL; + Py_DECREF(abs_name); + return NULL; } + + // Add the module name to sys.lazy_modules set (PEP 810) + PyObject *lazy_modules_set = interp->imports.lazy_modules_set; + if (lazy_modules_set != NULL) { + if (PySet_Add(lazy_modules_set, abs_name) < 0) { + Py_DECREF(res); + Py_DECREF(abs_name); + return NULL; + } + } + Py_DECREF(abs_name); return res; } @@ -4482,6 +4498,9 @@ _PyImport_ClearCore(PyInterpreterState *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_modules_set); + Py_CLEAR(interp->imports.lazy_importing_modules); } void diff --git a/Python/initconfig.c b/Python/initconfig.c index 6fc015ad0f3..38bfd06f203 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -318,7 +318,7 @@ 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=[on|off|default]: control global lazy imports; default is auto;\n\ +-X lazy_imports=[all|none|normal]: control global lazy imports; default is normal;\n\ 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\ @@ -2298,36 +2298,36 @@ config_init_lazy_imports(PyConfig *config) const char *env = config_get_env(config, "PYTHON_LAZY_IMPORTS"); if (env) { - if (strcmp(env, "on") == 0) { + if (strcmp(env, "all") == 0) { lazy_imports = 1; } - else if (strcmp(env, "off") == 0) { + else if (strcmp(env, "none") == 0) { lazy_imports = 0; } - else if (strcmp(env, "default") == 0) { + else if (strcmp(env, "normal") == 0) { lazy_imports = -1; } else { return _PyStatus_ERR("PYTHON_LAZY_IMPORTS: invalid value; " - "expected 'on', 'off', or 'default'"); + "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"on") == 0) { + if (wcscmp(x_value, L"all") == 0) { lazy_imports = 1; } - else if (wcscmp(x_value, L"off") == 0) { + else if (wcscmp(x_value, L"none") == 0) { lazy_imports = 0; } - else if (wcscmp(x_value, L"default") == 0) { + else if (wcscmp(x_value, L"normal") == 0) { lazy_imports = -1; } else { return _PyStatus_ERR("-X lazy_imports: invalid value; " - "expected 'on', 'off', or 'default'"); + "expected 'all', 'none', or 'normal'"); } config->lazy_imports = lazy_imports; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index e473901bcb3..25d87324efe 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2821,36 +2821,41 @@ sys_get_lazy_imports_filter_impl(PyObject *module) /*[clinic input] sys.set_lazy_imports - enabled: object + mode: object -Sets the global lazy imports flag. +Sets the global lazy imports mode. -True sets all imports at the top level as potentially lazy. -False disables lazy imports for any explicitly marked imports. -None causes only explicitly marked imports as lazy. +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 +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 *enabled) -/*[clinic end generated code: output=d601640d3e2d70fb input=d351054b5884eae5]*/ +sys_set_lazy_imports_impl(PyObject *module, PyObject *mode) +/*[clinic end generated code: output=1ff34ba6c4feaf73 input=f04e70d8bf9fe4f6]*/ { - PyImport_LazyImportsMode mode; - if (enabled == Py_None) { - mode = PyImport_LAZY_NORMAL; - } else if (enabled == Py_False) { - mode = PyImport_LAZY_NONE; - } else if (enabled == Py_True) { - mode = PyImport_LAZY_ALL; + 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, "expected None, True or False for enabled mode"); + PyErr_SetString(PyExc_ValueError, "mode must be 'normal', 'all', or 'none'"); return NULL; } - if (PyImport_SetLazyImportsMode(mode)) { + if (PyImport_SetLazyImportsMode(lazy_mode)) { return NULL; } Py_RETURN_NONE; @@ -2859,31 +2864,54 @@ sys_set_lazy_imports_impl(PyObject *module, PyObject *enabled) /*[clinic input] sys.get_lazy_imports -Gets the global lazy imports flag. +Gets the global lazy imports mode. -Returns True if all top level imports are potentially lazy. -Returns False if all explicilty marked lazy imports are suppressed. -Returns None if only explicitly marked imports are lazy. +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=d7b25d814165c8ce]*/ +/*[clinic end generated code: output=4147dec48c51ae99 input=8cb574f1e4e3003c]*/ { switch (PyImport_GetLazyImportsMode()) { case PyImport_LAZY_NORMAL: - Py_RETURN_NONE; + return PyUnicode_FromString("normal"); case PyImport_LAZY_ALL: - Py_RETURN_TRUE; + return PyUnicode_FromString("all"); case PyImport_LAZY_NONE: - Py_RETURN_FALSE; + return PyUnicode_FromString("none"); default: PyErr_SetString(PyExc_RuntimeError, "unknown lazy imports mode"); return NULL; } } +/*[clinic input] +sys.get_lazy_modules + +Gets the set of module names that have been lazily imported. + +Returns a set of fully-qualified module names that have been lazily imported +at some point (primarily for diagnostics and introspection). Note that modules +are removed from this set when they are reified (actually loaded). + +[clinic start generated code]*/ + +static PyObject * +sys_get_lazy_modules_impl(PyObject *module) +/*[clinic end generated code: output=4c641f8881ba87c0 input=06c7a1d05bcfa36a]*/ +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + PyObject *lazy_modules_set = interp->imports.lazy_modules_set; + if (lazy_modules_set == NULL) { + return PySet_New(NULL); + } + return Py_NewRef(lazy_modules_set); +} + static PyMethodDef sys_methods[] = { /* Might as well keep this in alphabetic order */ SYS_ADDAUDITHOOK_METHODDEF @@ -2952,6 +2980,7 @@ static PyMethodDef sys_methods[] = { SYS_SET_LAZY_IMPORTS_METHODDEF SYS_GET_LAZY_IMPORTS_FILTER_METHODDEF SYS_SET_LAZY_IMPORTS_FILTER_METHODDEF + SYS_GET_LAZY_MODULES_METHODDEF SYS__BASEREPL_METHODDEF #ifdef Py_STATS SYS__STATS_ON_METHODDEF @@ -4065,6 +4094,14 @@ _PySys_InitCore(PyThreadState *tstate, PyObject *sysdict) SET_SYS("path_importer_cache", PyDict_New()); SET_SYS("path_hooks", PyList_New(0)); + /* adding sys.lazy_modules set (PEP 810) */ + PyObject *lazy_modules_set = PySet_New(NULL); + if (lazy_modules_set == NULL) { + goto err_occurred; + } + interp->imports.lazy_modules_set = lazy_modules_set; + SET_SYS("lazy_modules", Py_NewRef(lazy_modules_set)); + if (_PyErr_Occurred(tstate)) { goto err_occurred; }