From d8894e2e9849fedcddc46b5ebf9e13f5c468c6f8 Mon Sep 17 00:00:00 2001 From: Aditya Jain Date: Wed, 12 Aug 2026 18:32:17 -0700 Subject: [PATCH] Defer docutils imports until help is rendered ``awscli.help`` imported ``docutils.core`` and the html4css1/manpage writers at module scope, and ``awscli.topictags`` imported ``docutils.core``. Because ``awscli.customizations.commands`` subclasses ``HelpCommand``, that chain was pulled in during customization registration on every CLI invocation -- including ``pygments`` and ``PIL`` via the docutils rst directives -- even for commands that never render help. All of the uses are inside methods, so the imports move into them. Measured with 25 subprocess runs of ``aws --version`` per arm, alternating between arms three times to control for drift: before min 246-255ms after min 232-240ms so roughly 14ms (~6%) off every invocation. ``aws help``, ``aws s3 help``, ``aws ec2 describe-instances help`` and ``aws help topics`` were checked by hand, and ``TopicTagDB.scan`` still parses topic files. Co-Authored-By: Claude Opus 5 --- .../next-release/enhancement-startup-22805.json | 5 +++++ awscli/clidriver.py | 13 ++++++++----- awscli/help.py | 16 +++++++++++----- awscli/topictags.py | 7 ++++--- 4 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 .changes/next-release/enhancement-startup-22805.json diff --git a/.changes/next-release/enhancement-startup-22805.json b/.changes/next-release/enhancement-startup-22805.json new file mode 100644 index 000000000000..bc5affc87e5a --- /dev/null +++ b/.changes/next-release/enhancement-startup-22805.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "startup", + "description": "Defer importing ``docutils`` until help output is actually rendered. It was previously imported on every CLI invocation, along with ``pygments`` and ``PIL``, even for commands that never render help." +} diff --git a/awscli/clidriver.py b/awscli/clidriver.py index bc860a4a8bf2..91a37dc36097 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -65,11 +65,6 @@ ) from awscli.formatter import get_formatter from awscli.handlers_registry import MAIN_COMMAND_TABLE_OPS -from awscli.help import ( - OperationHelpCommand, - ProviderHelpCommand, - ServiceHelpCommand, -) from awscli.lazy_emitter import LazyInitEmitter from awscli.logger import ( disable_crt_logging, @@ -534,6 +529,10 @@ def _create_cli_argument(self, option_name, option_params): ) def create_help_command(self): + # Imported here because the help machinery pulls in docutils, + # which is only needed when help is actually requested. + from awscli.help import ProviderHelpCommand + cli_data = self._get_cli_data() return ProviderHelpCommand( self.session, @@ -769,6 +768,8 @@ def _add_lineage(self, command_table): command_obj.lineage = self.lineage + [command_obj] def create_help_command(self): + from awscli.help import ServiceHelpCommand + command_table = self._get_command_table() return ServiceHelpCommand( session=self.session, @@ -964,6 +965,8 @@ def __call__(self, args, parsed_globals): ) def create_help_command(self): + from awscli.help import OperationHelpCommand + return OperationHelpCommand( self._session, operation_model=self._operation_model, diff --git a/awscli/help.py b/awscli/help.py index fa6a3099a41b..7c5b2975df5d 100644 --- a/awscli/help.py +++ b/awscli/help.py @@ -20,11 +20,6 @@ from subprocess import PIPE, Popen from botocore.exceptions import ProfileNotFound -from docutils.core import publish_string -from docutils.writers import ( - html4css1, - manpage, -) from awscli import ( _DEFAULT_BASE_REMOTE_URL, @@ -239,6 +234,9 @@ class PosixHelpRenderer(PosixPagingHelpRenderer): """ def _convert_doc_content(self, contents): + from docutils.core import publish_string + from docutils.writers import manpage + settings_overrides = self._DEFAULT_DOCUTILS_SETTINGS_OVERRIDES.copy() settings_overrides["report_level"] = 3 man_contents = publish_string( @@ -265,6 +263,9 @@ class PosixBrowserHelpRenderer(BrowserHelpRenderer): """ def _convert_doc_content(self, contents): + from docutils.core import publish_string + from docutils.writers import manpage + settings_overrides = self._DEFAULT_DOCUTILS_SETTINGS_OVERRIDES.copy() settings_overrides["report_level"] = 3 man_contents = publish_string( @@ -310,6 +311,8 @@ class WindowsHelpRenderer(WindowsPagingHelpRenderer): """Render help content on a Windows platform.""" def _convert_doc_content(self, contents): + from docutils.core import publish_string + text_output = publish_string( contents, writer=TextWriter(), @@ -322,6 +325,9 @@ class WindowsBrowserHelpRenderer(BrowserHelpRenderer): """Render help content in the browser on a Windows platform.""" def _convert_doc_content(self, contents): + from docutils.core import publish_string + from docutils.writers import html4css1 + text_output = publish_string( contents, writer=html4css1.Writer(), diff --git a/awscli/topictags.py b/awscli/topictags.py index dfd9f5a7f505..6b001ce9f21f 100644 --- a/awscli/topictags.py +++ b/awscli/topictags.py @@ -22,8 +22,6 @@ import json import os -import docutils.core - class TopicTagDB: """This class acts like a database for the tags of all available topics. @@ -182,7 +180,10 @@ def _find_topic_name(self, topic_src_file): def _add_tag_and_values_from_content(self, topic_name, content): # Retrieves tags and values and adds from content of topic file - # to the dictionary. + # to the dictionary. Imported here because docutils is only + # needed when the topic index is (re)generated or queried. + import docutils.core + doctree = docutils.core.publish_doctree(content).asdom() fields = doctree.getElementsByTagName('field') for field in fields: