Skip to content

Commit 2e4ae12

Browse files
authored
Fix regression of required not shown in help (#893)
1 parent edba416 commit 2e4ae12

6 files changed

Lines changed: 72 additions & 3 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ Changed
5656
<https://github.com/omni-us/jsonargparse/pull/887>`__).
5757
- Rely on ``required`` attributes to improve compatibility with third-party
5858
argparse extensions (`#890
59-
<https://github.com/omni-us/jsonargparse/pull/890>`__).
59+
<https://github.com/omni-us/jsonargparse/pull/890>`__, `#893
60+
<https://github.com/omni-us/jsonargparse/pull/893>`__).
6061

6162

6263
v4.47.0 (2026-03-13)

jsonargparse/_core.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
from ._paths import change_to_path_dir
7979
from ._required import (
8080
iter_required_keys,
81+
restore_suppressed_required,
8182
set_required,
8283
suppress_required_actions,
8384
)
@@ -1369,7 +1370,7 @@ def format_help(self) -> str:
13691370
note = f"tried getting defaults considering default_config_files but failed due to: {ex}"
13701371
group = self._default_config_files_group
13711372
group.description = f"{self._default_config_files}, Note: {note}"
1372-
with parser_context(parent_parser=self, defaults_cache=defaults):
1373+
with restore_suppressed_required(), parser_context(parent_parser=self, defaults_cache=defaults):
13731374
help_str = super().format_help()
13741375
return help_str
13751376

jsonargparse/_required.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from argparse import Action, _SubParsersAction
22
from contextlib import contextmanager
3+
from contextvars import ContextVar
34
from typing import Iterator, Optional, Union
45

56
from ._type_checking import ArgumentParser
67

8+
_suppressed_required_actions: ContextVar[tuple[Action, ...]] = ContextVar("_suppressed_required_actions", default=())
9+
710

811
def _iter_required_action_keys(parser: ArgumentParser) -> Iterator[str]:
912
"""Yields required destinations backed by real argparse actions."""
@@ -58,6 +61,7 @@ def _find_exact_action(parser: ArgumentParser, key: str) -> Optional[Action]:
5861
def suppress_required_actions(parser: ArgumentParser):
5962
"""Temporarily disables required enforcement on real argparse actions."""
6063
suppressed = []
64+
previously_suppressed = _suppressed_required_actions.get()
6165
visited = set()
6266

6367
def visit(subparser):
@@ -73,8 +77,23 @@ def visit(subparser):
7377
visit(choice_parser)
7478

7579
visit(parser)
80+
token = _suppressed_required_actions.set(previously_suppressed + tuple(suppressed))
7681
try:
7782
yield
7883
finally:
84+
_suppressed_required_actions.reset(token)
7985
for action in reversed(suppressed):
8086
action.required = True
87+
88+
89+
@contextmanager
90+
def restore_suppressed_required():
91+
"""Temporarily restores required=True for actions suppressed by suppress_required_actions."""
92+
suppressed = _suppressed_required_actions.get()
93+
for action in suppressed:
94+
action.required = True
95+
try:
96+
yield
97+
finally:
98+
for action in suppressed:
99+
action.required = False

jsonargparse_tests/test_formatters.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99

1010
from jsonargparse import ActionParser, ActionYesNo, ArgumentParser
11-
from jsonargparse_tests.conftest import get_parser_help, json_or_yaml_dump
11+
from jsonargparse_tests.conftest import get_parse_args_stdout, get_parser_help, json_or_yaml_dump
1212

1313

1414
@pytest.fixture
@@ -55,6 +55,12 @@ def test_help_required_and_default(parser):
5555
assert "Option v1. (required, default: v1)" in help_str
5656

5757

58+
def test_help_required_preserved_in_parse_args(parser):
59+
parser.add_argument("--v1", type=int, help="Option v1.", required=True)
60+
help_str = get_parse_args_stdout(parser, ["--help"])
61+
assert "Option v1. (required, type: int)" in help_str
62+
63+
5864
def test_help_type_and_null_default(parser):
5965
parser.add_argument("--v2", type=int, help="Option v2.")
6066
help_str = get_parser_help(parser)

jsonargparse_tests/test_signatures.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,41 @@ def test_add_method_parent_classes(parser):
537537
assert added_args == ["m.p2", "m.a1", "m.a2", "m.a3"]
538538

539539

540+
class Model:
541+
pass
542+
543+
544+
class TrainerLike:
545+
def foo(self, model: Model, x: int, y: float = 1.0):
546+
"""Sample extra function.
547+
548+
Args:
549+
model: A model
550+
x: The x
551+
y: The y
552+
"""
553+
554+
555+
@skip_if_docstring_parser_unavailable
556+
def test_add_method_required_argument_in_parse_args_help(parser):
557+
parser.add_method_arguments(TrainerLike, "foo", skip={"model"})
558+
559+
help_str = get_parse_args_stdout(parser, ["--help"])
560+
assert "The x (required, type: int)" in help_str
561+
assert "The y (type: float, default: 1.0)" in help_str
562+
563+
564+
@skip_if_docstring_parser_unavailable
565+
def test_add_method_required_argument_in_subcommand_parse_args_help(parser, subparser):
566+
subparser.description = "Sample extra function:"
567+
subparser.add_method_arguments(TrainerLike, "foo", skip={"model"})
568+
parser.add_subcommands().add_subcommand("foo", subparser)
569+
570+
help_str = get_parse_args_stdout(parser, ["foo", "--help"])
571+
assert "The x (required, type: int)" in help_str
572+
assert "The y (type: float, default: 1.0)" in help_str
573+
574+
540575
# add_function_arguments tests
541576

542577

jsonargparse_tests/test_subclasses.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,6 +1792,13 @@ def test_subclass_required_parameters_missing(parser):
17921792
ctx.match("the following arguments are required: p1, p2")
17931793

17941794

1795+
def test_subclass_help_required_parameters(parser):
1796+
parser.add_argument("--op", type=RequiredParamsMissing)
1797+
help_str = get_parse_args_stdout(parser, [f"--op.help={__name__}.RequiredParamsMissing"])
1798+
assert "(required, type: int)" in help_str
1799+
assert "(required, type: str)" in help_str
1800+
1801+
17951802
def test_subclass_get_defaults_lazy_instance(parser):
17961803
parser.add_argument("--op", type=RequiredParamsMissing, default=lazy_instance(RequiredParamsMissing, p1=1, p2="x"))
17971804
defaults = parser.get_defaults()

0 commit comments

Comments
 (0)