From a2c939581de35e312aa1ed479be13abc64837eb4 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Mon, 13 Apr 2026 15:21:43 +0100 Subject: [PATCH 1/4] Add test helper logic to allow for additional commands in lazy import tests --- Lib/test/support/import_helper.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index e8a58ed77061f5..e8a3d176ad6943 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -325,7 +325,7 @@ def ready_to_import(name=None, source=""): sys.modules.pop(name, None) -def ensure_lazy_imports(imported_module, modules_to_block): +def ensure_lazy_imports(imported_module, modules_to_block, *, additional_code=None): """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) @@ -343,6 +343,16 @@ def ensure_lazy_imports(imported_module, modules_to_block): raise AssertionError(f'unexpectedly imported after importing {imported_module}: {{after}}') """ ) + if additional_code: + script += additional_code + script += textwrap.dedent( + f""" + if unexpected := modules_to_block & sys.modules.keys(): + after = ", ".join(unexpected) + raise AssertionError(f'unexpectedly imported after additional code: {{after}}') + """ + ) + from .script_helper import assert_python_ok assert_python_ok("-S", "-c", script) From 629dd53c6b48925a4ce880eee50296fe1ab5a20b Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Mon, 13 Apr 2026 15:22:46 +0100 Subject: [PATCH 2/4] add tests for lazy imports --- Lib/test/test_argparse.py | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index e0c32976fd6f0d..ae67f6893c99af 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -80,6 +80,50 @@ def test_skip_invalid_stdout(self): self.assertRegex(mocked_stderr.getvalue(), r'usage:') +class TestLazyImports(unittest.TestCase): + LAZY_IMPORTS = { + "_colorize", + "copy", + "difflib", + "shutil", + "textwrap", + "warnings", + } + def test_module_import(self): + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS, + ) + + def test_create_parser(self): + # Test imports are still unused after + # creating a parser + create_parser = "argparse.ArgumentParser()" + imported_modules = {"shutil"} + + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS - imported_modules, + additional_code=create_parser, + ) + + def test_add_subparser(self): + # This fails as it currently imports colorize + add_subparser = textwrap.dedent( + f""" + parser = argparse.ArgumentParser() + parser.add_subparsers(dest='command', required=False) + """ + ) + imported_modules = {"shutil"} + + import_helper.ensure_lazy_imports( + "argparse", + self.LAZY_IMPORTS - imported_modules, + additional_code=add_subparser, + ) + + class TestArgumentParserPickleable(unittest.TestCase): @force_not_colorized From 4fa408edda57ffd5b801ddf507b6f8fecb5c812a Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Mon, 13 Apr 2026 15:26:59 +0100 Subject: [PATCH 3/4] Lazily import _colorize This uses a new lazy import so it works with short circuiting alongside a `_colorless_theme` object to prevent the `_colorize` import if color is set to False. _theme and _decolor are now properties to prevent `_set_color` from performing the imports on creation of a formatter. --- Lib/argparse.py | 52 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index d91707d9eec546..47ca78eb568ff6 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -92,6 +92,8 @@ from gettext import gettext as _ from gettext import ngettext +lazy import _colorize + SUPPRESS = '==SUPPRESS==' OPTIONAL = '?' @@ -156,6 +158,15 @@ def _identity(value): # Formatting Help # =============== +class _ColorlessTheme: + # A 'fake' theme for no colors + def __getattr__(self, name): + # _colorize's no_color themes are just all empty strings + # by directly using empty strings the import is avoided + return "" + +_colorless_theme = _ColorlessTheme() + class HelpFormatter(object): """Formatter for generating usage messages and argument help strings. @@ -196,14 +207,32 @@ def __init__( self._set_color(False) def _set_color(self, color, *, file=None): - from _colorize import can_colorize, decolor, get_theme - - if color and can_colorize(file=file): - self._theme = get_theme(force_color=True).argparse - self._decolor = decolor + # Set a new color setting and file, clear caches for theme and decolor + self._theme_color = color + self._theme_file = file + self._cached_theme = None + self._cached_decolor = None + + def _get_theme_and_decolor(self): + # If self._theme_color is false, this prevents _colorize from importing + if self._theme_color and _colorize.can_colorize(file=self._theme_file): + self._cached_theme = _colorize.get_theme(force_color=True).argparse + self._cached_decolor = _colorize.decolor else: - self._theme = get_theme(force_no_color=True).argparse - self._decolor = _identity + self._cached_theme = _colorless_theme + self._cached_decolor = _identity + + @property + def _theme(self): + if self._cached_theme is None: + self._get_theme_and_decolor() + return self._cached_theme + + @property + def _decolor(self): + if self._cached_decolor is None: + self._get_theme_and_decolor() + return self._cached_decolor # =============================== # Section and indentation methods @@ -2856,12 +2885,11 @@ def _print_message(self, message, file=None): pass def _get_theme(self, file=None): - from _colorize import can_colorize, get_theme - - if self.color and can_colorize(file=file): - return get_theme(force_color=True).argparse + # If self.color is False, _colorize is not imported + if self.color and _colorize.can_colorize(file=file): + return _colorize.get_theme(force_color=True).argparse else: - return get_theme(force_no_color=True).argparse + return _colorless_theme # =============== # Exiting methods From dd8dcf9ace683cebe28e5d7b2d03d4fdf37d6a86 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Tue, 21 Apr 2026 13:34:05 +0100 Subject: [PATCH 4/4] subparser has also been fixed --- Lib/test/test_argparse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index ae67f6893c99af..b8ea4d3593a3f2 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -108,7 +108,6 @@ def test_create_parser(self): ) def test_add_subparser(self): - # This fails as it currently imports colorize add_subparser = textwrap.dedent( f""" parser = argparse.ArgumentParser()