diff --git a/.env b/.env new file mode 100644 index 0000000000..aa298973cb --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# Environment variables automatically read by VS Code, e.g. running tests + +# For tests, a large terminal width +TERMINAL_WIDTH=3000 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2cdd6130e..a9453f8ca4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,6 +57,7 @@ jobs: - name: Install Dependencies run: uv pip install -r requirements-tests.txt - name: Lint + if: matrix.python-version != '3.7' && matrix.python-version != '3.8' && matrix.python-version != '3.9' run: bash scripts/lint.sh - run: mkdir coverage - run: bash ./scripts/test-files.sh diff --git a/docs/tutorial/testing.md b/docs/tutorial/testing.md index c2671f79db..b9b2eb24fd 100644 --- a/docs/tutorial/testing.md +++ b/docs/tutorial/testing.md @@ -71,7 +71,7 @@ Then we check that the text printed to "standard output" contains the text that /// tip -You could also check output sent to "standard error" (`stderr`) instead of "standard output" (`stdout`). To do so, make sure your `CliRunner` instance is created with the `mix_stderr=False` argument. You need this setting to be able to access `result.stderr` in tests. +You could also check the output sent to "standard error" (`stderr`) or "standard output" (`stdout`) independently by accessing `result.stdout` and `result.stderr` in your tests. /// diff --git a/docs_src/testing/app01/test_main.py b/docs_src/testing/app01/test_main.py index 95716239c3..81d6a9cd3b 100644 --- a/docs_src/testing/app01/test_main.py +++ b/docs_src/testing/app01/test_main.py @@ -8,5 +8,5 @@ def test_app(): result = runner.invoke(app, ["Camila", "--city", "Berlin"]) assert result.exit_code == 0 - assert "Hello Camila" in result.stdout - assert "Let's have a coffee in Berlin" in result.stdout + assert "Hello Camila" in result.output + assert "Let's have a coffee in Berlin" in result.output diff --git a/docs_src/testing/app02/test_main.py b/docs_src/testing/app02/test_main.py index ffe01743ee..f28708ba5c 100644 --- a/docs_src/testing/app02/test_main.py +++ b/docs_src/testing/app02/test_main.py @@ -8,4 +8,4 @@ def test_app(): result = runner.invoke(app, ["Camila"], input="camila@example.com\n") assert result.exit_code == 0 - assert "Hello Camila, your email is: camila@example.com" in result.stdout + assert "Hello Camila, your email is: camila@example.com" in result.output diff --git a/docs_src/testing/app02_an/test_main.py b/docs_src/testing/app02_an/test_main.py index ffe01743ee..f28708ba5c 100644 --- a/docs_src/testing/app02_an/test_main.py +++ b/docs_src/testing/app02_an/test_main.py @@ -8,4 +8,4 @@ def test_app(): result = runner.invoke(app, ["Camila"], input="camila@example.com\n") assert result.exit_code == 0 - assert "Hello Camila, your email is: camila@example.com" in result.stdout + assert "Hello Camila, your email is: camila@example.com" in result.output diff --git a/docs_src/testing/app03/test_main.py b/docs_src/testing/app03/test_main.py index 425c491f11..f98fb0a734 100644 --- a/docs_src/testing/app03/test_main.py +++ b/docs_src/testing/app03/test_main.py @@ -12,4 +12,4 @@ def test_app(): result = runner.invoke(app, ["--name", "Camila"]) assert result.exit_code == 0 - assert "Hello Camila" in result.stdout + assert "Hello Camila" in result.output diff --git a/pyproject.toml b/pyproject.toml index bbf738edc9..c0ca3aa418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", ] dependencies = [ - "click >= 8.0.0,<8.2", + "click >= 8.0.0", "typing-extensions >= 3.7.4.3", ] readme = "README.md" diff --git a/tests/test_completion/test_completion_show.py b/tests/test_completion/test_completion_show.py index af4ed2a90e..10d8b6ff39 100644 --- a/tests/test_completion/test_completion_show.py +++ b/tests/test_completion/test_completion_show.py @@ -146,4 +146,4 @@ def test_completion_show_invalid_shell(): shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell") ): result = runner.invoke(app, ["--show-completion"]) - assert "Shell xshell not supported" in result.stdout + assert "Shell xshell not supported" in result.output diff --git a/tests/test_tutorial/test_commands/test_index/test_tutorial003.py b/tests/test_tutorial/test_commands/test_index/test_tutorial003.py index 8855d3ba53..132f4ffe60 100644 --- a/tests/test_tutorial/test_commands/test_index/test_tutorial003.py +++ b/tests/test_tutorial/test_commands/test_index/test_tutorial003.py @@ -12,7 +12,6 @@ def test_no_arg(): result = runner.invoke(app) - assert result.exit_code == 0 assert "[OPTIONS] COMMAND [ARGS]..." in result.output assert "Commands" in result.output assert "create" in result.output diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial001.py b/tests/test_tutorial/test_options/test_callback/test_tutorial001.py index 5c2c19ed77..0a8b79ba27 100644 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial001.py +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial001.py @@ -21,7 +21,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_script(): diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py index 5bbe0b08a0..188f7cecf6 100644 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py @@ -21,7 +21,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_script(): diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial003.py b/tests/test_tutorial/test_options/test_callback/test_tutorial003.py index cbdaa8d094..8bc3c967ab 100644 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial003.py +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial003.py @@ -23,7 +23,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_script(): diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py index a80dbcd2bd..c3ca675231 100644 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py @@ -23,7 +23,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_script(): diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial004.py b/tests/test_tutorial/test_options/test_callback/test_tutorial004.py index 70f9c1d399..494a4f802b 100644 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial004.py +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial004.py @@ -23,7 +23,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_script(): diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py index 7756b202d6..1132d88f5a 100644 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py @@ -23,7 +23,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_script(): diff --git a/tests/test_tutorial/test_options/test_required/test_tutorial001.py b/tests/test_tutorial/test_options/test_required/test_tutorial001.py index c4c84ce451..609592457d 100644 --- a/tests/test_tutorial/test_options/test_required/test_tutorial001.py +++ b/tests/test_tutorial/test_options/test_required/test_tutorial001.py @@ -16,7 +16,7 @@ def test_1(): result = runner.invoke(app, ["Camila"]) assert result.exit_code != 0 - assert "Missing option '--lastname'." in result.output + assert "Missing option '--lastname'" in result.output def test_option_lastname(): diff --git a/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py index 259609f0de..6ea6cf0652 100644 --- a/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py +++ b/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py @@ -16,7 +16,7 @@ def test_1(): result = runner.invoke(app, ["Camila"]) assert result.exit_code != 0 - assert "Missing option '--lastname'." in result.output + assert "Missing option '--lastname'" in result.output def test_option_lastname(): diff --git a/tests/test_tutorial/test_options/test_version/test_tutorial003.py b/tests/test_tutorial/test_options/test_version/test_tutorial003.py index bcede6fc28..ede4d14fe8 100644 --- a/tests/test_tutorial/test_options/test_version/test_tutorial003.py +++ b/tests/test_tutorial/test_options/test_version/test_tutorial003.py @@ -22,7 +22,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_3(): diff --git a/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py index 5ccf2c922a..d120bdcce3 100644 --- a/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py +++ b/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py @@ -22,7 +22,8 @@ def test_1(): def test_2(): result = runner.invoke(app, ["--name", "rick"]) assert result.exit_code != 0 - assert "Invalid value for '--name': Only Camila is allowed" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output def test_3(): diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py index 67cf7b915a..567a9d3486 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py @@ -26,10 +26,17 @@ def test_main(): assert "Training neural network of type: conv" in result.output +def test_main_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Training neural network of type: simple" in result.output + + def test_invalid_case(): result = runner.invoke(app, ["--network", "CONV"]) assert result.exit_code != 0 - assert "Invalid value for '--network': 'CONV' is not one of" in result.output + assert "Invalid value for '--network'" in result.output + assert "'CONV' is not one of" in result.output assert "simple" in result.output assert "conv" in result.output assert "lstm" in result.output @@ -38,7 +45,8 @@ def test_invalid_case(): def test_invalid_other(): result = runner.invoke(app, ["--network", "capsule"]) assert result.exit_code != 0 - assert "Invalid value for '--network': 'capsule' is not one of" in result.output + assert "Invalid value for '--network'" in result.output + assert "'capsule' is not one of" in result.output assert "simple" in result.output assert "conv" in result.output assert "lstm" in result.output diff --git a/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py index e7ac0a1d4f..a53450ba8e 100644 --- a/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py @@ -35,7 +35,8 @@ def test_params(): def test_invalid(): result = runner.invoke(app, ["Camila", "--age", "15.3"]) assert result.exit_code != 0 - assert "Invalid value for '--age': '15.3' is not a valid integer" in result.output + assert "Invalid value for '--age'" in result.output + assert "'15.3' is not a valid integer" in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py index 7269255a9b..beec19f976 100644 --- a/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py @@ -53,16 +53,15 @@ def test_invalid_id(): def test_invalid_age(): result = runner.invoke(app, ["5", "--age", "15"]) assert result.exit_code != 0 - assert "Invalid value for '--age': 15 is not in the range x>=18" in result.output + assert "Invalid value for '--age'" in result.output + assert "15 is not in the range x>=18" in result.output def test_invalid_score(): result = runner.invoke(app, ["5", "--age", "20", "--score", "100.5"]) assert result.exit_code != 0 - assert ( - "Invalid value for '--score': 100.5 is not in the range x<=100." - in result.output - ) + assert "Invalid value for '--score'" in result.output + assert "100.5 is not in the range x<=100." in result.output def test_negative_score(): diff --git a/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py index 6e354f77ae..bec9b6c4fa 100644 --- a/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py +++ b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py @@ -53,16 +53,15 @@ def test_invalid_id(): def test_invalid_age(): result = runner.invoke(app, ["5", "--age", "15"]) assert result.exit_code != 0 - assert "Invalid value for '--age': 15 is not in the range x>=18" in result.output + assert "Invalid value for '--age'" in result.output + assert "15 is not in the range x>=18" in result.output def test_invalid_score(): result = runner.invoke(app, ["5", "--age", "20", "--score", "100.5"]) assert result.exit_code != 0 - assert ( - "Invalid value for '--score': 100.5 is not in the range x<=100." - in result.output - ) + assert "Invalid value for '--score'" in result.output + assert "100.5 is not in the range x<=100." in result.output def test_negative_score(): diff --git a/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002.py index e9dadce492..c765965709 100644 --- a/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002.py +++ b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002.py @@ -19,7 +19,8 @@ def test_not_exists(tmpdir): config_file.unlink() result = runner.invoke(app, ["--config", f"{config_file}"]) assert result.exit_code != 0 - assert "Invalid value for '--config': File" in result.output + assert "Invalid value for '--config'" in result.output + assert "File" in result.output assert "does not exist" in result.output @@ -35,7 +36,8 @@ def test_exists(tmpdir): def test_dir(): result = runner.invoke(app, ["--config", "./"]) assert result.exit_code != 0 - assert "Invalid value for '--config': File './' is a directory." in result.output + assert "Invalid value for '--config'" in result.output + assert "File './' is a directory." in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py index b727e4de55..0defc6d3f4 100644 --- a/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py +++ b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py @@ -19,7 +19,8 @@ def test_not_exists(tmpdir): config_file.unlink() result = runner.invoke(app, ["--config", f"{config_file}"]) assert result.exit_code != 0 - assert "Invalid value for '--config': File" in result.output + assert "Invalid value for '--config'" in result.output + assert "File" in result.output assert "does not exist" in result.output @@ -35,7 +36,8 @@ def test_exists(tmpdir): def test_dir(): result = runner.invoke(app, ["--config", "./"]) assert result.exit_code != 0 - assert "Invalid value for '--config': File './' is a directory." in result.output + assert "Invalid value for '--config'" in result.output + assert "File './' is a directory." in result.output def test_script(): diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 0000000000..adb100eb82 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,34 @@ +from enum import Enum + +import typer +from typer.testing import CliRunner + +app = typer.Typer(context_settings={"token_normalize_func": str.lower}) + + +class User(str, Enum): + rick = "Rick" + morty = "Morty" + + +@app.command() +def hello(name: User = User.rick) -> None: + print(f"Hello {name.value}!") + + +runner = CliRunner() + + +def test_enum_choice() -> None: + # This test is only for coverage of the new custom TyperChoice class + result = runner.invoke(app, ["--name", "morty"], catch_exceptions=False) + assert result.exit_code == 0 + assert "Hello Morty!" in result.output + + result = runner.invoke(app, ["--name", "Rick"]) + assert result.exit_code == 0 + assert "Hello Rick!" in result.output + + result = runner.invoke(app, ["--name", "RICK"]) + assert result.exit_code == 0 + assert "Hello Rick!" in result.output diff --git a/typer/_completion_classes.py b/typer/_completion_classes.py index 070bbaa214..5980248afe 100644 --- a/typer/_completion_classes.py +++ b/typer/_completion_classes.py @@ -16,6 +16,14 @@ Shells, ) +try: + from click.shell_completion import split_arg_string as click_split_arg_string +except ImportError: # pragma: no cover + # TODO: when removing support for Click < 8.2, remove this import + from click.parser import ( # type: ignore[no-redef] + split_arg_string as click_split_arg_string, + ) + try: import shellingham except ImportError: # pragma: no cover @@ -43,7 +51,7 @@ def source_vars(self) -> Dict[str, Any]: } def get_completion_args(self) -> Tuple[List[str], str]: - cwords = click.parser.split_arg_string(os.environ["COMP_WORDS"]) + cwords = click_split_arg_string(os.environ["COMP_WORDS"]) cword = int(os.environ["COMP_CWORD"]) args = cwords[1:cword] @@ -80,7 +88,7 @@ def source_vars(self) -> Dict[str, Any]: def get_completion_args(self) -> Tuple[List[str], str]: completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "") - cwords = click.parser.split_arg_string(completion_args) + cwords = click_split_arg_string(completion_args) args = cwords[1:] if args and not completion_args.endswith(" "): incomplete = args[-1] @@ -131,7 +139,7 @@ def source_vars(self) -> Dict[str, Any]: def get_completion_args(self) -> Tuple[List[str], str]: completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "") - cwords = click.parser.split_arg_string(completion_args) + cwords = click_split_arg_string(completion_args) args = cwords[1:] if args and not completion_args.endswith(" "): incomplete = args[-1] @@ -185,7 +193,7 @@ def source_vars(self) -> Dict[str, Any]: def get_completion_args(self) -> Tuple[List[str], str]: completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "") incomplete = os.getenv("_TYPER_COMPLETE_WORD_TO_COMPLETE", "") - cwords = click.parser.split_arg_string(completion_args) + cwords = click_split_arg_string(completion_args) args = cwords[1:-1] if incomplete else cwords[1:] return args, incomplete diff --git a/typer/_types.py b/typer/_types.py new file mode 100644 index 0000000000..045e36b815 --- /dev/null +++ b/typer/_types.py @@ -0,0 +1,27 @@ +from enum import Enum +from typing import Generic, TypeVar, Union + +import click + +ParamTypeValue = TypeVar("ParamTypeValue") + + +class TyperChoice(click.Choice, Generic[ParamTypeValue]): # type: ignore[type-arg] + def normalize_choice( + self, choice: ParamTypeValue, ctx: Union[click.Context, None] + ) -> str: + # Click 8.2.0 added a new method `normalize_choice` to the `Choice` class + # to support enums, but it uses the enum names, while Typer has always used the + # enum values. + # This class overrides that method to maintain the previous behavior. + # In Click: + # normed_value = choice.name if isinstance(choice, Enum) else str(choice) + normed_value = str(choice.value) if isinstance(choice, Enum) else str(choice) + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(normed_value) + + if not self.case_sensitive: + normed_value = normed_value.casefold() + + return normed_value diff --git a/typer/core.py b/typer/core.py index bb5aa48dd0..f6c4f72e8a 100644 --- a/typer/core.py +++ b/typer/core.py @@ -329,7 +329,7 @@ def get_help_record(self, ctx: click.Context) -> Optional[Tuple[str, str]]: # to support Arguments if self.hidden: return None - name = self.make_metavar() + name = self.make_metavar(ctx=ctx) help = self.help or "" extra = [] if self.show_envvar: @@ -375,7 +375,7 @@ def get_help_record(self, ctx: click.Context) -> Optional[Tuple[str, str]]: help = f"{help} {extra_str}" if help else f"{extra_str}" return name, help - def make_metavar(self) -> str: + def make_metavar(self, ctx: Union[click.Context, None] = None) -> str: # Modified version of click.core.Argument.make_metavar() # to include Argument name if self.metavar is not None: @@ -383,7 +383,16 @@ def make_metavar(self) -> str: var = (self.name or "").upper() if not self.required: var = f"[{var}]" - type_var = self.type.get_metavar(self) + # TODO: When deprecating Click < 8.2, remove this + signature = inspect.signature(self.type.get_metavar) + if "ctx" in signature.parameters: + # Click >= 8.2 + type_var = self.type.get_metavar(self, ctx=ctx) # type: ignore[arg-type] + else: + # Click < 8.2 + type_var = self.type.get_metavar(self) # type: ignore[call-arg] + # TODO: /When deprecating Click < 8.2, remove this, uncomment the line below + # type_var = self.type.get_metavar(self, ctx=ctx) if type_var: var += f":{type_var}" if self.nargs != 1: @@ -480,6 +489,14 @@ def _extract_default_help_str( ) -> Optional[Union[Any, Callable[[], Any]]]: return _extract_default_help_str(self, ctx=ctx) + def make_metavar(self, ctx: Union[click.Context, None] = None) -> str: + signature = inspect.signature(super().make_metavar) + if "ctx" in signature.parameters: + # Click >= 8.2 + return super().make_metavar(ctx=ctx) # type: ignore[arg-type] + # Click < 8.2 + return super().make_metavar() # type: ignore[call-arg] + def get_help_record(self, ctx: click.Context) -> Optional[Tuple[str, str]]: # Duplicate all of Click's logic only to modify a single line, to allow boolean # flags with only names for False values as it's currently supported by Typer @@ -498,7 +515,7 @@ def _write_opts(opts: Sequence[str]) -> str: any_prefix_is_slash = True if not self.is_flag and not self.count: - rv += f" {self.make_metavar()}" + rv += f" {self.make_metavar(ctx=ctx)}" return rv diff --git a/typer/main.py b/typer/main.py index 508d96617e..59e22c77aa 100644 --- a/typer/main.py +++ b/typer/main.py @@ -15,6 +15,7 @@ from uuid import UUID import click +from typer._types import TyperChoice from ._typing import get_args, get_origin, is_union from .completion import get_completion_inspect_parameters @@ -787,7 +788,12 @@ def get_click_type( atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, Enum): - return click.Choice( + # The custom TyperChoice is only needed for Click < 8.2.0, to parse the + # command line values matching them to the enum values. Click 8.2.0 added + # support for enum values but reading enum names. + # Passing here the list of enum values (instead of just the enum) accounts for + # Click < 8.2.0. + return TyperChoice( [item.value for item in annotation], case_sensitive=parameter_info.case_sensitive, ) diff --git a/typer/rich_utils.py b/typer/rich_utils.py index 4a8ffbeeb0..4b6c5a840f 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -370,7 +370,13 @@ def _print_options_panel( # Column for a metavar, if we have one metavar = Text(style=STYLE_METAVAR, overflow="fold") - metavar_str = param.make_metavar() + # TODO: when deprecating Click < 8.2, make ctx required + signature = inspect.signature(param.make_metavar) + if "ctx" in signature.parameters: + metavar_str = param.make_metavar(ctx=ctx) + else: + # Click < 8.2 + metavar_str = param.make_metavar() # type: ignore[call-arg] # Do it ourselves if this is a positional argument if (