From 70de3c71c1079a678b895f54be6e5ae772a7e443 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Thu, 9 Apr 2026 06:58:34 +0200 Subject: [PATCH] Fix required shown in help --- CHANGELOG.rst | 3 ++- jsonargparse/_core.py | 3 ++- jsonargparse/_required.py | 19 +++++++++++++++ jsonargparse_tests/test_formatters.py | 8 +++++- jsonargparse_tests/test_signatures.py | 35 +++++++++++++++++++++++++++ jsonargparse_tests/test_subclasses.py | 7 ++++++ 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9fbbbd93..912bbfd9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -56,7 +56,8 @@ Changed `__). - Rely on ``required`` attributes to improve compatibility with third-party argparse extensions (`#890 - `__). + `__, `#893 + `__). v4.47.0 (2026-03-13) diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 0a0e901a..db7d2ce2 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -78,6 +78,7 @@ from ._paths import change_to_path_dir from ._required import ( iter_required_keys, + restore_suppressed_required, set_required, suppress_required_actions, ) @@ -1369,7 +1370,7 @@ def format_help(self) -> str: note = f"tried getting defaults considering default_config_files but failed due to: {ex}" group = self._default_config_files_group group.description = f"{self._default_config_files}, Note: {note}" - with parser_context(parent_parser=self, defaults_cache=defaults): + with restore_suppressed_required(), parser_context(parent_parser=self, defaults_cache=defaults): help_str = super().format_help() return help_str diff --git a/jsonargparse/_required.py b/jsonargparse/_required.py index 54f0ad3c..618f2966 100644 --- a/jsonargparse/_required.py +++ b/jsonargparse/_required.py @@ -1,9 +1,12 @@ from argparse import Action, _SubParsersAction from contextlib import contextmanager +from contextvars import ContextVar from typing import Iterator, Optional, Union from ._type_checking import ArgumentParser +_suppressed_required_actions: ContextVar[tuple[Action, ...]] = ContextVar("_suppressed_required_actions", default=()) + def _iter_required_action_keys(parser: ArgumentParser) -> Iterator[str]: """Yields required destinations backed by real argparse actions.""" @@ -58,6 +61,7 @@ def _find_exact_action(parser: ArgumentParser, key: str) -> Optional[Action]: def suppress_required_actions(parser: ArgumentParser): """Temporarily disables required enforcement on real argparse actions.""" suppressed = [] + previously_suppressed = _suppressed_required_actions.get() visited = set() def visit(subparser): @@ -73,8 +77,23 @@ def visit(subparser): visit(choice_parser) visit(parser) + token = _suppressed_required_actions.set(previously_suppressed + tuple(suppressed)) try: yield finally: + _suppressed_required_actions.reset(token) for action in reversed(suppressed): action.required = True + + +@contextmanager +def restore_suppressed_required(): + """Temporarily restores required=True for actions suppressed by suppress_required_actions.""" + suppressed = _suppressed_required_actions.get() + for action in suppressed: + action.required = True + try: + yield + finally: + for action in suppressed: + action.required = False diff --git a/jsonargparse_tests/test_formatters.py b/jsonargparse_tests/test_formatters.py index 0145ae4f..8f8c78ac 100644 --- a/jsonargparse_tests/test_formatters.py +++ b/jsonargparse_tests/test_formatters.py @@ -8,7 +8,7 @@ import pytest from jsonargparse import ActionParser, ActionYesNo, ArgumentParser -from jsonargparse_tests.conftest import get_parser_help, json_or_yaml_dump +from jsonargparse_tests.conftest import get_parse_args_stdout, get_parser_help, json_or_yaml_dump @pytest.fixture @@ -55,6 +55,12 @@ def test_help_required_and_default(parser): assert "Option v1. (required, default: v1)" in help_str +def test_help_required_preserved_in_parse_args(parser): + parser.add_argument("--v1", type=int, help="Option v1.", required=True) + help_str = get_parse_args_stdout(parser, ["--help"]) + assert "Option v1. (required, type: int)" in help_str + + def test_help_type_and_null_default(parser): parser.add_argument("--v2", type=int, help="Option v2.") help_str = get_parser_help(parser) diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py index afea9773..58f5a533 100644 --- a/jsonargparse_tests/test_signatures.py +++ b/jsonargparse_tests/test_signatures.py @@ -537,6 +537,41 @@ def test_add_method_parent_classes(parser): assert added_args == ["m.p2", "m.a1", "m.a2", "m.a3"] +class Model: + pass + + +class TrainerLike: + def foo(self, model: Model, x: int, y: float = 1.0): + """Sample extra function. + + Args: + model: A model + x: The x + y: The y + """ + + +@skip_if_docstring_parser_unavailable +def test_add_method_required_argument_in_parse_args_help(parser): + parser.add_method_arguments(TrainerLike, "foo", skip={"model"}) + + help_str = get_parse_args_stdout(parser, ["--help"]) + assert "The x (required, type: int)" in help_str + assert "The y (type: float, default: 1.0)" in help_str + + +@skip_if_docstring_parser_unavailable +def test_add_method_required_argument_in_subcommand_parse_args_help(parser, subparser): + subparser.description = "Sample extra function:" + subparser.add_method_arguments(TrainerLike, "foo", skip={"model"}) + parser.add_subcommands().add_subcommand("foo", subparser) + + help_str = get_parse_args_stdout(parser, ["foo", "--help"]) + assert "The x (required, type: int)" in help_str + assert "The y (type: float, default: 1.0)" in help_str + + # add_function_arguments tests diff --git a/jsonargparse_tests/test_subclasses.py b/jsonargparse_tests/test_subclasses.py index ff0bbd99..3072cbe6 100644 --- a/jsonargparse_tests/test_subclasses.py +++ b/jsonargparse_tests/test_subclasses.py @@ -1792,6 +1792,13 @@ def test_subclass_required_parameters_missing(parser): ctx.match("the following arguments are required: p1, p2") +def test_subclass_help_required_parameters(parser): + parser.add_argument("--op", type=RequiredParamsMissing) + help_str = get_parse_args_stdout(parser, [f"--op.help={__name__}.RequiredParamsMissing"]) + assert "(required, type: int)" in help_str + assert "(required, type: str)" in help_str + + def test_subclass_get_defaults_lazy_instance(parser): parser.add_argument("--op", type=RequiredParamsMissing, default=lazy_instance(RequiredParamsMissing, p1=1, p2="x")) defaults = parser.get_defaults()