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 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) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index e0c32976fd6f0d..b8ea4d3593a3f2 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -80,6 +80,49 @@ 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): + 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