From be300284d5a556304b97e400e44df3f9693c92cc Mon Sep 17 00:00:00 2001 From: Wouter Zwerink Date: Sun, 3 Aug 2025 00:30:28 +0200 Subject: [PATCH 1/3] Treat kwarg_only as OptionInfo --- tests/test_ambiguous_params.py | 18 ++++++++++++++++++ typer/main.py | 5 ++++- typer/models.py | 2 ++ typer/utils.py | 2 +- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py index 0693c8e9aa..e15bab1071 100644 --- a/tests/test_ambiguous_params.py +++ b/tests/test_ambiguous_params.py @@ -229,3 +229,21 @@ def cmd( ) def test_error_rendering(error, message): assert str(error) == message + + +def test_keyword_only_without_default_becomes_option(): + app = typer.Typer() + + @app.command() + def cmd(*, my_param): + print(my_param) + + # Check that it works with the keyword + result = runner.invoke(app, ["--my-param", "hello"]) + assert result.exit_code == 0, result.output + assert "hello" in result.output + + # Check that it fails without the keyword + result = runner.invoke(app, ["hello"]) + assert result.exit_code != 0, result.output + assert "Missing option" in result.output, result.output diff --git a/typer/main.py b/typer/main.py index 59e22c77aa..c4fff79e1b 100644 --- a/typer/main.py +++ b/typer/main.py @@ -823,7 +823,10 @@ def get_click_param( default_value = parameter_info.default elif param.default == Required or param.default is param.empty: required = True - parameter_info = ArgumentInfo() + if param.kind == inspect.Parameter.KEYWORD_ONLY: + parameter_info = OptionInfo() + else: + parameter_info = ArgumentInfo() else: default_value = param.default parameter_info = OptionInfo() diff --git a/typer/models.py b/typer/models.py index e0bddb965b..c2ac4b5775 100644 --- a/typer/models.py +++ b/typer/models.py @@ -514,10 +514,12 @@ def __init__( name: str, default: Any = inspect.Parameter.empty, annotation: Any = inspect.Parameter.empty, + kind: inspect.Parameter = inspect.Parameter.POSITIONAL_OR_KEYWORD, ) -> None: self.name = name self.default = default self.annotation = annotation + self.kind = kind class DeveloperExceptionConfig: diff --git a/typer/utils.py b/typer/utils.py index 81dc4dd61d..091bd8cf7c 100644 --- a/typer/utils.py +++ b/typer/utils.py @@ -185,6 +185,6 @@ def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: default = parameter_info params[param.name] = ParamMeta( - name=param.name, default=default, annotation=annotation + name=param.name, default=default, annotation=annotation, kind=param.kind ) return params From c17b63e015e20f118c94b2d5ef2fedb1a0d82d03 Mon Sep 17 00:00:00 2001 From: Wouter Zwerink Date: Sat, 9 Aug 2025 01:04:01 +0200 Subject: [PATCH 2/3] Appease MyPy --- typer/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/models.py b/typer/models.py index c2ac4b5775..6cbc5d9cc9 100644 --- a/typer/models.py +++ b/typer/models.py @@ -514,7 +514,7 @@ def __init__( name: str, default: Any = inspect.Parameter.empty, annotation: Any = inspect.Parameter.empty, - kind: inspect.Parameter = inspect.Parameter.POSITIONAL_OR_KEYWORD, + kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD ) -> None: self.name = name self.default = default From 5462ee968789a8fb2330a18a2f5b81bcd4a90e09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:04:10 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/models.py b/typer/models.py index 6cbc5d9cc9..ee1ffa2d4c 100644 --- a/typer/models.py +++ b/typer/models.py @@ -514,7 +514,7 @@ def __init__( name: str, default: Any = inspect.Parameter.empty, annotation: Any = inspect.Parameter.empty, - kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD + kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD, ) -> None: self.name = name self.default = default