Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Added
<https://github.com/omni-us/jsonargparse/pull/701>`__).
- Resolve parameters completely from stubs when ``inspect.signature`` fails
(`#698 <https://github.com/omni-us/jsonargparse/pull/698>`__).
- Option to enable validation of default values (`#711
<https://github.com/omni-us/jsonargparse/pull/711>`__).

Changed
^^^^^^^
Expand Down
32 changes: 28 additions & 4 deletions jsonargparse/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
reconplogger_support,
typing_extensions_import,
)
from ._type_checking import ArgumentParser
from ._type_checking import ActionsContainer, ArgumentParser

__all__ = [
"LoggerProperty",
Expand All @@ -55,7 +55,7 @@ def __call__(self, class_type: Type[ClassType], *args, **kwargs) -> ClassType:
InstantiatorsDictType = Dict[Tuple[type, bool], InstantiatorCallable]


parent_parser: ContextVar[Optional["ArgumentParser"]] = ContextVar("parent_parser", default=None)
parent_parser: ContextVar[Optional[ArgumentParser]] = ContextVar("parent_parser", default=None)
parser_capture: ContextVar[bool] = ContextVar("parser_capture", default=False)
defaults_cache: ContextVar[Optional[Namespace]] = ContextVar("defaults_cache", default=None)
lenient_check: ContextVar[Union[bool, str]] = ContextVar("lenient_check", default=False)
Expand Down Expand Up @@ -90,22 +90,34 @@ def parser_context(**kwargs):


parsing_settings = dict(
validate_defaults=False,
parse_optionals_as_positionals=False,
)


def set_parsing_settings(*, parse_optionals_as_positionals: Optional[bool] = None) -> None:
def set_parsing_settings(
*,
validate_defaults: Optional[bool] = None,
parse_optionals_as_positionals: Optional[bool] = None,
) -> None:
"""
Modify settings that affect the parsing behavior.

Args:
validate_defaults: Whether default values must be valid according to the
argument type. The default is False, meaning no default validation,
like in argparse.
parse_optionals_as_positionals: [EXPERIMENTAL] If True, the parser will
take extra positional command line arguments as values for optional
arguments. This means that optional arguments can be given by name
--key=value as usual, but also as positional. The extra positionals
are applied to optionals in the order that they were added to the
parser.
parser. By default, this is False.
"""
if isinstance(validate_defaults, bool):
parsing_settings["validate_defaults"] = validate_defaults
elif validate_defaults is not None:
raise ValueError(f"validate_defaults must be a boolean, but got {validate_defaults}.")
if isinstance(parse_optionals_as_positionals, bool):
parsing_settings["parse_optionals_as_positionals"] = parse_optionals_as_positionals
elif parse_optionals_as_positionals is not None:
Expand All @@ -118,6 +130,18 @@ def get_parsing_setting(name: str):
return parsing_settings[name]


def validate_default(container: ActionsContainer, action: argparse.Action):
if not isinstance(action, Action) or not get_parsing_setting("validate_defaults") or action.default is None:
return
try:
with parser_context(parent_parser=container):
default = action.default
action.default = None
action.default = action._check_type_(default)
except Exception as ex:
raise ValueError(f"Default value is not valid: {ex}") from ex


def get_optionals_as_positionals_actions(parser, include_positionals=False):
from jsonargparse._actions import ActionConfigFile, _ActionConfigLoad, filter_default_actions
from jsonargparse._completions import ShtabAction
Expand Down
2 changes: 2 additions & 0 deletions jsonargparse/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
lenient_check,
parser_context,
supports_optionals_as_positionals,
validate_default,
)
from ._completions import (
argcomplete_namespace,
Expand Down Expand Up @@ -151,6 +152,7 @@ def add_argument(self, *args, enable_path: bool = False, **kwargs):
raise ValueError(f'Argument with destination name "{action.dest}" not allowed.')
if action.option_strings == [] and "default" in kwargs:
raise ValueError("Positional arguments not allowed to have a default value.")
validate_default(self, action)
if action.help is None:
action.help = empty_help
if action.required:
Expand Down
22 changes: 22 additions & 0 deletions jsonargparse_tests/test_parsing_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ def test_get_parsing_setting_failure():
get_parsing_setting("unknown_setting")


# validate_defaults


def test_set_validate_defaults_failure():
with pytest.raises(ValueError, match="validate_defaults must be a boolean"):
set_parsing_settings(validate_defaults="invalid")


def test_validate_defaults_success(parser):
set_parsing_settings(validate_defaults=True)

parser.add_argument("--num", type=int, default=1)
parser.add_argument("--untyped", default=2)


def test_validate_defaults_failure(parser):
set_parsing_settings(validate_defaults=True)

with pytest.raises(ValueError, match="Default value is not valid:"):
parser.add_argument("--num", type=int, default="x")


# parse_optionals_as_positionals


Expand Down