Skip to content

Commit c5ead2e

Browse files
committed
Added new ActionFail for arguments that should fail parsing with a given error message (#335).
1 parent 0177681 commit c5ead2e

File tree

5 files changed

+83
-3
lines changed

5 files changed

+83
-3
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Added
1919
^^^^^
2020
- Support for Python 3.14 (`#753
2121
<https://github.com/omni-us/jsonargparse/pull/753>`__).
22+
- New ``ActionFail`` for arguments that should fail parsing with a given error
23+
message (`#759 <https://github.com/omni-us/jsonargparse/pull/759>`__).
2224

2325
Fixed
2426
^^^^^

DOCUMENTATION.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,37 @@ a function, it is recommended to implement a type, see :ref:`custom-types`.
398398
parser.add_argument("--int_or_off", type=int_or_off)
399399

400400

401+
Always fail arguments
402+
---------------------
403+
404+
In scenarios where an argument should be included in the parser but should
405+
always fail parsing, there is the :class:`.ActionFail` action. This is
406+
particularly useful when a feature is optional and only accessible if a specific
407+
package is installed:
408+
409+
.. testsetup:: always-fail
410+
411+
parser = ArgumentParser()
412+
some_package_installed = False
413+
414+
.. testcode:: always-fail
415+
416+
from jsonargparse import ActionFail
417+
418+
if some_package_installed:
419+
parser.add_argument("--module", type=SomeClass)
420+
else:
421+
parser.add_argument(
422+
"--module",
423+
action=ActionFail(message="install 'package' to enable"),
424+
help="Option unavailable due to missing 'package'",
425+
)
426+
427+
With this setup, if the argument is provided as ``--module=...`` or in a nested
428+
form like ``--module.child=...``, the parsing will fail and display the
429+
configured error message.
430+
431+
401432
.. _type-hints:
402433

403434
Type hints

jsonargparse/_actions.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929

3030
__all__ = [
3131
"ActionConfigFile",
32-
"ActionYesNo",
32+
"ActionFail",
3333
"ActionParser",
34+
"ActionYesNo",
3435
]
3536

3637

@@ -65,7 +66,7 @@ def _find_action_and_subcommand(
6566
fallback_action = None
6667
for action in actions:
6768
if action.dest == dest or f"--{dest}" in action.option_strings:
68-
if isinstance(action, _ActionConfigLoad):
69+
if isinstance(action, (_ActionConfigLoad, ActionFail)):
6970
fallback_action = action
7071
else:
7172
return action, None
@@ -435,6 +436,30 @@ def get_args_after_opt(self, args):
435436
return args[num + 1 :]
436437

437438

439+
class ActionFail(Action):
440+
"""Action that always fails parsing with a given error."""
441+
442+
def __init__(self, message: str = "option unavailable", **kwargs):
443+
"""Initializer for ActionFail instance.
444+
445+
Args:
446+
message: Text for the error to show.
447+
"""
448+
if len(kwargs) == 0:
449+
self._message = message
450+
else:
451+
self._message = kwargs.pop("_message")
452+
kwargs["default"] = SUPPRESS
453+
super().__init__(**kwargs)
454+
455+
def __call__(self, *args, **kwargs):
456+
"""Always fails with given message."""
457+
if len(args) == 0:
458+
kwargs["_message"] = self._message
459+
return ActionFail(**kwargs)
460+
args[0].error(self._message)
461+
462+
438463
class ActionYesNo(Action):
439464
"""Paired options --[yes_prefix]opt, --[no_prefix]opt to set True or False respectively."""
440465

jsonargparse/_typehints.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from ._actions import (
4040
Action,
4141
ActionConfigFile,
42+
ActionFail,
4243
_ActionHelpClassPath,
4344
_ActionPrintConfig,
4445
_find_action,
@@ -405,7 +406,7 @@ def parse_argv_item(arg_string):
405406
action = _find_parent_action(parser, arg_base[2:])
406407

407408
typehint = typehint_from_action(action)
408-
if typehint:
409+
if typehint or isinstance(action, ActionFail):
409410
if parse_optional_num_return == 4:
410411
return action, arg_base, sep, explicit_arg
411412
elif parse_optional_num_return == 1:

jsonargparse_tests/test_actions.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from jsonargparse import (
8+
ActionFail,
89
ActionParser,
910
ActionYesNo,
1011
ArgumentError,
@@ -60,6 +61,26 @@ def test_action_config_file_argument_errors(parser, tmp_cwd):
6061
pytest.raises(ArgumentError, lambda: parser.parse_args(["--cfg", '{"k":"v"}']))
6162

6263

64+
# ActionFail tests
65+
66+
67+
def test_action_fail(parser):
68+
parser.add_argument(
69+
"--unavailable",
70+
action=ActionFail(message="needs package xyz"),
71+
help="Option not available due to missing package xyz",
72+
)
73+
help_str = get_parser_help(parser)
74+
assert "--unavailable" in help_str
75+
assert "Option not available due to missing package xyz" in help_str
76+
defaults = parser.get_defaults()
77+
assert "unavailable" not in defaults
78+
with pytest.raises(ArgumentError, match="needs package xyz"):
79+
parser.parse_args(["--unavailable=x"])
80+
with pytest.raises(ArgumentError, match="needs package xyz"):
81+
parser.parse_args(["--unavailable.child=x"])
82+
83+
6384
# ActionYesNo tests
6485

6586

0 commit comments

Comments
 (0)