Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions httpie/cli/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 30 additions & 3 deletions httpie/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Loading