diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 857a9cf6..f0271368 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,12 @@ Added - Support for Python 3.14 (`#753 `__). +Fixed +^^^^^ +- Improved parameter kind handling for argument requirement determination. + ``KEYWORD_ONLY`` parameters now correctly use ``--flag`` style (`#756 + `__). + Changed ^^^^^^^ - Removed support for python 3.8 (`#752 diff --git a/jsonargparse/_signatures.py b/jsonargparse/_signatures.py index fa144690..2ab26c57 100644 --- a/jsonargparse/_signatures.py +++ b/jsonargparse/_signatures.py @@ -344,7 +344,22 @@ def _add_signature_parameter( default = None elif get_typehint_origin(annotation) in not_required_types: default = SUPPRESS - is_required = default == inspect_empty + # Determine argument characteristics based on parameter kind and default value + if kind == kinds.POSITIONAL_ONLY: + is_required = True # Always required + is_non_positional = False # Can be positional + elif kind == kinds.POSITIONAL_OR_KEYWORD: + is_required = default == inspect_empty # Required if no default + is_non_positional = False # Can be positional + elif kind == kinds.KEYWORD_ONLY: + is_required = default == inspect_empty # Required if no default + is_non_positional = True # Must use --flag style + elif kind is None: + # programmatically created parameters without kind + is_required = default == inspect_empty # Required if no default + is_non_positional = False # Can be positional (preserve old behavior) + else: + raise RuntimeError(f"The code should never reach here: kind={kind}") # pragma: no cover src = get_parameter_origins(param.component, param.parent) skip_message = f'Skipping parameter "{name}" from "{src}" because of: ' if not fail_untyped and annotation == inspect_empty: @@ -356,7 +371,7 @@ def _add_signature_parameter( default = None is_required = False is_required_link_target = True - if kind in {kinds.VAR_POSITIONAL, kinds.VAR_KEYWORD} or (not is_required and name[0] == "_"): + if not is_required and name[0] == "_": return elif skip and name in skip: self.logger.debug(skip_message + "Parameter requested to be skipped.") @@ -371,12 +386,12 @@ def _add_signature_parameter( kwargs["default"] = default if default is None and not is_optional(annotation, object) and not is_required_link_target: annotation = Optional[annotation] - elif not as_positional: + elif not as_positional or is_non_positional: kwargs["required"] = True is_subclass_typehint = False is_dataclass_like_typehint = is_dataclass_like(annotation) dest = (nested_key + "." if nested_key else "") + name - args = [dest if is_required and as_positional else "--" + dest] + args = [dest if is_required and as_positional and not is_non_positional else "--" + dest] if param.origin: parser = container if not isinstance(container, ArgumentParser): diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py index da706d11..c936ce05 100644 --- a/jsonargparse_tests/test_signatures.py +++ b/jsonargparse_tests/test_signatures.py @@ -697,3 +697,19 @@ def test_add_function_param_conflict(parser): with pytest.raises(ValueError) as ctx: parser.add_function_arguments(func_param_conflict) ctx.match("Unable to add parameter 'cfg' from") + + +def func_positional_and_keyword_only(a: int, /, b: int, *, c: int, d: int = 1): + pass + + +def test_add_function_positional_and_keyword_only_parameters(parser): + parser.add_function_arguments(func_positional_and_keyword_only, as_positional=True) + + # Test that we can parse with both parameters + cfg = parser.parse_args(["1", "2", "--c=3", "--d=4"]) + assert cfg == Namespace(a=1, b=2, c=3, d=4) + with pytest.raises(ArgumentError, match="Unrecognized arguments: --b=2"): + parser.parse_args(["1", "--b=2", "--c=3", "--d=4"]) + with pytest.raises(ArgumentError, match='Key "c" is required'): + parser.parse_args(["1", "2", "--d=4"])