From a55b37e6c76deefc8a96bb4a718171cafd792341 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 16 Apr 2025 13:39:28 -0700 Subject: [PATCH 1/2] gh-118761: Add helper to ensure that lazy imports are actually lazy This ensures that if we jump through some hoops to make sure something is imported lazily, we don't regress on importing it. I recently already accidentally made typing import warnings and annotationlib eagerly. --- Lib/test/support/import_helper.py | 20 ++++++++++++++++++++ Lib/test/test_annotationlib.py | 7 +++++++ Lib/test/test_typing.py | 9 +++++++++ 3 files changed, 36 insertions(+) diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index 2b91bdcf9cd859..5fe745acd1a91c 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -5,6 +5,7 @@ import os import shutil import sys +import textwrap import unittest import warnings @@ -309,3 +310,22 @@ def ready_to_import(name=None, source=""): sys.modules[name] = old_module else: sys.modules.pop(name, None) + + +def ensure_lazy_imports(imported_module, modules_to_block): + """Test that when imported_module is imported, none of the modules in + modules_to_block are imported as a side effect.""" + script = textwrap.dedent( + f""" + import sys + modules_to_block = {modules_to_block} + for mod in modules_to_block: + assert mod not in sys.modules, f"{{mod}} was imported at startup" + + import {imported_module} + for mod in modules_to_block: + assert mod not in sys.modules, f"{{mod}} was imported after importing {imported_module}" + """ + ) + from .script_helper import assert_python_ok + assert_python_ok("-S", "-c", script) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 6f097c07295f3b..f72a5e913b102a 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -24,6 +24,7 @@ ) from test import support +from test.support import import_helper from test.test_inspect import inspect_stock_annotations from test.test_inspect import inspect_stringized_annotations from test.test_inspect import inspect_stringized_annotations_2 @@ -1367,3 +1368,9 @@ def test_multiple_ways_to_create(self): class TestAnnotationLib(unittest.TestCase): def test__all__(self): support.check__all__(self, annotationlib) + + def test_lazy_imports(self): + import_helper.ensure_lazy_imports("annotationlib", [ + "typing", + "warnings", + ]) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 32f12a3f8b22f1..a5fafd8070536e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6317,6 +6317,15 @@ def test_collect_parameters(self): typing._collect_parameters self.assertEqual(cm.filename, __file__) + def test_lazy_import(self): + import_helper.ensure_lazy_imports("typing", [ + "warnings", + "inspect", + "re", + "contextlib", + # "annotationlib", # TODO + ]) + @lru_cache() def cached_func(x, y): From a77577e3747bc91f4ca530b4cee2cb6b054a3630 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 16 Apr 2025 20:21:57 -0700 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Lib/test/support/import_helper.py | 11 +++++++---- Lib/test/test_annotationlib.py | 4 ++-- Lib/test/test_typing.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index 5fe745acd1a91c..42cfe9cfa8cb72 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -315,16 +315,19 @@ def ready_to_import(name=None, source=""): def ensure_lazy_imports(imported_module, modules_to_block): """Test that when imported_module is imported, none of the modules in modules_to_block are imported as a side effect.""" + modules_to_block = frozenset(modules_to_block) script = textwrap.dedent( f""" import sys modules_to_block = {modules_to_block} - for mod in modules_to_block: - assert mod not in sys.modules, f"{{mod}} was imported at startup" + if unexpected := modules_to_block & sys.modules.keys(): + startup = ", ".join(unexpected) + raise AssertionError(f'unexpectedly imported at startup: {{startup}}') import {imported_module} - for mod in modules_to_block: - assert mod not in sys.modules, f"{{mod}} was imported after importing {imported_module}" + if unexpected := modules_to_block & sys.modules.keys(): + after = ", ".join(unexpected) + raise AssertionError(f'unexpectedly imported after importing {imported_module}: {{after}}') """ ) from .script_helper import assert_python_ok diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index f72a5e913b102a..0890be529a7e52 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1370,7 +1370,7 @@ def test__all__(self): support.check__all__(self, annotationlib) def test_lazy_imports(self): - import_helper.ensure_lazy_imports("annotationlib", [ + import_helper.ensure_lazy_imports("annotationlib", { "typing", "warnings", - ]) + }) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a5fafd8070536e..81474a81be645d 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6318,13 +6318,13 @@ def test_collect_parameters(self): self.assertEqual(cm.filename, __file__) def test_lazy_import(self): - import_helper.ensure_lazy_imports("typing", [ + import_helper.ensure_lazy_imports("typing", { "warnings", "inspect", "re", "contextlib", # "annotationlib", # TODO - ]) + }) @lru_cache()