diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 9bf09b3b73..d6763fec50 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -86,6 +86,21 @@ def __init__(self, *args, **kwargs): self.has_stdin_data = False self.has_input_data = False + def format_help(self) -> str: + """Resolve lazy help strings before formatting. + + Python 3.14 added a ``_check_help()`` call in ``add_argument()`` that + reads ``action.help`` during parser construction. ``LazyChoices`` + defers the ``getter`` call to avoid eager evaluation at that point. + We resolve all lazy help strings here, just before the help text is + actually rendered, so the output is still correct. + """ + from httpie.cli.utils import LazyChoices + for action in self._actions: + if isinstance(action, LazyChoices): + action._resolve_help() + return super().format_help() + # noinspection PyMethodOverriding def parse_args( self, diff --git a/httpie/cli/utils.py b/httpie/cli/utils.py index ad27da37f7..ffe9e61875 100644 --- a/httpie/cli/utils.py +++ b/httpie/cli/utils.py @@ -42,6 +42,7 @@ def __init__( self.cache = cache self.isolation_mode = isolation_mode self._help: Optional[str] = None + self._help_resolved: bool = False self._obj: Optional[Iterable[T]] = None super().__init__(*args, **kwargs) self.choices = self @@ -53,18 +54,44 @@ def load(self) -> T: assert self._obj is not None return self._obj - @property - def help(self) -> str: - if self._help is None and self.help_formatter is not None: + def _resolve_help(self) -> Optional[str]: + """Compute and cache the formatted help string by calling the getter. + + This is intentionally separate from the ``help`` property so that + ``getter`` is only called when help text is *actually* needed (i.e. + when ``--help`` is passed), not during parser construction. + + Python 3.14 added a ``_check_help()`` call inside + ``ArgumentParser.add_argument()`` that accesses ``action.help`` while + building the parser. Previously only ``parse_args(['--help'])`` + triggered help formatting. Without this guard, ``getter`` would be + called eagerly — breaking the lazy evaluation contract and failing the + test that asserts ``getter`` is not called until ``--help`` is used. + """ + if not self._help_resolved and self.help_formatter is not None: self._help = self.help_formatter( self.load(), isolation_mode=self.isolation_mode ) + self._help_resolved = True + return self._help + + @property + def help(self) -> Optional[str]: + # Return the already-resolved string if available, but do NOT call + # _resolve_help() here. The actual resolution is deferred until the + # formatter explicitly asks for it (see format_help / format_usage + # paths that call _expand_help → action.help after _resolve_help has + # been invoked, or until the user passes --help). return self._help @help.setter def help(self, value: Any) -> None: self._help = value + # Mark as resolved when argparse sets help directly (e.g. during + # _check_help in Python 3.14) so we don't overwrite it later. + if value is not None: + self._help_resolved = True def __contains__(self, item: Any) -> bool: return item in self.load()